Skip to content

Commit 03739f4

Browse files
authored
feat: bump acp protocol to 0.12.2 (#93)
* feat: bump acp protocol to 0.12.2 Signed-off-by: Chojan Shang <psiace@apache.org> * *: refine Signed-off-by: Chojan Shang <psiace@apache.org> --------- Signed-off-by: Chojan Shang <psiace@apache.org>
1 parent df72173 commit 03739f4

16 files changed

Lines changed: 7734 additions & 2216 deletions

File tree

docs/quickstart.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ import asyncio
9292
import sys
9393
from pathlib import Path
9494
from typing import Any
95+
from uuid import uuid4
9596

96-
from acp import spawn_agent_process, text_block
97+
from acp import PROTOCOL_VERSION, spawn_agent_process, text_block
9798
from acp.interfaces import Client
9899

99100

@@ -110,11 +111,12 @@ class SimpleClient(Client):
110111
async def main() -> None:
111112
script = Path("examples/echo_agent.py")
112113
async with spawn_agent_process(SimpleClient(), sys.executable, str(script)) as (conn, _proc):
113-
await conn.initialize(protocol_version=1)
114+
await conn.initialize(protocol_version=PROTOCOL_VERSION)
114115
session = await conn.new_session(cwd=str(script.parent), mcp_servers=[])
115116
await conn.prompt(
116117
session_id=session.session_id,
117118
prompt=[text_block("Hello from spawn!")],
119+
message_id=str(uuid4()),
118120
)
119121

120122
asyncio.run(main())
@@ -133,17 +135,17 @@ from acp import Agent, PromptResponse
133135

134136

135137
class MyAgent(Agent):
136-
async def prompt(self, prompt, session_id, **kwargs) -> PromptResponse:
138+
async def prompt(self, prompt, session_id, message_id=None, **kwargs) -> PromptResponse:
137139
# inspect prompt, stream updates, then finish the turn
138-
return PromptResponse(stop_reason="end_turn")
140+
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
139141
```
140142

141143
Run it with `run_agent()` inside an async entrypoint and wire it to your client. Refer to:
142144

143145
- [`examples/echo_agent.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/echo_agent.py) for the smallest streaming agent
144146
- [`examples/agent.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/agent.py) for an implementation that negotiates capabilities and streams richer updates
145147
- [`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
146-
- [`examples/gemini.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/gemini.py) to drive the Gemini CLI (`--experimental-acp`) directly from Python
148+
- [`examples/gemini.py`](https://github.com/agentclientprotocol/python-sdk/blob/main/examples/gemini.py) to drive the Gemini CLI (`--acp`) directly from Python
147149

148150
Need builders for common payloads? `acp.helpers` mirrors the Go/TS helper APIs:
149151

@@ -167,8 +169,8 @@ _Have the Gemini CLI installed? Run the bridge to exercise permission flows._
167169
If you have the Gemini CLI installed and authenticated:
168170

169171
```bash
170-
python examples/gemini.py --yolo # auto-approve permission prompts
171-
python examples/gemini.py --sandbox --model gemini-1.5-pro
172+
python examples/gemini.py --skip-trust --yolo # auto-approve permission prompts
173+
python examples/gemini.py --skip-trust --sandbox --model gemini-1.5-pro
172174
```
173175

174176
Environment helpers:

examples/agent.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ async def authenticate(self, method_id: str, **kwargs: Any) -> AuthenticateRespo
6565
return AuthenticateResponse()
6666

6767
async def new_session(
68-
self, cwd: str, mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio], **kwargs: Any
68+
self,
69+
cwd: str,
70+
additional_directories: list[str] | None = None,
71+
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
72+
**kwargs: Any,
6973
) -> NewSessionResponse:
7074
logging.info("Received new session request")
7175
session_id = str(self._next_session_id)
@@ -74,7 +78,12 @@ async def new_session(
7478
return NewSessionResponse(session_id=session_id, modes=None)
7579

7680
async def load_session(
77-
self, cwd: str, mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio], session_id: str, **kwargs: Any
81+
self,
82+
cwd: str,
83+
session_id: str,
84+
additional_directories: list[str] | None = None,
85+
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
86+
**kwargs: Any,
7887
) -> LoadSessionResponse | None:
7988
logging.info("Received load session request %s", session_id)
8089
self._sessions.add(session_id)
@@ -94,6 +103,7 @@ async def prompt(
94103
| EmbeddedResourceContentBlock
95104
],
96105
session_id: str,
106+
message_id: str | None = None,
97107
**kwargs: Any,
98108
) -> PromptResponse:
99109
logging.info("Received prompt request for session %s", session_id)
@@ -103,7 +113,7 @@ async def prompt(
103113
await self._send_agent_message(session_id, text_block("Client sent:"))
104114
for block in prompt:
105115
await self._send_agent_message(session_id, block)
106-
return PromptResponse(stop_reason="end_turn")
116+
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
107117

108118
async def cancel(self, session_id: str, **kwargs: Any) -> None:
109119
logging.info("Received cancel notification for session %s", session_id)

examples/client.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
from pathlib import Path
88
from typing import Any
9+
from uuid import uuid4
910

1011
from acp import (
1112
PROTOCOL_VERSION,
@@ -22,6 +23,7 @@
2223
AudioContentBlock,
2324
AvailableCommandsUpdate,
2425
ClientCapabilities,
26+
ConfigOptionUpdate,
2527
CreateTerminalResponse,
2628
CurrentModeUpdate,
2729
EmbeddedResourceContentBlock,
@@ -34,11 +36,13 @@
3436
ReleaseTerminalResponse,
3537
RequestPermissionResponse,
3638
ResourceContentBlock,
39+
SessionInfoUpdate,
3740
TerminalOutputResponse,
3841
TextContentBlock,
39-
ToolCall,
4042
ToolCallProgress,
4143
ToolCallStart,
44+
ToolCallUpdate,
45+
UsageUpdate,
4246
UserMessageChunk,
4347
WaitForTerminalExitResponse,
4448
WriteTextFileResponse,
@@ -47,7 +51,7 @@
4751

4852
class ExampleClient(Client):
4953
async def request_permission(
50-
self, options: list[PermissionOption], session_id: str, tool_call: ToolCall, **kwargs: Any
54+
self, options: list[PermissionOption], session_id: str, tool_call: ToolCallUpdate, **kwargs: Any
5155
) -> RequestPermissionResponse:
5256
raise RequestError.method_not_found("session/request_permission")
5357

@@ -99,7 +103,10 @@ async def session_update(
99103
| ToolCallProgress
100104
| AgentPlanUpdate
101105
| AvailableCommandsUpdate
102-
| CurrentModeUpdate,
106+
| CurrentModeUpdate
107+
| ConfigOptionUpdate
108+
| SessionInfoUpdate
109+
| UsageUpdate,
103110
**kwargs: Any,
104111
) -> None:
105112
if not isinstance(update, AgentMessageChunk):
@@ -151,6 +158,7 @@ async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None:
151158
await conn.prompt(
152159
session_id=session_id,
153160
prompt=[text_block(line)],
161+
message_id=str(uuid4()),
154162
)
155163
except Exception as exc:
156164
logging.error("Prompt failed: %s", exc) # noqa: TRY400

examples/echo_agent.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ async def initialize(
4848
return InitializeResponse(protocol_version=protocol_version)
4949

5050
async def new_session(
51-
self, cwd: str, mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio], **kwargs: Any
51+
self,
52+
cwd: str,
53+
additional_directories: list[str] | None = None,
54+
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
55+
**kwargs: Any,
5256
) -> NewSessionResponse:
5357
return NewSessionResponse(session_id=uuid4().hex)
5458

@@ -62,6 +66,7 @@ async def prompt(
6266
| EmbeddedResourceContentBlock
6367
],
6468
session_id: str,
69+
message_id: str | None = None,
6570
**kwargs: Any,
6671
) -> PromptResponse:
6772
for block in prompt:
@@ -71,7 +76,7 @@ async def prompt(
7176
chunk.content.field_meta = {"echo": True}
7277

7378
await self._conn.session_update(session_id=session_id, update=chunk, source="echo_agent")
74-
return PromptResponse(stop_reason="end_turn")
79+
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
7580

7681

7782
async def main() -> None:

examples/gemini.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
AllowedOutcome,
2828
AvailableCommandsUpdate,
2929
ClientCapabilities,
30+
ConfigOptionUpdate,
3031
CreateTerminalResponse,
3132
CurrentModeUpdate,
3233
DeniedOutcome,
@@ -40,12 +41,14 @@
4041
ReleaseTerminalResponse,
4142
RequestPermissionResponse,
4243
ResourceContentBlock,
44+
SessionInfoUpdate,
4345
TerminalOutputResponse,
4446
TerminalToolCallContent,
4547
TextContentBlock,
46-
ToolCall,
4748
ToolCallProgress,
4849
ToolCallStart,
50+
ToolCallUpdate,
51+
UsageUpdate,
4952
UserMessageChunk,
5053
WaitForTerminalExitResponse,
5154
WriteTextFileResponse,
@@ -59,7 +62,7 @@ def __init__(self, auto_approve: bool) -> None:
5962
self._auto_approve = auto_approve
6063

6164
async def request_permission(
62-
self, options: list[PermissionOption], session_id: str, tool_call: ToolCall, **kwargs: Any
65+
self, options: list[PermissionOption], session_id: str, tool_call: ToolCallUpdate, **kwargs: Any
6366
) -> RequestPermissionResponse:
6467
if self._auto_approve:
6568
option = _pick_preferred_option(options)
@@ -122,7 +125,10 @@ async def session_update( # noqa: C901
122125
| ToolCallProgress
123126
| AgentPlanUpdate
124127
| AvailableCommandsUpdate
125-
| CurrentModeUpdate,
128+
| CurrentModeUpdate
129+
| ConfigOptionUpdate
130+
| SessionInfoUpdate
131+
| UsageUpdate,
126132
**kwargs: Any,
127133
) -> None:
128134
if isinstance(update, AgentMessageChunk):
@@ -227,7 +233,18 @@ def _print_text_content(content: object) -> None:
227233
print(text)
228234

229235

230-
async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None:
236+
async def _send_prompt(conn: ClientSideConnection, session_id: str, prompt: str, timeout: float | None) -> None:
237+
request = conn.prompt(
238+
session_id=session_id,
239+
prompt=[text_block(prompt)],
240+
)
241+
if timeout is None:
242+
await request
243+
return
244+
await asyncio.wait_for(request, timeout=timeout)
245+
246+
247+
async def interactive_loop(conn: ClientSideConnection, session_id: str, prompt_timeout: float | None) -> None:
231248
print("Type a message and press Enter to send.")
232249
print("Commands: :cancel, :exit")
233250

@@ -248,10 +265,11 @@ async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None:
248265
continue
249266

250267
try:
251-
await conn.prompt(
252-
session_id=session_id,
253-
prompt=[text_block(line)],
254-
)
268+
await _send_prompt(conn, session_id, line, prompt_timeout)
269+
except asyncio.TimeoutError:
270+
print("prompt timed out waiting for final ACP response", file=sys.stderr)
271+
with contextlib.suppress(Exception):
272+
await asyncio.wait_for(conn.cancel(session_id=session_id), timeout=2)
255273
except RequestError as err:
256274
_print_request_error("prompt", err)
257275
except Exception as exc:
@@ -274,24 +292,36 @@ async def run(argv: list[str]) -> int: # noqa: C901
274292
parser = argparse.ArgumentParser(description="Interact with the Gemini CLI over ACP.")
275293
parser.add_argument("--gemini", help="Path to the Gemini CLI binary")
276294
parser.add_argument("--model", help="Model identifier to pass to Gemini")
295+
parser.add_argument("--prompt", help="Send one prompt and exit")
296+
parser.add_argument(
297+
"--prompt-timeout",
298+
type=float,
299+
default=120.0,
300+
help="Seconds to wait for session/prompt to finish; use 0 to disable",
301+
)
277302
parser.add_argument("--sandbox", action="store_true", help="Enable Gemini sandbox mode")
278303
parser.add_argument("--debug", action="store_true", help="Pass --debug to Gemini")
304+
parser.add_argument("--experimental-acp", action="store_true", help="Use Gemini's deprecated ACP flag")
305+
parser.add_argument("--skip-trust", action="store_true", help="Trust the current workspace for this session")
279306
parser.add_argument("--yolo", action="store_true", help="Auto-approve permission prompts")
280307
args = parser.parse_args(argv[1:])
308+
prompt_timeout = None if args.prompt_timeout == 0 else args.prompt_timeout
281309

282310
try:
283311
gemini_path = _resolve_gemini_cli(args.gemini)
284312
except FileNotFoundError as exc:
285313
print(exc, file=sys.stderr)
286314
return 1
287315

288-
cmd = [gemini_path, "--experimental-acp"]
316+
cmd = [gemini_path, "--experimental-acp" if args.experimental_acp else "--acp"]
289317
if args.model:
290318
cmd += ["--model", args.model]
291319
if args.sandbox:
292320
cmd.append("--sandbox")
293321
if args.debug:
294322
cmd.append("--debug")
323+
if args.skip_trust:
324+
cmd.append("--skip-trust")
295325

296326
try:
297327
proc = await asyncio.create_subprocess_exec(
@@ -350,7 +380,15 @@ async def run(argv: list[str]) -> int: # noqa: C901
350380
print(f"📝 Created session: {session.session_id}")
351381

352382
try:
353-
await interactive_loop(conn, session.session_id)
383+
if args.prompt is None:
384+
await interactive_loop(conn, session.session_id, prompt_timeout)
385+
else:
386+
await _send_prompt(conn, session.session_id, args.prompt, prompt_timeout)
387+
except asyncio.TimeoutError:
388+
print("prompt timed out waiting for final ACP response", file=sys.stderr)
389+
with contextlib.suppress(Exception):
390+
await asyncio.wait_for(conn.cancel(session_id=session.session_id), timeout=2)
391+
return 1
354392
finally:
355393
await _shutdown(proc, conn)
356394

@@ -373,14 +411,15 @@ def _print_request_error(stage: str, err: RequestError) -> None:
373411

374412
async def _shutdown(proc: asyncio.subprocess.Process, conn: ClientSideConnection) -> None:
375413
with contextlib.suppress(Exception):
376-
await conn.close()
414+
await asyncio.wait_for(conn.close(), timeout=2)
377415
if proc.returncode is None:
378416
proc.terminate()
379417
try:
380418
await asyncio.wait_for(proc.wait(), timeout=5)
381419
except asyncio.TimeoutError:
382420
proc.kill()
383-
await proc.wait()
421+
with contextlib.suppress(Exception):
422+
await asyncio.wait_for(proc.wait(), timeout=5)
384423

385424

386425
def main(argv: list[str] | None = None) -> int:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "agent-client-protocol"
3-
version = "0.9.0"
3+
version = "0.10.0"
44
description = "A Python implement of Agent Client Protocol (ACP, by Zed Industries)"
55
authors = [
66
{ name = "Chojan Shang", email = "psiace@apache.org" },

schema/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
refs/tags/v0.11.2
1+
refs/tags/v0.12.2

schema/meta.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
{
22
"agentMethods": {
33
"authenticate": "authenticate",
4+
"document_did_change": "document/didChange",
5+
"document_did_close": "document/didClose",
6+
"document_did_focus": "document/didFocus",
7+
"document_did_open": "document/didOpen",
8+
"document_did_save": "document/didSave",
49
"initialize": "initialize",
10+
"logout": "logout",
11+
"nes_accept": "nes/accept",
12+
"nes_close": "nes/close",
13+
"nes_reject": "nes/reject",
14+
"nes_start": "nes/start",
15+
"nes_suggest": "nes/suggest",
16+
"providers_disable": "providers/disable",
17+
"providers_list": "providers/list",
18+
"providers_set": "providers/set",
519
"session_cancel": "session/cancel",
620
"session_close": "session/close",
721
"session_fork": "session/fork",
@@ -15,6 +29,8 @@
1529
"session_set_model": "session/set_model"
1630
},
1731
"clientMethods": {
32+
"elicitation_complete": "elicitation/complete",
33+
"elicitation_create": "elicitation/create",
1834
"fs_read_text_file": "fs/read_text_file",
1935
"fs_write_text_file": "fs/write_text_file",
2036
"session_request_permission": "session/request_permission",

0 commit comments

Comments
 (0)