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
16 changes: 9 additions & 7 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ import asyncio
import sys
from pathlib import Path
from typing import Any
from uuid import uuid4

from acp import spawn_agent_process, text_block
from acp import PROTOCOL_VERSION, spawn_agent_process, text_block
from acp.interfaces import Client


Expand All @@ -110,11 +111,12 @@ class SimpleClient(Client):
async def main() -> None:
script = Path("examples/echo_agent.py")
async with spawn_agent_process(SimpleClient(), sys.executable, str(script)) as (conn, _proc):
await conn.initialize(protocol_version=1)
await conn.initialize(protocol_version=PROTOCOL_VERSION)
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!")],
message_id=str(uuid4()),
)

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


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

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

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

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

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

```bash
python examples/gemini.py --yolo # auto-approve permission prompts
python examples/gemini.py --sandbox --model gemini-1.5-pro
python examples/gemini.py --skip-trust --yolo # auto-approve permission prompts
python examples/gemini.py --skip-trust --sandbox --model gemini-1.5-pro
```

Environment helpers:
Expand Down
16 changes: 13 additions & 3 deletions examples/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ async def authenticate(self, method_id: str, **kwargs: Any) -> AuthenticateRespo
return AuthenticateResponse()

async def new_session(
self, cwd: str, mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio], **kwargs: Any
self,
cwd: str,
additional_directories: list[str] | None = None,
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
**kwargs: Any,
) -> NewSessionResponse:
logging.info("Received new session request")
session_id = str(self._next_session_id)
Expand All @@ -74,7 +78,12 @@ async def new_session(
return NewSessionResponse(session_id=session_id, modes=None)

async def load_session(
self, cwd: str, mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio], session_id: str, **kwargs: Any
self,
cwd: str,
session_id: str,
additional_directories: list[str] | None = None,
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
**kwargs: Any,
) -> LoadSessionResponse | None:
logging.info("Received load session request %s", session_id)
self._sessions.add(session_id)
Expand All @@ -94,6 +103,7 @@ async def prompt(
| EmbeddedResourceContentBlock
],
session_id: str,
message_id: str | None = None,
**kwargs: Any,
) -> PromptResponse:
logging.info("Received prompt request for session %s", session_id)
Expand All @@ -103,7 +113,7 @@ async def prompt(
await self._send_agent_message(session_id, text_block("Client sent:"))
for block in prompt:
await self._send_agent_message(session_id, block)
return PromptResponse(stop_reason="end_turn")
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)

async def cancel(self, session_id: str, **kwargs: Any) -> None:
logging.info("Received cancel notification for session %s", session_id)
Expand Down
14 changes: 11 additions & 3 deletions examples/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import sys
from pathlib import Path
from typing import Any
from uuid import uuid4

from acp import (
PROTOCOL_VERSION,
Expand All @@ -22,6 +23,7 @@
AudioContentBlock,
AvailableCommandsUpdate,
ClientCapabilities,
ConfigOptionUpdate,
CreateTerminalResponse,
CurrentModeUpdate,
EmbeddedResourceContentBlock,
Expand All @@ -34,11 +36,13 @@
ReleaseTerminalResponse,
RequestPermissionResponse,
ResourceContentBlock,
SessionInfoUpdate,
TerminalOutputResponse,
TextContentBlock,
ToolCall,
ToolCallProgress,
ToolCallStart,
ToolCallUpdate,
UsageUpdate,
UserMessageChunk,
WaitForTerminalExitResponse,
WriteTextFileResponse,
Expand All @@ -47,7 +51,7 @@

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

Expand Down Expand Up @@ -99,7 +103,10 @@ async def session_update(
| ToolCallProgress
| AgentPlanUpdate
| AvailableCommandsUpdate
| CurrentModeUpdate,
| CurrentModeUpdate
| ConfigOptionUpdate
| SessionInfoUpdate
| UsageUpdate,
**kwargs: Any,
) -> None:
if not isinstance(update, AgentMessageChunk):
Expand Down Expand Up @@ -151,6 +158,7 @@ async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None:
await conn.prompt(
session_id=session_id,
prompt=[text_block(line)],
message_id=str(uuid4()),
)
except Exception as exc:
logging.error("Prompt failed: %s", exc) # noqa: TRY400
Expand Down
9 changes: 7 additions & 2 deletions examples/echo_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ async def initialize(
return InitializeResponse(protocol_version=protocol_version)

async def new_session(
self, cwd: str, mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio], **kwargs: Any
self,
cwd: str,
additional_directories: list[str] | None = None,
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
**kwargs: Any,
) -> NewSessionResponse:
return NewSessionResponse(session_id=uuid4().hex)

Expand All @@ -62,6 +66,7 @@ async def prompt(
| EmbeddedResourceContentBlock
],
session_id: str,
message_id: str | None = None,
**kwargs: Any,
) -> PromptResponse:
for block in prompt:
Expand All @@ -71,7 +76,7 @@ async def prompt(
chunk.content.field_meta = {"echo": True}

await self._conn.session_update(session_id=session_id, update=chunk, source="echo_agent")
return PromptResponse(stop_reason="end_turn")
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)


async def main() -> None:
Expand Down
63 changes: 51 additions & 12 deletions examples/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
AllowedOutcome,
AvailableCommandsUpdate,
ClientCapabilities,
ConfigOptionUpdate,
CreateTerminalResponse,
CurrentModeUpdate,
DeniedOutcome,
Expand All @@ -40,12 +41,14 @@
ReleaseTerminalResponse,
RequestPermissionResponse,
ResourceContentBlock,
SessionInfoUpdate,
TerminalOutputResponse,
TerminalToolCallContent,
TextContentBlock,
ToolCall,
ToolCallProgress,
ToolCallStart,
ToolCallUpdate,
UsageUpdate,
UserMessageChunk,
WaitForTerminalExitResponse,
WriteTextFileResponse,
Expand All @@ -59,7 +62,7 @@ def __init__(self, auto_approve: bool) -> None:
self._auto_approve = auto_approve

async def request_permission(
self, options: list[PermissionOption], session_id: str, tool_call: ToolCall, **kwargs: Any
self, options: list[PermissionOption], session_id: str, tool_call: ToolCallUpdate, **kwargs: Any
) -> RequestPermissionResponse:
if self._auto_approve:
option = _pick_preferred_option(options)
Expand Down Expand Up @@ -122,7 +125,10 @@ async def session_update( # noqa: C901
| ToolCallProgress
| AgentPlanUpdate
| AvailableCommandsUpdate
| CurrentModeUpdate,
| CurrentModeUpdate
| ConfigOptionUpdate
| SessionInfoUpdate
| UsageUpdate,
**kwargs: Any,
) -> None:
if isinstance(update, AgentMessageChunk):
Expand Down Expand Up @@ -227,7 +233,18 @@ def _print_text_content(content: object) -> None:
print(text)


async def interactive_loop(conn: ClientSideConnection, session_id: str) -> 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)],
)
if timeout is None:
await request
return
await asyncio.wait_for(request, timeout=timeout)


async def interactive_loop(conn: ClientSideConnection, session_id: str, prompt_timeout: float | None) -> None:
print("Type a message and press Enter to send.")
print("Commands: :cancel, :exit")

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

try:
await conn.prompt(
session_id=session_id,
prompt=[text_block(line)],
)
await _send_prompt(conn, session_id, line, prompt_timeout)
except asyncio.TimeoutError:
print("prompt timed out waiting for final ACP response", file=sys.stderr)
with contextlib.suppress(Exception):
await asyncio.wait_for(conn.cancel(session_id=session_id), timeout=2)
except RequestError as err:
_print_request_error("prompt", err)
except Exception as exc:
Expand All @@ -274,24 +292,36 @@ async def run(argv: list[str]) -> int: # noqa: C901
parser = argparse.ArgumentParser(description="Interact with the Gemini CLI over ACP.")
parser.add_argument("--gemini", help="Path to the Gemini CLI binary")
parser.add_argument("--model", help="Model identifier to pass to Gemini")
parser.add_argument("--prompt", help="Send one prompt and exit")
parser.add_argument(
"--prompt-timeout",
type=float,
default=120.0,
help="Seconds to wait for session/prompt to finish; use 0 to disable",
)
parser.add_argument("--sandbox", action="store_true", help="Enable Gemini sandbox mode")
parser.add_argument("--debug", action="store_true", help="Pass --debug to Gemini")
parser.add_argument("--experimental-acp", action="store_true", help="Use Gemini's deprecated ACP flag")
parser.add_argument("--skip-trust", action="store_true", help="Trust the current workspace for this session")
parser.add_argument("--yolo", action="store_true", help="Auto-approve permission prompts")
args = parser.parse_args(argv[1:])
prompt_timeout = None if args.prompt_timeout == 0 else args.prompt_timeout

try:
gemini_path = _resolve_gemini_cli(args.gemini)
except FileNotFoundError as exc:
print(exc, file=sys.stderr)
return 1

cmd = [gemini_path, "--experimental-acp"]
cmd = [gemini_path, "--experimental-acp" if args.experimental_acp else "--acp"]
if args.model:
cmd += ["--model", args.model]
if args.sandbox:
cmd.append("--sandbox")
if args.debug:
cmd.append("--debug")
if args.skip_trust:
cmd.append("--skip-trust")

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

try:
await interactive_loop(conn, session.session_id)
if args.prompt is None:
await interactive_loop(conn, session.session_id, prompt_timeout)
else:
await _send_prompt(conn, session.session_id, args.prompt, prompt_timeout)
except asyncio.TimeoutError:
print("prompt timed out waiting for final ACP response", file=sys.stderr)
with contextlib.suppress(Exception):
await asyncio.wait_for(conn.cancel(session_id=session.session_id), timeout=2)
return 1
finally:
await _shutdown(proc, conn)

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

async def _shutdown(proc: asyncio.subprocess.Process, conn: ClientSideConnection) -> None:
with contextlib.suppress(Exception):
await conn.close()
await asyncio.wait_for(conn.close(), timeout=2)
if proc.returncode is None:
proc.terminate()
try:
await asyncio.wait_for(proc.wait(), timeout=5)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
with contextlib.suppress(Exception):
await asyncio.wait_for(proc.wait(), timeout=5)


def main(argv: list[str] | None = None) -> int:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "agent-client-protocol"
version = "0.9.0"
version = "0.10.0"
description = "A Python implement of Agent Client Protocol (ACP, by Zed Industries)"
authors = [
{ name = "Chojan Shang", email = "psiace@apache.org" },
Expand Down
2 changes: 1 addition & 1 deletion schema/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
refs/tags/v0.11.2
refs/tags/v0.12.2
16 changes: 16 additions & 0 deletions schema/meta.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
{
"agentMethods": {
"authenticate": "authenticate",
"document_did_change": "document/didChange",
"document_did_close": "document/didClose",
"document_did_focus": "document/didFocus",
"document_did_open": "document/didOpen",
"document_did_save": "document/didSave",
"initialize": "initialize",
"logout": "logout",
"nes_accept": "nes/accept",
"nes_close": "nes/close",
"nes_reject": "nes/reject",
"nes_start": "nes/start",
"nes_suggest": "nes/suggest",
"providers_disable": "providers/disable",
"providers_list": "providers/list",
"providers_set": "providers/set",
"session_cancel": "session/cancel",
"session_close": "session/close",
"session_fork": "session/fork",
Expand All @@ -15,6 +29,8 @@
"session_set_model": "session/set_model"
},
"clientMethods": {
"elicitation_complete": "elicitation/complete",
"elicitation_create": "elicitation/create",
"fs_read_text_file": "fs/read_text_file",
"fs_write_text_file": "fs/write_text_file",
"session_request_permission": "session/request_permission",
Expand Down
Loading
Loading