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
24 changes: 12 additions & 12 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ Spin up a working ACP agent/client loop in minutes. Keep this page beside the te

## Quick checklist

| Goal | Command / Link |
| --- | --- |
| Install the SDK | `pip install agent-client-protocol` or `uv add agent-client-protocol` |
| Run the echo agent | `python examples/echo_agent.py` |
| Point Zed (or another client) at it | Update `settings.json` as shown below |
| Programmatically drive an agent | Copy the `spawn_agent_process` example |
| Run tests before hacking further | `make check && make test` |
| Goal | Command / Link |
| ----------------------------------- | --------------------------------------------------------------------- |
| Install the SDK | `pip install agent-client-protocol` or `uv add agent-client-protocol` |
| Run the echo agent | `python examples/echo_agent.py` |
| Point Zed (or another client) at it | Update `settings.json` as shown below |
| Programmatically drive an agent | Copy the `spawn_agent_process` example |
| Run tests before hacking further | `make check && make test` |

## Before you begin

Expand Down Expand Up @@ -84,17 +84,17 @@ class SimpleClient(Client):
return {"outcome": {"outcome": "cancelled"}}

async def sessionUpdate(self, params: SessionNotification) -> None:
print("update:", params.sessionId, params.update)
print("update:", params.session_id, params.update)


async def main() -> None:
script = Path("examples/echo_agent.py")
async with spawn_agent_process(lambda _agent: SimpleClient(), sys.executable, str(script)) as (conn, _proc):
await conn.initialize(InitializeRequest(protocolVersion=1))
session = await conn.newSession(NewSessionRequest(cwd=str(script.parent), mcpServers=[]))
await conn.initialize(InitializeRequest(protocol_version=1))
session = await conn.newSession(NewSessionRequest(cwd=str(script.parent), mcp_servers=[]))
await conn.prompt(
PromptRequest(
sessionId=session.sessionId,
session_id=session.session_id,
prompt=[text_block("Hello from spawn!")],
)
)
Expand All @@ -117,7 +117,7 @@ from acp import Agent, PromptRequest, PromptResponse
class MyAgent(Agent):
async def prompt(self, params: PromptRequest) -> PromptResponse:
# inspect params.prompt, stream updates, then finish the turn
return PromptResponse(stopReason="end_turn")
return PromptResponse(stop_reason="end_turn")
```

Hook it up with `AgentSideConnection` inside an async entrypoint and wire it to your client. Refer to:
Expand Down
31 changes: 15 additions & 16 deletions examples/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,44 +40,43 @@ async def _send_agent_message(self, session_id: str, content: Any) -> None:
async def initialize(self, params: InitializeRequest) -> InitializeResponse: # noqa: ARG002
logging.info("Received initialize request")
return InitializeResponse(
protocolVersion=PROTOCOL_VERSION,
agentCapabilities=AgentCapabilities(),
agentInfo=Implementation(name="example-agent", title="Example Agent", version="0.1.0"),
protocol_version=PROTOCOL_VERSION,
agent_capabilities=AgentCapabilities(),
agent_info=Implementation(name="example-agent", title="Example Agent", version="0.1.0"),
)

async def authenticate(self, params: AuthenticateRequest) -> AuthenticateResponse | None: # noqa: ARG002
logging.info("Received authenticate request %s", params.methodId)
logging.info("Received authenticate request %s", params.method_id)
return AuthenticateResponse()

async def newSession(self, params: NewSessionRequest) -> NewSessionResponse: # noqa: ARG002
logging.info("Received new session request")
session_id = str(self._next_session_id)
self._next_session_id += 1
self._sessions.add(session_id)
return NewSessionResponse(sessionId=session_id, modes=None)
return NewSessionResponse(session_id=session_id, modes=None)

async def loadSession(self, params: LoadSessionRequest) -> LoadSessionResponse | None: # noqa: ARG002
logging.info("Received load session request %s", params.sessionId)
self._sessions.add(params.sessionId)
logging.info("Received load session request %s", params.session_id)
self._sessions.add(params.session_id)
return LoadSessionResponse()

async def setSessionMode(self, params: SetSessionModeRequest) -> SetSessionModeResponse | None: # noqa: ARG002
logging.info("Received set session mode request %s -> %s", params.sessionId, params.modeId)
logging.info("Received set session mode request %s -> %s", params.session_id, params.mode_id)
return SetSessionModeResponse()

async def prompt(self, params: PromptRequest) -> PromptResponse:
logging.info("Received prompt request for session %s", params.sessionId)
if params.sessionId not in self._sessions:
self._sessions.add(params.sessionId)
logging.info("Received prompt request for session %s", params.session_id)
if params.session_id not in self._sessions:
self._sessions.add(params.session_id)

await self._send_agent_message(params.sessionId, text_block("Client sent:"))
await self._send_agent_message(params.session_id, text_block("Client sent:"))
for block in params.prompt:
await self._send_agent_message(params.sessionId, block)

return PromptResponse(stopReason="end_turn")
await self._send_agent_message(params.session_id, block)
return PromptResponse(stop_reason="end_turn")

async def cancel(self, params: CancelNotification) -> None: # noqa: ARG002
logging.info("Received cancel notification for session %s", params.sessionId)
logging.info("Received cancel notification for session %s", params.session_id)

async def extMethod(self, method: str, params: dict) -> dict: # noqa: ARG002
logging.info("Received extension method call: %s", method)
Expand Down
12 changes: 6 additions & 6 deletions examples/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None:
try:
await conn.prompt(
PromptRequest(
sessionId=session_id,
session_id=session_id,
prompt=[text_block(line)],
)
)
Expand Down Expand Up @@ -146,14 +146,14 @@ async def main(argv: list[str]) -> int:

await conn.initialize(
InitializeRequest(
protocolVersion=PROTOCOL_VERSION,
clientCapabilities=ClientCapabilities(),
clientInfo=Implementation(name="example-client", title="Example Client", version="0.1.0"),
protocol_version=PROTOCOL_VERSION,
client_capabilities=ClientCapabilities(),
client_info=Implementation(name="example-client", title="Example Client", version="0.1.0"),
)
)
session = await conn.newSession(NewSessionRequest(mcpServers=[], cwd=os.getcwd()))
session = await conn.newSession(NewSessionRequest(mcp_servers=[], cwd=os.getcwd()))

await interactive_loop(conn, session.sessionId)
await interactive_loop(conn, session.session_id)

if proc.returncode is None:
proc.terminate()
Expand Down
10 changes: 5 additions & 5 deletions examples/echo_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ def __init__(self, conn):
self._conn = conn

async def initialize(self, params: InitializeRequest) -> InitializeResponse:
return InitializeResponse(protocolVersion=params.protocolVersion)
return InitializeResponse(protocol_version=params.protocol_version)

async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
return NewSessionResponse(sessionId=uuid4().hex)
return NewSessionResponse(session_id=uuid4().hex)

async def prompt(self, params: PromptRequest) -> PromptResponse:
for block in params.prompt:
Expand All @@ -34,16 +34,16 @@ async def prompt(self, params: PromptRequest) -> PromptResponse:
chunk.field_meta = {"echo": True}
chunk.content.field_meta = {"echo": True}

notification = session_notification(params.sessionId, chunk)
notification = session_notification(params.session_id, chunk)
notification.field_meta = {"source": "echo_agent"}

await self._conn.sessionUpdate(notification)
return PromptResponse(stopReason="end_turn")
return PromptResponse(stop_reason="end_turn")


async def main() -> None:
reader, writer = await stdio_streams()
AgentSideConnection(lambda conn: EchoAgent(conn), writer, reader)
AgentSideConnection(EchoAgent, writer, reader)
await asyncio.Event().wait()


Expand Down
32 changes: 17 additions & 15 deletions examples/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ async def requestPermission(
option = _pick_preferred_option(params.options)
if option is None:
return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
return RequestPermissionResponse(outcome=AllowedOutcome(optionId=option.optionId, outcome="selected"))
return RequestPermissionResponse(outcome=AllowedOutcome(option_id=option.option_id, outcome="selected"))

title = params.toolCall.title or "<permission>"
title = params.tool_call.title or "<permission>"
if not params.options:
print(f"\n🔐 Permission requested: {title} (no options, cancelling)")
return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
Expand All @@ -92,7 +92,9 @@ async def requestPermission(
idx = int(choice) - 1
if 0 <= idx < len(params.options):
opt = params.options[idx]
return RequestPermissionResponse(outcome=AllowedOutcome(optionId=opt.optionId, outcome="selected"))
return RequestPermissionResponse(
outcome=AllowedOutcome(option_id=opt.option_id, outcome="selected")
)
print("Invalid selection, try again.")

async def writeTextFile(
Expand Down Expand Up @@ -141,13 +143,13 @@ async def sessionUpdate(
print(f"\n🔧 {update.title} ({update.status or 'pending'})")
elif isinstance(update, ToolCallProgress):
status = update.status or "in_progress"
print(f"\n🔧 Tool call `{update.toolCallId}` → {status}")
print(f"\n🔧 Tool call `{update.tool_call_id}` → {status}")
if update.content:
for item in update.content:
if isinstance(item, FileEditToolCallContent):
print(f" diff: {item.path}")
elif isinstance(item, TerminalToolCallContent):
print(f" terminal: {item.terminalId}")
print(f" terminal: {item.terminal_id}")
elif isinstance(item, dict):
print(f" content: {json.dumps(item, indent=2)}")
else:
Expand All @@ -159,7 +161,7 @@ async def createTerminal(
params: CreateTerminalRequest,
) -> CreateTerminalResponse: # type: ignore[override]
print(f"[Client] createTerminal: {params}")
return CreateTerminalResponse(terminalId="term-1")
return CreateTerminalResponse(terminal_id="term-1")

async def terminalOutput(
self,
Expand Down Expand Up @@ -246,13 +248,13 @@ async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None:
if line in {":exit", ":quit"}:
break
if line == ":cancel":
await conn.cancel(CancelNotification(sessionId=session_id))
await conn.cancel(CancelNotification(session_id=session_id))
continue

try:
await conn.prompt(
PromptRequest(
sessionId=session_id,
session_id=session_id,
prompt=[text_block(line)],
)
)
Expand Down Expand Up @@ -321,9 +323,9 @@ async def run(argv: list[str]) -> int:
try:
init_resp = await conn.initialize(
InitializeRequest(
protocolVersion=PROTOCOL_VERSION,
clientCapabilities=ClientCapabilities(
fs=FileSystemCapability(readTextFile=True, writeTextFile=True),
protocol_version=PROTOCOL_VERSION,
client_capabilities=ClientCapabilities(
fs=FileSystemCapability(read_text_file=True, write_text_file=True),
terminal=True,
),
)
Expand All @@ -337,13 +339,13 @@ async def run(argv: list[str]) -> int:
await _shutdown(proc, conn)
return 1

print(f"✅ Connected to Gemini (protocol v{init_resp.protocolVersion})")
print(f"✅ Connected to Gemini (protocol v{init_resp.protocol_version})")

try:
session = await conn.newSession(
NewSessionRequest(
cwd=os.getcwd(),
mcpServers=[],
mcp_servers=[],
)
)
except RequestError as err:
Expand All @@ -355,10 +357,10 @@ async def run(argv: list[str]) -> int:
await _shutdown(proc, conn)
return 1

print(f"📝 Created session: {session.sessionId}")
print(f"📝 Created session: {session.session_id}")

try:
await interactive_loop(conn, session.sessionId)
await interactive_loop(conn, session.session_id)
finally:
await _shutdown(proc, conn)

Expand Down
1 change: 1 addition & 0 deletions scripts/gen_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def generate_schema(*, format_output: bool = True) -> None:
"--output-model-type",
"pydantic_v2.BaseModel",
"--use-annotated",
"--snake-case-field",
]

subprocess.check_call(cmd) # noqa: S603
Expand Down
2 changes: 1 addition & 1 deletion src/acp/agent/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async def createTerminal(self, params: CreateTerminalRequest) -> TerminalHandle:
params,
CreateTerminalResponse,
)
return TerminalHandle(create_response.terminalId, params.sessionId, self._conn)
return TerminalHandle(create_response.terminal_id, params.session_id, self._conn)

async def terminalOutput(self, params: TerminalOutputRequest) -> TerminalOutputResponse:
return await request_model(
Expand Down
10 changes: 5 additions & 5 deletions src/acp/contrib/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def __init__(self) -> None:
def default_permission_options() -> tuple[PermissionOption, PermissionOption, PermissionOption]:
"""Return a standard approval/reject option set."""
return (
PermissionOption(optionId="approve", name="Approve", kind="allow_once"),
PermissionOption(optionId="approve_for_session", name="Approve for session", kind="allow_always"),
PermissionOption(optionId="reject", name="Reject", kind="reject_once"),
PermissionOption(option_id="approve", name="Approve", kind="allow_once"),
PermissionOption(option_id="approve_for_session", name="Approve for session", kind="allow_always"),
PermissionOption(option_id="reject", name="Reject", kind="reject_once"),
)


Expand Down Expand Up @@ -83,8 +83,8 @@ async def request_for(
raise MissingPermissionOptionsError()

request = RequestPermissionRequest(
sessionId=self._session_id,
toolCall=tool_call,
session_id=self._session_id,
tool_call=tool_call,
options=list(option_set),
)
return await self._requester(request)
Expand Down
26 changes: 13 additions & 13 deletions src/acp/contrib/session_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ def apply_start(self, update: ToolCallStart) -> None:
self.status = update.status
self.content = _copy_model_list(update.content)
self.locations = _copy_model_list(update.locations)
self.raw_input = update.rawInput
self.raw_output = update.rawOutput
self.raw_input = update.raw_input
self.raw_output = update.raw_output

def apply_progress(self, update: ToolCallProgress) -> None:
if update.title is not None:
Expand All @@ -76,10 +76,10 @@ def apply_progress(self, update: ToolCallProgress) -> None:
self.content = _copy_model_list(update.content)
if update.locations is not None:
self.locations = _copy_model_list(update.locations)
if update.rawInput is not None:
self.raw_input = update.rawInput
if update.rawOutput is not None:
self.raw_output = update.rawOutput
if update.raw_input is not None:
self.raw_input = update.raw_input
if update.raw_output is not None:
self.raw_output = update.raw_output

def snapshot(self) -> ToolCallView:
return ToolCallView(
Expand Down Expand Up @@ -185,11 +185,11 @@ def apply(self, notification: SessionNotification) -> SessionSnapshot:

def _ensure_session(self, notification: SessionNotification) -> None:
if self.session_id is None:
self.session_id = notification.sessionId
self.session_id = notification.session_id
return

if notification.sessionId != self.session_id:
self._handle_session_change(notification.sessionId)
if notification.session_id != self.session_id:
self._handle_session_change(notification.session_id)

def _handle_session_change(self, session_id: str) -> None:
expected = self.session_id
Expand All @@ -206,14 +206,14 @@ def _handle_session_change(self, session_id: str) -> None:
def _apply_update(self, update: Any) -> None:
if isinstance(update, ToolCallStart):
state = self._tool_calls.setdefault(
update.toolCallId, _MutableToolCallState(tool_call_id=update.toolCallId)
update.tool_call_id, _MutableToolCallState(tool_call_id=update.tool_call_id)
)
state.apply_start(update)
return

if isinstance(update, ToolCallProgress):
state = self._tool_calls.setdefault(
update.toolCallId, _MutableToolCallState(tool_call_id=update.toolCallId)
update.tool_call_id, _MutableToolCallState(tool_call_id=update.tool_call_id)
)
state.apply_progress(update)
return
Expand All @@ -223,11 +223,11 @@ def _apply_update(self, update: Any) -> None:
return

if isinstance(update, CurrentModeUpdate):
self._current_mode_id = update.currentModeId
self._current_mode_id = update.current_mode_id
return

if isinstance(update, AvailableCommandsUpdate):
self._available_commands = _copy_model_list(update.availableCommands) or []
self._available_commands = _copy_model_list(update.available_commands) or []
return

if isinstance(update, UserMessageChunk):
Expand Down
Loading