From 0abac50d0d797b4a52c4c8c2f9db6dbb745b42c1 Mon Sep 17 00:00:00 2001 From: PsiACE Date: Mon, 31 Aug 2026 03:00:30 +0800 Subject: [PATCH] refactor: remove schema helper API --- README.md | 8 +- docs/index.md | 5 +- docs/quickstart.md | 19 +-- examples/agent.py | 6 +- examples/client.py | 3 +- examples/echo_agent.py | 5 +- examples/gemini.py | 3 +- examples/http_client.py | 5 +- examples/http_server.py | 6 +- examples/ws_client.py | 7 +- src/acp/__init__.py | 49 ------ src/acp/contrib/permissions.py | 12 +- src/acp/contrib/tool_calls.py | 5 +- src/acp/helpers.py | 280 --------------------------------- tests/test_golden.py | 182 ++++++++++----------- tests/test_rpc.py | 17 +- 16 files changed, 143 insertions(+), 469 deletions(-) delete mode 100644 src/acp/helpers.py diff --git a/README.md b/README.md index 99085f6..b8aaac8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Agent Client Protocol (Python) -Build ACP-compliant agents and clients in Python with generated schema models, asyncio transports, helper builders, and runnable demos. +Build ACP-compliant agents and clients in Python with generated schema models, asyncio transports, and runnable demos. > Releases track the upstream ACP schema; contributions that tighten coverage or tooling are always welcome. @@ -19,14 +19,14 @@ uv add agent-client-protocol ## At a glance - **Spec parity:** Generated Pydantic models in `acp.schema` track every ACP release so payloads stay valid. +- **Direct construction:** Generated discriminator fields have defaults, so models can be instantiated directly. - **Runtime ergonomics:** Async base classes, stdio JSON-RPC plumbing, and lifecycle helpers keep custom agents tiny. - **Examples ready:** Streaming, permissions, Gemini bridge, and duet demos live under `examples/`. -- **Helper builders:** `acp.helpers` mirrors the Go/TS SDK APIs for content blocks, tool calls, and session updates. - **Contrib utilities:** Session accumulators, tool call trackers, and permission brokers share patterns from real deployments. ## Who benefits -- Agent authors who need typed models, helper builders, and event-stream ergonomics for ACP-compatible assistants. +- Agent authors who need typed models and event-stream ergonomics for ACP-compatible assistants. - Client integrators embedding ACP parties inside Python applications or wrapping existing CLIs via stdio. - Tooling teams experimenting with permission flows, streaming UX, or Gemini bridges without re-implementing transports. See real adopters like kimi-cli in the [Use Cases list](https://agentclientprotocol.github.io/python-sdk/use-cases/). @@ -52,7 +52,7 @@ See real adopters like kimi-cli in the [Use Cases list](https://agentclientproto ## Project layout -- `src/acp/`: runtime package (agents, clients, transports, helpers, schema bindings, contrib utilities) +- `src/acp/`: runtime package (agents, clients, transports, schema bindings, contrib utilities) - `schema/`: upstream JSON schema sources (regenerate via `make gen-all`) - `docs/`: MkDocs content backing the published documentation - `examples/`: runnable scripts covering stdio orchestration patterns diff --git a/docs/index.md b/docs/index.md index 88dc076..ba1c710 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,9 +24,8 @@ Next steps live in the [Quickstart](quickstart.md): launch the echo agent, wire ## SDK building blocks -- `acp.schema`: generated Pydantic models that validate every payload against the canonical specification. +- `acp.schema`: generated Pydantic models with defaults for protocol discriminator fields. - `acp.agent` / `acp.client`: async base classes, JSON-RPC supervision, and lifecycle orchestration. -- `acp.helpers`: builders for content blocks, tool calls, permissions, and notifications. - `acp.contrib`: experimental utilities (session accumulators, permission brokers, tool call trackers) harvested from production deployments. - `examples/`: runnable agents, clients, duet demos, and the Gemini CLI bridge. @@ -44,7 +43,7 @@ Next steps live in the [Quickstart](quickstart.md): launch the echo agent, wire ## Choose a path - **Just exploring?** Skim [use-cases.md](use-cases.md) to see how kimi-cli, agent-client-kernel, and others use the SDK. -- **Building agents?** Copy `examples/echo_agent.py` or `examples/agent.py`, then layer in `acp.helpers` for tool calls and permissions. +- **Building agents?** Copy `examples/echo_agent.py` or `examples/agent.py`, then use the generated models directly. Use `acp.contrib` for permission workflows and other stateful patterns. - **Embedding clients?** Start with `examples/client.py` or the `spawn_agent_process` / `spawn_client_process` helpers in the [Quickstart](quickstart.md#programmatic-launch). ## Reference material diff --git a/docs/quickstart.md b/docs/quickstart.md index 04ef33d..0c52c73 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -93,8 +93,9 @@ import sys from pathlib import Path from typing import Any -from acp import PROTOCOL_VERSION, spawn_agent_process, text_block +from acp import PROTOCOL_VERSION, spawn_agent_process from acp.interfaces import Client +from acp.schema import TextContentBlock class SimpleClient(Client): @@ -114,7 +115,7 @@ async def main() -> None: session = await conn.new_session(cwd=str(script.parent), mcp_servers=[]) await conn.prompt( session_id=session.session_id, - prompt=[text_block("Hello from spawn!")], + prompt=[TextContentBlock(text="Hello from spawn!")], ) asyncio.run(main()) @@ -145,21 +146,17 @@ Run it with `run_agent()` inside an async entrypoint and wire it to your client. - [`examples/duet.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/duet.py) to see `spawn_agent_process` in action alongside the interactive client - [`examples/gemini.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/gemini.py) to drive the Gemini CLI (`--acp`; use `--experimental-acp` for older versions) directly from Python -Need builders for common payloads? `acp.helpers` mirrors the Go/TS helper APIs: +Generated models provide defaults for discriminator fields such as `type` and +`sessionUpdate`, so they can be constructed directly: ```python -from acp import start_tool_call, update_tool_call, text_block, tool_content +from acp.schema import AgentMessageChunk, TextContentBlock -start_update = start_tool_call("call-42", "Open file", kind="read", status="pending") -finish_update = update_tool_call( - "call-42", - status="completed", - content=[tool_content(text_block("File opened."))], +update = AgentMessageChunk( + content=TextContentBlock(text="File opened."), ) ``` -Each helper wraps the generated Pydantic models in `acp.schema`, so the right discriminator fields (`type`, `sessionUpdate`, and friends) are always populated. That keeps examples readable while maintaining the same validation guarantees as constructing the models directly. Golden fixtures in `tests/test_golden.py` ensure the helpers stay in sync with future schema revisions. - ## Optional — Talk to the Gemini CLI _Have the Gemini CLI installed? Run the bridge to exercise permission flows._ diff --git a/examples/agent.py b/examples/agent.py index 9ee0272..d032c40 100644 --- a/examples/agent.py +++ b/examples/agent.py @@ -12,8 +12,6 @@ PromptResponse, SetSessionModeResponse, run_agent, - text_block, - update_agent_message, ) from acp.interfaces import Client from acp.schema import ( @@ -43,7 +41,7 @@ def on_connect(self, conn: Client) -> None: self._conn = conn async def _send_agent_message(self, session_id: str, content: Any) -> None: - update = content if isinstance(content, AgentMessageChunk) else update_agent_message(content) + update = content if isinstance(content, AgentMessageChunk) else AgentMessageChunk(content=content) await self._conn.session_update(session_id, update) async def initialize( @@ -109,7 +107,7 @@ async def prompt( if session_id not in self._sessions: self._sessions.add(session_id) - await self._send_agent_message(session_id, text_block("Client sent:")) + await self._send_agent_message(session_id, TextContentBlock(text="Client sent:")) for block in prompt: await self._send_agent_message(session_id, block) return PromptResponse(stop_reason="end_turn") diff --git a/examples/client.py b/examples/client.py index 653aaad..bc961dd 100644 --- a/examples/client.py +++ b/examples/client.py @@ -12,7 +12,6 @@ Client, RequestError, connect_to_agent, - text_block, ) from acp.core import ClientSideConnection from acp.schema import ( @@ -170,7 +169,7 @@ async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None: try: await conn.prompt( session_id=session_id, - prompt=[text_block(line)], + prompt=[TextContentBlock(text=line)], ) except Exception as exc: logging.error("Prompt failed: %s", exc) # noqa: TRY400 diff --git a/examples/echo_agent.py b/examples/echo_agent.py index ffb84bc..24b5372 100644 --- a/examples/echo_agent.py +++ b/examples/echo_agent.py @@ -14,11 +14,10 @@ NewSessionResponse, PromptResponse, run_agent, - text_block, - update_agent_message, ) from acp.interfaces import Client from acp.schema import ( + AgentMessageChunk, AudioContentBlock, ClientCapabilities, EmbeddedResourceContentBlock, @@ -70,7 +69,7 @@ async def prompt( ) -> PromptResponse: for block in prompt: text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "") - chunk = update_agent_message(text_block(text)) + chunk = AgentMessageChunk(content=TextContentBlock(text=text)) chunk.field_meta = {"echo": True} chunk.content.field_meta = {"echo": True} diff --git a/examples/gemini.py b/examples/gemini.py index ebbb244..c803982 100644 --- a/examples/gemini.py +++ b/examples/gemini.py @@ -17,7 +17,6 @@ Client, RequestError, connect_to_agent, - text_block, ) from acp.core import ClientSideConnection from acp.schema import ( @@ -254,7 +253,7 @@ def _print_text_content(content: object) -> None: async def _send_prompt(conn: ClientSideConnection, session_id: str, prompt: str, timeout: float | None) -> None: request = conn.prompt( session_id=session_id, - prompt=[text_block(prompt)], + prompt=[TextContentBlock(text=prompt)], ) if timeout is None: await request diff --git a/examples/http_client.py b/examples/http_client.py index 1843c01..c8f8f0e 100644 --- a/examples/http_client.py +++ b/examples/http_client.py @@ -12,9 +12,10 @@ import asyncio from typing import Any -from acp import connect_to_agent, text_block +from acp import connect_to_agent from acp.http import create_http_stream from acp.interfaces import Client +from acp.schema import TextContentBlock class ExampleClient(Client): @@ -43,7 +44,7 @@ async def main() -> None: print(f"initialized (protocol v{init.protocol_version})") session = await conn.new_session(cwd=".", mcp_servers=[]) print(f"session: {session.session_id}") - result = await conn.prompt(session_id=session.session_id, prompt=[text_block("hello over http")]) + result = await conn.prompt(session_id=session.session_id, prompt=[TextContentBlock(text="hello over http")]) print(f"stop reason: {result.stop_reason}") finally: await conn.close() diff --git a/examples/http_server.py b/examples/http_server.py index 98b4fc7..624bcee 100644 --- a/examples/http_server.py +++ b/examples/http_server.py @@ -25,12 +25,10 @@ InitializeResponse, NewSessionResponse, PromptResponse, - text_block, - update_agent_message, ) from acp.http.asgi import create_asgi_app from acp.interfaces import Client -from acp.schema import ClientCapabilities, Implementation +from acp.schema import AgentMessageChunk, ClientCapabilities, Implementation, TextContentBlock class EchoAgent(Agent): @@ -56,7 +54,7 @@ async def prompt(self, session_id: str, prompt: list[Any], **kwargs: Any) -> Pro text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "") await self._conn.session_update( session_id=session_id, - update=update_agent_message(text_block(f"echo: {text}")), + update=AgentMessageChunk(content=TextContentBlock(text=f"echo: {text}")), ) return PromptResponse(stop_reason="end_turn") diff --git a/examples/ws_client.py b/examples/ws_client.py index ac9c05c..ed2a3bd 100644 --- a/examples/ws_client.py +++ b/examples/ws_client.py @@ -12,8 +12,9 @@ import asyncio from typing import Any -from acp import connect_to_agent, text_block +from acp import connect_to_agent from acp.interfaces import Client +from acp.schema import TextContentBlock from acp.ws import create_websocket_stream @@ -42,7 +43,9 @@ async def main() -> None: print(f"initialized (protocol v{init.protocol_version})") session = await conn.new_session(cwd=".", mcp_servers=[]) print(f"session: {session.session_id}") - result = await conn.prompt(session_id=session.session_id, prompt=[text_block("hello over websocket")]) + result = await conn.prompt( + session_id=session.session_id, prompt=[TextContentBlock(text="hello over websocket")] + ) print(f"stop reason: {result.stop_reason}") finally: await conn.close() diff --git a/src/acp/__init__.py b/src/acp/__init__.py index e6cd484..ded77bb 100644 --- a/src/acp/__init__.py +++ b/src/acp/__init__.py @@ -7,31 +7,6 @@ connect_to_agent, run_agent, ) -from .helpers import ( - audio_block, - embedded_blob_resource, - embedded_text_resource, - image_block, - plan_entry, - resource_block, - resource_link_block, - session_notification, - start_edit_tool_call, - start_read_tool_call, - start_tool_call, - text_block, - tool_content, - tool_diff_content, - tool_terminal_ref, - update_agent_message, - update_agent_message_text, - update_agent_thought, - update_agent_thought_text, - update_plan, - update_tool_call, - update_user_message, - update_user_message_text, -) from .meta import ( AGENT_METHODS, CLIENT_METHODS, @@ -198,30 +173,6 @@ "spawn_client_process", "default_environment", "spawn_stdio_transport", - # helpers - "text_block", - "image_block", - "audio_block", - "resource_link_block", - "embedded_text_resource", - "embedded_blob_resource", - "resource_block", - "tool_content", - "tool_diff_content", - "tool_terminal_ref", - "plan_entry", - "update_plan", - "update_user_message", - "update_user_message_text", - "update_agent_message", - "update_agent_message_text", - "update_agent_thought", - "update_agent_thought_text", - "session_notification", - "start_tool_call", - "start_read_tool_call", - "start_edit_tool_call", - "update_tool_call", ] diff --git a/src/acp/contrib/permissions.py b/src/acp/contrib/permissions.py index 7dfc8e8..fedb00a 100644 --- a/src/acp/contrib/permissions.py +++ b/src/acp/contrib/permissions.py @@ -3,8 +3,14 @@ from collections.abc import Awaitable, Callable, Sequence from typing import Any -from ..helpers import text_block, tool_content -from ..schema import PermissionOption, RequestPermissionRequest, RequestPermissionResponse, ToolCallUpdate +from ..schema import ( + ContentToolCallContent, + PermissionOption, + RequestPermissionRequest, + RequestPermissionResponse, + TextContentBlock, + ToolCallUpdate, +) from .tool_calls import ToolCallTracker, _copy_model_list @@ -75,7 +81,7 @@ async def request_for( if description: existing = tool_call.content or [] - existing.append(tool_content(text_block(description))) + existing.append(ContentToolCallContent(content=TextContentBlock(text=description))) tool_call.content = existing option_set = tuple(option.model_copy(deep=True) for option in (options or self._default_options)) diff --git a/src/acp/contrib/tool_calls.py b/src/acp/contrib/tool_calls.py index 107d134..8776e38 100644 --- a/src/acp/contrib/tool_calls.py +++ b/src/acp/contrib/tool_calls.py @@ -6,8 +6,9 @@ from pydantic import BaseModel, ConfigDict -from ..helpers import text_block, tool_content from ..schema import ( + ContentToolCallContent, + TextContentBlock, ToolCallLocation, ToolCallProgress, ToolCallStart, @@ -173,7 +174,7 @@ def append_stream_text( status: Any = UNSET, ) -> ToolCallProgress: self._stream_buffer = (self._stream_buffer or "") + text - content = [tool_content(text_block(self._stream_buffer))] + content = [ContentToolCallContent(content=TextContentBlock(text=self._stream_buffer))] return self.update(title=title, status=status, content=content) diff --git a/src/acp/helpers.py b/src/acp/helpers.py deleted file mode 100644 index 8830c43..0000000 --- a/src/acp/helpers.py +++ /dev/null @@ -1,280 +0,0 @@ -from __future__ import annotations - -from collections.abc import Iterable, Sequence -from typing import Any - -from .schema import ( - AgentMessageChunk, - AgentPlanUpdate, - AgentThoughtChunk, - AudioContentBlock, - AvailableCommand, - AvailableCommandsUpdate, - BlobResourceContents, - ContentToolCallContent, - CurrentModeUpdate, - EmbeddedResourceContentBlock, - FileEditToolCallContent, - ImageContentBlock, - PlanEntry, - PlanEntryPriority, - PlanEntryStatus, - ResourceContentBlock, - SessionInfoUpdate, - SessionNotification, - TerminalToolCallContent, - TextContentBlock, - TextResourceContents, - ToolCallLocation, - ToolCallProgress, - ToolCallStart, - ToolCallStatus, - ToolKind, - UserMessageChunk, -) - -ContentBlock = ( - TextContentBlock | ImageContentBlock | AudioContentBlock | ResourceContentBlock | EmbeddedResourceContentBlock -) - -SessionUpdate = ( - AgentMessageChunk - | AgentPlanUpdate - | AgentThoughtChunk - | AvailableCommandsUpdate - | CurrentModeUpdate - | UserMessageChunk - | ToolCallStart - | ToolCallProgress - | SessionInfoUpdate -) - -ToolCallContentVariant = ContentToolCallContent | FileEditToolCallContent | TerminalToolCallContent - -__all__ = [ - "audio_block", - "embedded_blob_resource", - "embedded_text_resource", - "image_block", - "plan_entry", - "resource_block", - "resource_link_block", - "session_notification", - "start_edit_tool_call", - "start_read_tool_call", - "start_tool_call", - "text_block", - "tool_content", - "tool_diff_content", - "tool_terminal_ref", - "update_agent_message", - "update_agent_message_text", - "update_agent_thought", - "update_agent_thought_text", - "update_available_commands", - "update_current_mode", - "update_plan", - "update_tool_call", - "update_user_message", - "update_user_message_text", -] - - -def text_block(text: str) -> TextContentBlock: - return TextContentBlock(type="text", text=text) - - -def image_block(data: str, mime_type: str, *, uri: str | None = None) -> ImageContentBlock: - return ImageContentBlock(type="image", data=data, mime_type=mime_type, uri=uri) - - -def audio_block(data: str, mime_type: str) -> AudioContentBlock: - return AudioContentBlock(type="audio", data=data, mime_type=mime_type) - - -def resource_link_block( - name: str, - uri: str, - *, - mime_type: str | None = None, - size: int | None = None, - description: str | None = None, - title: str | None = None, -) -> ResourceContentBlock: - return ResourceContentBlock( - type="resource_link", - name=name, - uri=uri, - mime_type=mime_type, - size=size, - description=description, - title=title, - ) - - -def embedded_text_resource(uri: str, text: str, *, mime_type: str | None = None) -> TextResourceContents: - return TextResourceContents(uri=uri, text=text, mime_type=mime_type) - - -def embedded_blob_resource(uri: str, blob: str, *, mime_type: str | None = None) -> BlobResourceContents: - return BlobResourceContents(uri=uri, blob=blob, mime_type=mime_type) - - -def resource_block( - resource: TextResourceContents | BlobResourceContents, -) -> EmbeddedResourceContentBlock: - return EmbeddedResourceContentBlock(type="resource", resource=resource) - - -def tool_content(block: ContentBlock) -> ContentToolCallContent: - return ContentToolCallContent(type="content", content=block) - - -def tool_diff_content(path: str, new_text: str, old_text: str | None = None) -> FileEditToolCallContent: - return FileEditToolCallContent(type="diff", path=path, new_text=new_text, old_text=old_text) - - -def tool_terminal_ref(terminal_id: str) -> TerminalToolCallContent: - return TerminalToolCallContent(type="terminal", terminal_id=terminal_id) - - -def plan_entry( - content: str, - *, - priority: PlanEntryPriority = "medium", - status: PlanEntryStatus = "pending", -) -> PlanEntry: - return PlanEntry(content=content, priority=priority, status=status) - - -def update_plan(entries: Iterable[PlanEntry]) -> AgentPlanUpdate: - return AgentPlanUpdate(session_update="plan", entries=list(entries)) - - -def update_user_message(content: ContentBlock) -> UserMessageChunk: - return UserMessageChunk(session_update="user_message_chunk", content=content) - - -def update_user_message_text(text: str) -> UserMessageChunk: - return update_user_message(text_block(text)) - - -def update_agent_message(content: ContentBlock) -> AgentMessageChunk: - return AgentMessageChunk(session_update="agent_message_chunk", content=content) - - -def update_agent_message_text(text: str) -> AgentMessageChunk: - return update_agent_message(text_block(text)) - - -def update_agent_thought(content: ContentBlock) -> AgentThoughtChunk: - return AgentThoughtChunk(session_update="agent_thought_chunk", content=content) - - -def update_agent_thought_text(text: str) -> AgentThoughtChunk: - return update_agent_thought(text_block(text)) - - -def update_available_commands(commands: Iterable[AvailableCommand]) -> AvailableCommandsUpdate: - return AvailableCommandsUpdate( - session_update="available_commands_update", - available_commands=list(commands), - ) - - -def update_current_mode(current_mode_id: str) -> CurrentModeUpdate: - return CurrentModeUpdate(session_update="current_mode_update", current_mode_id=current_mode_id) - - -def session_notification(session_id: str, update: SessionUpdate) -> SessionNotification: - return SessionNotification(session_id=session_id, update=update) - - -def start_tool_call( - tool_call_id: str, - title: str, - *, - kind: ToolKind | None = None, - status: ToolCallStatus | None = None, - content: Sequence[ToolCallContentVariant] | None = None, - locations: Sequence[ToolCallLocation] | None = None, - raw_input: Any | None = None, - raw_output: Any | None = None, -) -> ToolCallStart: - return ToolCallStart( - session_update="tool_call", - tool_call_id=tool_call_id, - title=title, - kind=kind, - status=status, - content=list(content) if content is not None else None, - locations=list(locations) if locations is not None else None, - raw_input=raw_input, - raw_output=raw_output, - ) - - -def start_read_tool_call( - tool_call_id: str, - title: str, - path: str, - *, - extra_options: Sequence[ToolCallContentVariant] | None = None, -) -> ToolCallStart: - content = list(extra_options) if extra_options is not None else None - locations = [ToolCallLocation(path=path)] - raw_input = {"path": path} - return start_tool_call( - tool_call_id, - title, - kind="read", - status="pending", - content=content, - locations=locations, - raw_input=raw_input, - ) - - -def start_edit_tool_call( - tool_call_id: str, - title: str, - path: str, - content: Any, - *, - extra_options: Sequence[ToolCallContentVariant] | None = None, -) -> ToolCallStart: - locations = [ToolCallLocation(path=path)] - raw_input = {"path": path, "content": content} - return start_tool_call( - tool_call_id, - title, - kind="edit", - status="pending", - content=list(extra_options) if extra_options is not None else None, - locations=locations, - raw_input=raw_input, - ) - - -def update_tool_call( - tool_call_id: str, - *, - title: str | None = None, - kind: ToolKind | None = None, - status: ToolCallStatus | None = None, - content: Sequence[ToolCallContentVariant] | None = None, - locations: Sequence[ToolCallLocation] | None = None, - raw_input: Any | None = None, - raw_output: Any | None = None, -) -> ToolCallProgress: - return ToolCallProgress( - session_update="tool_call_update", - tool_call_id=tool_call_id, - title=title, - kind=kind, - status=status, - content=list(content) if content is not None else None, - locations=list(locations) if locations is not None else None, - raw_input=raw_input, - raw_output=raw_output, - ) diff --git a/tests/test_golden.py b/tests/test_golden.py index 2420ed2..1a30493 100644 --- a/tests/test_golden.py +++ b/tests/test_golden.py @@ -7,33 +7,13 @@ import pytest from pydantic import BaseModel -from acp import ( - audio_block, - embedded_blob_resource, - embedded_text_resource, - image_block, - plan_entry, - resource_block, - resource_link_block, - start_edit_tool_call, - start_read_tool_call, - start_tool_call, - text_block, - tool_content, - tool_diff_content, - tool_terminal_ref, - update_agent_message_text, - update_agent_thought_text, - update_plan, - update_tool_call, - update_user_message_text, -) from acp.schema import ( AgentMessageChunk, AgentPlanUpdate, AgentThoughtChunk, AllowedOutcome, AudioContentBlock, + BlobResourceContents, CancelNotification, ConfigOptionUpdate, ContentToolCallContent, @@ -45,6 +25,7 @@ InitializeResponse, NewSessionRequest, NewSessionResponse, + PlanEntry, PromptRequest, ReadTextFileRequest, ReadTextFileResponse, @@ -55,6 +36,7 @@ SetSessionConfigOptionSelectRequest, TerminalToolCallContent, TextContentBlock, + TextResourceContents, ToolCallLocation, ToolCallProgress, ToolCallStart, @@ -107,98 +89,122 @@ _PARAMS = tuple(sorted(GOLDEN_CASES.items())) _PARAM_IDS = [name for name, _ in _PARAMS] -GOLDEN_BUILDERS: dict[str, Callable[[], BaseModel]] = { - "content_text": lambda: text_block("What's the weather like today?"), - "content_image": lambda: image_block("iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...", "image/png"), - "content_audio": lambda: audio_block("UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB...", "audio/wav"), - "content_resource_text": lambda: resource_block( - embedded_text_resource( - "file:///home/user/script.py", - "def hello():\n print('Hello, world!')", +MODEL_BUILDERS: dict[str, Callable[[], BaseModel]] = { + "content_text": lambda: TextContentBlock(text="What's the weather like today?"), + "content_image": lambda: ImageContentBlock( + data="iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...", + mime_type="image/png", + ), + "content_audio": lambda: AudioContentBlock( + data="UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB...", + mime_type="audio/wav", + ), + "content_resource_text": lambda: EmbeddedResourceContentBlock( + resource=TextResourceContents( + uri="file:///home/user/script.py", + text="def hello():\n print('Hello, world!')", mime_type="text/x-python", ) ), - "content_resource_blob": lambda: resource_block( - embedded_blob_resource( - "file:///home/user/document.pdf", - "", + "content_resource_blob": lambda: EmbeddedResourceContentBlock( + resource=BlobResourceContents( + uri="file:///home/user/document.pdf", + blob="", mime_type="application/pdf", ) ), - "content_resource_link": lambda: resource_link_block( - "document.pdf", - "file:///home/user/document.pdf", + "content_resource_link": lambda: ResourceContentBlock( + name="document.pdf", + uri="file:///home/user/document.pdf", mime_type="application/pdf", size=1_024_000, ), - "tool_content_content_text": lambda: tool_content(text_block("Analysis complete. Found 3 issues.")), - "tool_content_diff": lambda: tool_diff_content( - "/home/user/project/src/config.json", - '{\n "debug": true\n}', - '{\n "debug": false\n}', - ), - "tool_content_diff_no_old": lambda: tool_diff_content( - "/home/user/project/src/config.json", - '{\n "debug": true\n}', - ), - "tool_content_terminal": lambda: tool_terminal_ref("term_001"), - "session_update_user_message_chunk": lambda: update_user_message_text("What's the capital of France?"), - "session_update_agent_message_chunk": lambda: update_agent_message_text("The capital of France is Paris."), - "session_update_agent_thought_chunk": lambda: update_agent_thought_text("Thinking about best approach..."), - "session_update_plan": lambda: update_plan([ - plan_entry( - "Check for syntax errors", - priority="high", - status="pending", - ), - plan_entry( - "Identify potential type issues", - priority="medium", - status="pending", - ), - ]), - "session_update_tool_call": lambda: start_tool_call( - "call_001", - "Reading configuration file", + "tool_content_content_text": lambda: ContentToolCallContent( + content=TextContentBlock(text="Analysis complete. Found 3 issues.") + ), + "tool_content_diff": lambda: FileEditToolCallContent( + path="/home/user/project/src/config.json", + new_text='{\n "debug": true\n}', + old_text='{\n "debug": false\n}', + ), + "tool_content_diff_no_old": lambda: FileEditToolCallContent( + path="/home/user/project/src/config.json", + new_text='{\n "debug": true\n}', + ), + "tool_content_terminal": lambda: TerminalToolCallContent(terminal_id="term_001"), + "session_update_user_message_chunk": lambda: UserMessageChunk( + content=TextContentBlock(text="What's the capital of France?") + ), + "session_update_agent_message_chunk": lambda: AgentMessageChunk( + content=TextContentBlock(text="The capital of France is Paris.") + ), + "session_update_agent_thought_chunk": lambda: AgentThoughtChunk( + content=TextContentBlock(text="Thinking about best approach...") + ), + "session_update_plan": lambda: AgentPlanUpdate( + entries=[ + PlanEntry( + content="Check for syntax errors", + priority="high", + status="pending", + ), + PlanEntry( + content="Identify potential type issues", + priority="medium", + status="pending", + ), + ] + ), + "session_update_tool_call": lambda: ToolCallStart( + tool_call_id="call_001", + title="Reading configuration file", kind="read", status="pending", ), - "session_update_tool_call_read": lambda: start_read_tool_call( - "call_001", - "Reading configuration file", - "/home/user/project/src/config.json", + "session_update_tool_call_read": lambda: ToolCallStart( + tool_call_id="call_001", + title="Reading configuration file", + kind="read", + status="pending", + locations=[ToolCallLocation(path="/home/user/project/src/config.json")], + raw_input={"path": "/home/user/project/src/config.json"}, ), - "session_update_tool_call_edit": lambda: start_edit_tool_call( - "call_003", - "Apply edit", - "/home/user/project/src/config.json", - "print('hello')", + "session_update_tool_call_edit": lambda: ToolCallStart( + tool_call_id="call_003", + title="Apply edit", + kind="edit", + status="pending", + locations=[ToolCallLocation(path="/home/user/project/src/config.json")], + raw_input={ + "path": "/home/user/project/src/config.json", + "content": "print('hello')", + }, ), - "session_update_tool_call_locations_rawinput": lambda: start_tool_call( - "call_lr", - "Tracking file", + "session_update_tool_call_locations_rawinput": lambda: ToolCallStart( + tool_call_id="call_lr", + title="Tracking file", locations=[ToolCallLocation(path="/home/user/project/src/config.json")], raw_input={"path": "/home/user/project/src/config.json"}, ), - "session_update_tool_call_update_content": lambda: update_tool_call( - "call_001", + "session_update_tool_call_update_content": lambda: ToolCallProgress( + tool_call_id="call_001", status="in_progress", - content=[tool_content(text_block("Found 3 configuration files..."))], + content=[ContentToolCallContent(content=TextContentBlock(text="Found 3 configuration files..."))], ), - "session_update_tool_call_update_more_fields": lambda: update_tool_call( - "call_010", + "session_update_tool_call_update_more_fields": lambda: ToolCallProgress( + tool_call_id="call_010", title="Processing changes", kind="edit", status="completed", locations=[ToolCallLocation(path="/home/user/project/src/config.json")], raw_input={"path": "/home/user/project/src/config.json"}, raw_output={"result": "ok"}, - content=[tool_content(text_block("Edit completed."))], + content=[ContentToolCallContent(content=TextContentBlock(text="Edit completed."))], ), } -_HELPER_PARAMS = tuple(sorted(GOLDEN_BUILDERS.items())) -_HELPER_IDS = [name for name, _ in _HELPER_PARAMS] +_MODEL_PARAMS = tuple(sorted(MODEL_BUILDERS.items())) +_MODEL_IDS = [name for name, _ in _MODEL_PARAMS] def _load_golden(name: str) -> dict: @@ -228,10 +234,10 @@ def test_json_golden_roundtrip(name: str, model_cls: type[BaseModel]) -> None: @pytest.mark.parametrize( ("name", "builder"), - _HELPER_PARAMS, - ids=_HELPER_IDS, + _MODEL_PARAMS, + ids=_MODEL_IDS, ) -def test_helpers_match_golden(name: str, builder: Callable[[], BaseModel]) -> None: +def test_models_match_golden(name: str, builder: Callable[[], BaseModel]) -> None: raw = _load_golden(name) model = builder() assert isinstance(model, BaseModel) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index ef4623e..611c6fd 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -28,9 +28,6 @@ SetSessionModeResponse, WriteTextFileResponse, spawn_agent_process, - start_tool_call, - update_agent_message_text, - update_tool_call, ) from acp.connection import Connection from acp.core import AgentSideConnection, ClientSideConnection @@ -754,14 +751,14 @@ async def prompt( await self._conn.session_update( session_id, - update_agent_message_text("I'll help you with that."), + AgentMessageChunk(content=TextContentBlock(text="I'll help you with that.")), ) await self._conn.session_update( session_id, - start_tool_call( - "call_1", - "Modifying configuration", + ToolCallStart( + tool_call_id="call_1", + title="Modifying configuration", kind="edit", status="pending", locations=[ToolCallLocation(path="/project/config.json")], @@ -790,15 +787,15 @@ async def prompt( if isinstance(response.outcome, AllowedOutcome) and response.outcome.option_id == "allow": await self._conn.session_update( session_id, - update_tool_call( - "call_1", + ToolCallProgress( + tool_call_id="call_1", status="completed", raw_output={"success": True}, ), ) await self._conn.session_update( session_id, - update_agent_message_text("Done."), + AgentMessageChunk(content=TextContentBlock(text="Done.")), ) return PromptResponse(stop_reason="end_turn")