Skip to content

Commit 713cbc1

Browse files
authored
refactor: remove schema helper API (#137)
1 parent c0b5dd5 commit 713cbc1

16 files changed

Lines changed: 143 additions & 469 deletions

‎README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# Agent Client Protocol (Python)
66

7-
Build ACP-compliant agents and clients in Python with generated schema models, asyncio transports, helper builders, and runnable demos.
7+
Build ACP-compliant agents and clients in Python with generated schema models, asyncio transports, and runnable demos.
88

99
> Releases track the upstream ACP schema; contributions that tighten coverage or tooling are always welcome.
1010
@@ -19,14 +19,14 @@ uv add agent-client-protocol
1919
## At a glance
2020

2121
- **Spec parity:** Generated Pydantic models in `acp.schema` track every ACP release so payloads stay valid.
22+
- **Direct construction:** Generated discriminator fields have defaults, so models can be instantiated directly.
2223
- **Runtime ergonomics:** Async base classes, stdio JSON-RPC plumbing, and lifecycle helpers keep custom agents tiny.
2324
- **Examples ready:** Streaming, permissions, Gemini bridge, and duet demos live under `examples/`.
24-
- **Helper builders:** `acp.helpers` mirrors the Go/TS SDK APIs for content blocks, tool calls, and session updates.
2525
- **Contrib utilities:** Session accumulators, tool call trackers, and permission brokers share patterns from real deployments.
2626

2727
## Who benefits
2828

29-
- Agent authors who need typed models, helper builders, and event-stream ergonomics for ACP-compatible assistants.
29+
- Agent authors who need typed models and event-stream ergonomics for ACP-compatible assistants.
3030
- Client integrators embedding ACP parties inside Python applications or wrapping existing CLIs via stdio.
3131
- Tooling teams experimenting with permission flows, streaming UX, or Gemini bridges without re-implementing transports.
3232
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
5252

5353
## Project layout
5454

55-
- `src/acp/`: runtime package (agents, clients, transports, helpers, schema bindings, contrib utilities)
55+
- `src/acp/`: runtime package (agents, clients, transports, schema bindings, contrib utilities)
5656
- `schema/`: upstream JSON schema sources (regenerate via `make gen-all`)
5757
- `docs/`: MkDocs content backing the published documentation
5858
- `examples/`: runnable scripts covering stdio orchestration patterns

‎docs/index.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ Next steps live in the [Quickstart](quickstart.md): launch the echo agent, wire
2424

2525
## SDK building blocks
2626

27-
- `acp.schema`: generated Pydantic models that validate every payload against the canonical specification.
27+
- `acp.schema`: generated Pydantic models with defaults for protocol discriminator fields.
2828
- `acp.agent` / `acp.client`: async base classes, JSON-RPC supervision, and lifecycle orchestration.
29-
- `acp.helpers`: builders for content blocks, tool calls, permissions, and notifications.
3029
- `acp.contrib`: experimental utilities (session accumulators, permission brokers, tool call trackers) harvested from production deployments.
3130
- `examples/`: runnable agents, clients, duet demos, and the Gemini CLI bridge.
3231

@@ -44,7 +43,7 @@ Next steps live in the [Quickstart](quickstart.md): launch the echo agent, wire
4443
## Choose a path
4544

4645
- **Just exploring?** Skim [use-cases.md](use-cases.md) to see how kimi-cli, agent-client-kernel, and others use the SDK.
47-
- **Building agents?** Copy `examples/echo_agent.py` or `examples/agent.py`, then layer in `acp.helpers` for tool calls and permissions.
46+
- **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.
4847
- **Embedding clients?** Start with `examples/client.py` or the `spawn_agent_process` / `spawn_client_process` helpers in the [Quickstart](quickstart.md#programmatic-launch).
4948

5049
## Reference material

‎docs/quickstart.md‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,9 @@ import sys
9393
from pathlib import Path
9494
from typing import Any
9595

96-
from acp import PROTOCOL_VERSION, spawn_agent_process, text_block
96+
from acp import PROTOCOL_VERSION, spawn_agent_process
9797
from acp.interfaces import Client
98+
from acp.schema import TextContentBlock
9899

99100

100101
class SimpleClient(Client):
@@ -114,7 +115,7 @@ async def main() -> None:
114115
session = await conn.new_session(cwd=str(script.parent), mcp_servers=[])
115116
await conn.prompt(
116117
session_id=session.session_id,
117-
prompt=[text_block("Hello from spawn!")],
118+
prompt=[TextContentBlock(text="Hello from spawn!")],
118119
)
119120

120121
asyncio.run(main())
@@ -145,21 +146,17 @@ Run it with `run_agent()` inside an async entrypoint and wire it to your client.
145146
- [`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
146147
- [`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
147148

148-
Need builders for common payloads? `acp.helpers` mirrors the Go/TS helper APIs:
149+
Generated models provide defaults for discriminator fields such as `type` and
150+
`sessionUpdate`, so they can be constructed directly:
149151

150152
```python
151-
from acp import start_tool_call, update_tool_call, text_block, tool_content
153+
from acp.schema import AgentMessageChunk, TextContentBlock
152154

153-
start_update = start_tool_call("call-42", "Open file", kind="read", status="pending")
154-
finish_update = update_tool_call(
155-
"call-42",
156-
status="completed",
157-
content=[tool_content(text_block("File opened."))],
155+
update = AgentMessageChunk(
156+
content=TextContentBlock(text="File opened."),
158157
)
159158
```
160159

161-
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.
162-
163160
## Optional — Talk to the Gemini CLI
164161

165162
_Have the Gemini CLI installed? Run the bridge to exercise permission flows._

‎examples/agent.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
PromptResponse,
1313
SetSessionModeResponse,
1414
run_agent,
15-
text_block,
16-
update_agent_message,
1715
)
1816
from acp.interfaces import Client
1917
from acp.schema import (
@@ -43,7 +41,7 @@ def on_connect(self, conn: Client) -> None:
4341
self._conn = conn
4442

4543
async def _send_agent_message(self, session_id: str, content: Any) -> None:
46-
update = content if isinstance(content, AgentMessageChunk) else update_agent_message(content)
44+
update = content if isinstance(content, AgentMessageChunk) else AgentMessageChunk(content=content)
4745
await self._conn.session_update(session_id, update)
4846

4947
async def initialize(
@@ -109,7 +107,7 @@ async def prompt(
109107
if session_id not in self._sessions:
110108
self._sessions.add(session_id)
111109

112-
await self._send_agent_message(session_id, text_block("Client sent:"))
110+
await self._send_agent_message(session_id, TextContentBlock(text="Client sent:"))
113111
for block in prompt:
114112
await self._send_agent_message(session_id, block)
115113
return PromptResponse(stop_reason="end_turn")

‎examples/client.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
Client,
1313
RequestError,
1414
connect_to_agent,
15-
text_block,
1615
)
1716
from acp.core import ClientSideConnection
1817
from acp.schema import (
@@ -170,7 +169,7 @@ async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None:
170169
try:
171170
await conn.prompt(
172171
session_id=session_id,
173-
prompt=[text_block(line)],
172+
prompt=[TextContentBlock(text=line)],
174173
)
175174
except Exception as exc:
176175
logging.error("Prompt failed: %s", exc) # noqa: TRY400

‎examples/echo_agent.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414
NewSessionResponse,
1515
PromptResponse,
1616
run_agent,
17-
text_block,
18-
update_agent_message,
1917
)
2018
from acp.interfaces import Client
2119
from acp.schema import (
20+
AgentMessageChunk,
2221
AudioContentBlock,
2322
ClientCapabilities,
2423
EmbeddedResourceContentBlock,
@@ -70,7 +69,7 @@ async def prompt(
7069
) -> PromptResponse:
7170
for block in prompt:
7271
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
73-
chunk = update_agent_message(text_block(text))
72+
chunk = AgentMessageChunk(content=TextContentBlock(text=text))
7473
chunk.field_meta = {"echo": True}
7574
chunk.content.field_meta = {"echo": True}
7675

‎examples/gemini.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
Client,
1818
RequestError,
1919
connect_to_agent,
20-
text_block,
2120
)
2221
from acp.core import ClientSideConnection
2322
from acp.schema import (
@@ -254,7 +253,7 @@ def _print_text_content(content: object) -> None:
254253
async def _send_prompt(conn: ClientSideConnection, session_id: str, prompt: str, timeout: float | None) -> None:
255254
request = conn.prompt(
256255
session_id=session_id,
257-
prompt=[text_block(prompt)],
256+
prompt=[TextContentBlock(text=prompt)],
258257
)
259258
if timeout is None:
260259
await request

‎examples/http_client.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
import asyncio
1313
from typing import Any
1414

15-
from acp import connect_to_agent, text_block
15+
from acp import connect_to_agent
1616
from acp.http import create_http_stream
1717
from acp.interfaces import Client
18+
from acp.schema import TextContentBlock
1819

1920

2021
class ExampleClient(Client):
@@ -43,7 +44,7 @@ async def main() -> None:
4344
print(f"initialized (protocol v{init.protocol_version})")
4445
session = await conn.new_session(cwd=".", mcp_servers=[])
4546
print(f"session: {session.session_id}")
46-
result = await conn.prompt(session_id=session.session_id, prompt=[text_block("hello over http")])
47+
result = await conn.prompt(session_id=session.session_id, prompt=[TextContentBlock(text="hello over http")])
4748
print(f"stop reason: {result.stop_reason}")
4849
finally:
4950
await conn.close()

‎examples/http_server.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,10 @@
2525
InitializeResponse,
2626
NewSessionResponse,
2727
PromptResponse,
28-
text_block,
29-
update_agent_message,
3028
)
3129
from acp.http.asgi import create_asgi_app
3230
from acp.interfaces import Client
33-
from acp.schema import ClientCapabilities, Implementation
31+
from acp.schema import AgentMessageChunk, ClientCapabilities, Implementation, TextContentBlock
3432

3533

3634
class EchoAgent(Agent):
@@ -56,7 +54,7 @@ async def prompt(self, session_id: str, prompt: list[Any], **kwargs: Any) -> Pro
5654
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
5755
await self._conn.session_update(
5856
session_id=session_id,
59-
update=update_agent_message(text_block(f"echo: {text}")),
57+
update=AgentMessageChunk(content=TextContentBlock(text=f"echo: {text}")),
6058
)
6159
return PromptResponse(stop_reason="end_turn")
6260

‎examples/ws_client.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
import asyncio
1313
from typing import Any
1414

15-
from acp import connect_to_agent, text_block
15+
from acp import connect_to_agent
1616
from acp.interfaces import Client
17+
from acp.schema import TextContentBlock
1718
from acp.ws import create_websocket_stream
1819

1920

@@ -42,7 +43,9 @@ async def main() -> None:
4243
print(f"initialized (protocol v{init.protocol_version})")
4344
session = await conn.new_session(cwd=".", mcp_servers=[])
4445
print(f"session: {session.session_id}")
45-
result = await conn.prompt(session_id=session.session_id, prompt=[text_block("hello over websocket")])
46+
result = await conn.prompt(
47+
session_id=session.session_id, prompt=[TextContentBlock(text="hello over websocket")]
48+
)
4649
print(f"stop reason: {result.stop_reason}")
4750
finally:
4851
await conn.close()

0 commit comments

Comments
 (0)