Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ If one deployment works while another fails against the same upstream provider,
- The service forwards A2A `message:send` to OpenCode session/message calls.
- Main chat requests may override the upstream model for one request through `metadata.shared.model`.
- Provider/model catalog discovery is available through `opencode.providers.list` and `opencode.models.list`.
- Main chat requests that explicitly send `configuration.acceptedOutputModes` must stay compatible with the declared chat output modes.
- Current main chat requests must continue accepting `text/plain`; requests that only accept `application/json` or other incompatible modes are rejected before execution starts.
- `application/json` is additive structured-output support for incremental `tool_call` payloads. It does not guarantee that ordinary assistant prose can always be losslessly represented as JSON, so consumers that expect normal chat text should keep accepting `text/plain`.
- Main chat input supports structured A2A `parts` passthrough:
- `TextPart` is forwarded as an OpenCode text part.
- `FilePart(FileWithBytes)` is forwarded as a `file` part with a `data:` URL.
Expand Down
8 changes: 6 additions & 2 deletions src/opencode_a2a/server/agent_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,9 @@ def _build_agent_skills(
name="OpenCode Chat",
description=(
"Handle core A2A chat turns with shared session binding and optional "
"request-scoped model selection."
"request-scoped model selection. Chat clients should continue accepting "
"text/plain responses; application/json is additive structured-output "
"support."
),
input_modes=list(_CHAT_INPUT_MODES),
output_modes=list(_CHAT_OUTPUT_MODES),
Expand Down Expand Up @@ -453,7 +455,9 @@ def _build_agent_skills(
description=(
"Handle core A2A message/send and message/stream requests by routing "
"TextPart and FilePart inputs to OpenCode sessions with shared session "
"binding and optional request-scoped model selection."
"binding and optional request-scoped model selection. Chat clients "
"should continue accepting text/plain responses; application/json is "
"additive structured-output support."
),
input_modes=list(_CHAT_INPUT_MODES),
output_modes=list(_CHAT_OUTPUT_MODES),
Expand Down
1 change: 1 addition & 0 deletions tests/server/test_agent_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ def test_agent_card_chat_examples_include_project_hint_when_configured() -> None
chat_skill = next(skill for skill in card.skills if skill.id == "opencode.chat")
assert chat_skill.examples is None
assert "shared session binding" in chat_skill.description
assert "text/plain responses" in chat_skill.description
assert "core-a2a" in chat_skill.tags
assert "portable" in chat_skill.tags

Expand Down
34 changes: 34 additions & 0 deletions tests/server/test_app_behaviors.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,40 @@ async def _setup_message_execution(self, params, context=None): # noqa: ANN001

assert isinstance(exc_info.value.error, UnsupportedOperationError)
assert "require text/plain" in exc_info.value.error.message
assert exc_info.value.error.data == {
"accepted_output_modes": ["application/json"],
"required_output_modes": ["text/plain"],
"supported_output_modes": ["text/plain", "application/json"],
}
assert handler.setup_called is False


@pytest.mark.asyncio
async def test_on_message_send_stream_rejects_incompatible_output_modes_before_execution() -> None:
class _Handler(OpencodeRequestHandler):
def __init__(self) -> None:
super().__init__(agent_executor=MagicMock(), task_store=MagicMock())
self.setup_called = False

async def _setup_message_execution(self, params, context=None): # noqa: ANN001
del params, context
self.setup_called = True
raise AssertionError("_setup_message_execution should not be called")

handler = _Handler()
params = types.SimpleNamespace(
configuration=types.SimpleNamespace(accepted_output_modes=["image/png"])
)

with pytest.raises(ServerError) as exc_info:
await handler.on_message_send_stream(params).__anext__()

assert isinstance(exc_info.value.error, UnsupportedOperationError)
assert "not compatible" in exc_info.value.error.message
assert exc_info.value.error.data == {
"accepted_output_modes": ["image/png"],
"supported_output_modes": ["text/plain", "application/json"],
}
assert handler.setup_called is False


Expand Down