Skip to content

Commit ae5e795

Browse files
authored
feat: follow agent-client-protocol 0.3.0
1 parent 273109d commit ae5e795

File tree

22 files changed

+2886
-567
lines changed

22 files changed

+2886
-567
lines changed

README.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ A Python implementation of the Agent Client Protocol (ACP). Use it to build agen
55
- Package name: `agent-client-protocol` (import as `acp`)
66
- Repository: https://github.com/psiace/agent-client-protocol-python
77
- Docs: https://psiace.github.io/agent-client-protocol-python/
8+
- Featured: Listed as the first third-party SDK on the official ACP site — see https://agentclientprotocol.com/libraries/community
89

910
## Install
1011

@@ -24,51 +25,54 @@ make test # run tests
2425

2526
## Minimal agent example
2627

28+
See a complete streaming echo example in [examples/echo_agent.py](examples/echo_agent.py). It streams back each text block using `session/update` and ends the turn.
29+
2730
```python
2831
import asyncio
2932

3033
from acp import (
3134
Agent,
3235
AgentSideConnection,
33-
AuthenticateRequest,
34-
CancelNotification,
3536
InitializeRequest,
3637
InitializeResponse,
37-
LoadSessionRequest,
3838
NewSessionRequest,
3939
NewSessionResponse,
4040
PromptRequest,
4141
PromptResponse,
42+
SessionNotification,
4243
stdio_streams,
4344
)
45+
from acp.schema import ContentBlock1, SessionUpdate2
4446

4547

4648
class EchoAgent(Agent):
49+
def __init__(self, conn):
50+
self._conn = conn
51+
4752
async def initialize(self, params: InitializeRequest) -> InitializeResponse:
4853
return InitializeResponse(protocolVersion=params.protocolVersion)
4954

5055
async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
5156
return NewSessionResponse(sessionId="sess-1")
5257

53-
async def loadSession(self, params: LoadSessionRequest) -> None:
54-
return None
55-
56-
async def authenticate(self, params: AuthenticateRequest) -> None:
57-
return None
58-
5958
async def prompt(self, params: PromptRequest) -> PromptResponse:
60-
# Normally you'd stream updates via sessionUpdate
59+
for block in params.prompt:
60+
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
61+
await self._conn.sessionUpdate(
62+
SessionNotification(
63+
sessionId=params.sessionId,
64+
update=SessionUpdate2(
65+
sessionUpdate="agent_message_chunk",
66+
content=ContentBlock1(type="text", text=text),
67+
),
68+
)
69+
)
6170
return PromptResponse(stopReason="end_turn")
6271

63-
async def cancel(self, params: CancelNotification) -> None:
64-
return None
65-
6672

6773
async def main() -> None:
6874
reader, writer = await stdio_streams()
69-
# For an agent process, local writes go to client stdin (writer=stdout)
70-
AgentSideConnection(lambda _conn: EchoAgent(), writer, reader)
71-
# Keep running; in a real agent you would await tasks or add your own loop
75+
AgentSideConnection(lambda conn: EchoAgent(conn), writer, reader)
7276
await asyncio.Event().wait()
7377

7478

docs/index.md

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,56 @@ pip install agent-client-protocol
1111
## Minimal usage
1212

1313
```python
14-
from acp import Agent, AgentSideConnection, Client, stdio_streams, PROTOCOL_VERSION, InitializeRequest, InitializeResponse, PromptRequest, PromptResponse
15-
from acp.schema import ContentBlock1, SessionUpdate2, SessionNotification
16-
17-
class MyAgent(Agent):
18-
def __init__(self, client: Client):
19-
self.client = client
20-
async def initialize(self, _p: InitializeRequest) -> InitializeResponse:
21-
return InitializeResponse(protocolVersion=PROTOCOL_VERSION)
22-
async def prompt(self, p: PromptRequest) -> PromptResponse:
23-
await self.client.sessionUpdate(SessionNotification(
24-
sessionId=p.sessionId,
25-
update=SessionUpdate2(sessionUpdate="agent_message_chunk", content=ContentBlock1(type="text", text="Hello from ACP")),
26-
))
14+
import asyncio
15+
16+
from acp import (
17+
Agent,
18+
AgentSideConnection,
19+
InitializeRequest,
20+
InitializeResponse,
21+
NewSessionRequest,
22+
NewSessionResponse,
23+
PromptRequest,
24+
PromptResponse,
25+
SessionNotification,
26+
stdio_streams,
27+
)
28+
from acp.schema import ContentBlock1, SessionUpdate2
29+
30+
31+
class EchoAgent(Agent):
32+
def __init__(self, conn):
33+
self._conn = conn
34+
35+
async def initialize(self, params: InitializeRequest) -> InitializeResponse:
36+
return InitializeResponse(protocolVersion=params.protocolVersion)
37+
38+
async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
39+
return NewSessionResponse(sessionId="sess-1")
40+
41+
async def prompt(self, params: PromptRequest) -> PromptResponse:
42+
for block in params.prompt:
43+
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
44+
await self._conn.sessionUpdate(
45+
SessionNotification(
46+
sessionId=params.sessionId,
47+
update=SessionUpdate2(
48+
sessionUpdate="agent_message_chunk",
49+
content=ContentBlock1(type="text", text=text),
50+
),
51+
)
52+
)
2753
return PromptResponse(stopReason="end_turn")
54+
55+
56+
async def main() -> None:
57+
reader, writer = await stdio_streams()
58+
AgentSideConnection(lambda conn: EchoAgent(conn), writer, reader)
59+
await asyncio.Event().wait()
60+
61+
62+
if __name__ == "__main__":
63+
asyncio.run(main())
2864
```
2965

3066
- Quickstart: [quickstart.md](quickstart.md)

docs/mini-swe-agent.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,49 @@
11
# Mini SWE Agent bridge
22

3-
This example wraps mini-swe-agent behind ACP so Zed can run it as an external agent over stdio.
3+
> Just a show of the bridge in action. Not a best-effort or absolutely-correct implementation of the agent.
4+
5+
This example wraps mini-swe-agent behind ACP so Zed can run it as an external agent over stdio. It also includes a local Textual UI client connected via a duet launcher
46

57
## Behavior
68

7-
- Prompts: text blocks are concatenated into a single task string; referenced resources (`resource_link` / `resource`) are surfaced as hints in the task.
8-
- Streaming: incremental text is sent via `session/update` with `agent_message_chunk`.
9+
- Prompts: text blocks are concatenated into a single task string. (Resource embedding is not used in this example.)
10+
- Streaming: only LM output is streamed via `session/update` `agent_message_chunk`.
911
- Tool calls: when the agent executes a shell command, the bridge sends:
1012
- `tool_call` with `kind=execute`, pending status, and a bash code block containing the command
1113
- `tool_call_update` upon completion, including output and a `rawOutput` object with `output` and `returncode`
1214
- Final result: on task submission (mini-swe-agent prints `COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` as the first line), a final `agent_message_chunk` with the submission content is sent.
1315

14-
Non-terminating events (e.g. user rejection or timeouts) are surfaced both as a `tool_call_update` with `status=cancelled|failed` and as a text chunk so the session can continue.
15-
1616
## Configuration
1717

18-
Environment variables set in the Zed server config control the model:
18+
Environment variables control the model:
1919

2020
- `MINI_SWE_MODEL`: model ID (e.g. `openrouter/openai/gpt-4o-mini`)
21-
- `MINI_SWE_MODEL_KWARGS`: JSON string of extra parameters (e.g. `{ "api_base": "https://openrouter.ai/api/v1" }`)
22-
- Vendor API keys (e.g. `OPENROUTER_API_KEY`) must be present in the environment
21+
- `OPENROUTER_API_KEY` for OpenRouter; or `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` for native providers
22+
- Optional `MINI_SWE_MODEL_KWARGS`: JSON, e.g. `{ "api_base": "https://openrouter.ai/api/v1" }` (auto-injected for OpenRouter if missing)
23+
24+
Agent behavior automatically maps the appropriate API key based on the chosen model and available environment variables.
2325

2426
If `mini-swe-agent` is not installed in the venv, the bridge attempts to import a vendored reference copy under `reference/mini-swe-agent/src`.
2527

28+
## How to run
29+
30+
- In Zed (editor integration): configure an agent server to launch `examples/mini_swe_agent/agent.py` and set the environment variables there. Use Zed’s “Open ACP Logs” to inspect `tool_call`/`tool_call_update` and message chunks.
31+
- In terminal (local TUI): run the duet launcher to start both the agent and the Textual client with the same environment and dedicated pipes:
32+
33+
```bash
34+
python examples/mini_swe_agent/duet.py
35+
```
36+
37+
The launcher loads `.env` from the repo root (using python-dotenv) so both processes share the same configuration.
38+
39+
### TUI usage
40+
41+
- Hotkeys: `y` → YOLO, `c` → Confirm, `u` → Human, `Enter` → Continue.
42+
- In Human mode, you’ll be prompted for a bash command; it will be executed and streamed back as a tool call.
43+
- Each executed command appears in the “TOOL CALLS” section with live status and output.
44+
2645
## Files
2746

28-
- Example entry: [`examples/mini_swe_agent/agent.py`](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/mini_swe_agent/agent.py)
47+
- Agent entry: [`examples/mini_swe_agent/agent.py`](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/mini_swe_agent/agent.py)
48+
- Duet launcher: [`examples/mini_swe_agent/duet.py`](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/mini_swe_agent/duet.py)
49+
- Textual client: [`examples/mini_swe_agent/client.py`](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/mini_swe_agent/client.py)

docs/quickstart.md

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,46 @@ import asyncio
1616
from acp import (
1717
Agent,
1818
AgentSideConnection,
19-
AuthenticateRequest,
20-
CancelNotification,
2119
InitializeRequest,
2220
InitializeResponse,
23-
LoadSessionRequest,
2421
NewSessionRequest,
2522
NewSessionResponse,
2623
PromptRequest,
2724
PromptResponse,
25+
SessionNotification,
2826
stdio_streams,
2927
)
28+
from acp.schema import ContentBlock1, SessionUpdate2
3029

3130

3231
class EchoAgent(Agent):
32+
def __init__(self, conn):
33+
self._conn = conn
34+
3335
async def initialize(self, params: InitializeRequest) -> InitializeResponse:
3436
return InitializeResponse(protocolVersion=params.protocolVersion)
3537

3638
async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
3739
return NewSessionResponse(sessionId="sess-1")
3840

39-
async def loadSession(self, params: LoadSessionRequest) -> None:
40-
return None
41-
42-
async def authenticate(self, params: AuthenticateRequest) -> None:
43-
return None
44-
4541
async def prompt(self, params: PromptRequest) -> PromptResponse:
46-
# Normally you'd stream updates via sessionUpdate
42+
for block in params.prompt:
43+
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
44+
await self._conn.sessionUpdate(
45+
SessionNotification(
46+
sessionId=params.sessionId,
47+
update=SessionUpdate2(
48+
sessionUpdate="agent_message_chunk",
49+
content=ContentBlock1(type="text", text=text),
50+
),
51+
)
52+
)
4753
return PromptResponse(stopReason="end_turn")
4854

49-
async def cancel(self, params: CancelNotification) -> None:
50-
return None
51-
5255

5356
async def main() -> None:
5457
reader, writer = await stdio_streams()
55-
# For an agent process, local writes go to client stdin (writer=stdout)
56-
AgentSideConnection(lambda _conn: EchoAgent(), writer, reader)
57-
# Keep running; in a real agent you would await tasks or add your own loop
58+
AgentSideConnection(lambda conn: EchoAgent(conn), writer, reader)
5859
await asyncio.Event().wait()
5960

6061

@@ -84,14 +85,26 @@ Add an agent server to Zed’s `settings.json`:
8485
],
8586
"env": {
8687
"MINI_SWE_MODEL": "openrouter/openai/gpt-4o-mini",
87-
"MINI_SWE_MODEL_KWARGS": "{\"api_base\":\"https://openrouter.ai/api/v1\"}",
8888
"OPENROUTER_API_KEY": "sk-or-..."
8989
}
9090
}
9191
}
9292
}
9393
```
9494

95+
- For OpenRouter, `api_base` is set automatically to `https://openrouter.ai/api/v1` if not provided.
96+
- Alternatively, use native providers by setting `MINI_SWE_MODEL` accordingly and providing `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`.
97+
9598
In Zed, open the Agents panel and select "Mini SWE Agent (Python)".
9699

97100
See [mini-swe-agent.md](mini-swe-agent.md) for behavior and message mapping details.
101+
102+
## Run locally with a TUI
103+
104+
Use the duet launcher to run both the agent and the local Textual client over dedicated pipes:
105+
106+
```bash
107+
python examples/mini_swe_agent/duet.py
108+
```
109+
110+
The launcher loads `.env` from the repo root so both processes share the same configuration (requires python-dotenv).

0 commit comments

Comments
 (0)