Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,3 @@ repos:
args: [ --exit-non-zero-on-fix ]
exclude: ^src/acp/(meta|schema)\.py$
- id: ruff-format
exclude: ^src/acp/(meta|schema)\.py$
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ install: ## Install the virtual environment and install the pre-commit hooks
gen-all: ## Generate all code from schema
@echo "🚀 Generating all code"
@uv run scripts/gen_all.py
@uv run ruff check --fix
@uv run ruff format .

.PHONY: check
check: ## Run code quality tools.
Expand Down
25 changes: 12 additions & 13 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,26 @@ from pathlib import Path

from acp import spawn_agent_process, text_block
from acp.interfaces import Client
from acp.schema import InitializeRequest, NewSessionRequest, PromptRequest, SessionNotification


class SimpleClient(Client):
async def requestPermission(self, params): # pragma: no cover - minimal stub
async def request_permission(
self, options, session_id, tool_call, **kwargs: Any
)
return {"outcome": {"outcome": "cancelled"}}

async def sessionUpdate(self, params: SessionNotification) -> None:
print("update:", params.session_id, params.update)
async def session_update(self, session_id, update, **kwargs):
print("update:", session_id, 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(protocol_version=1))
session = await conn.newSession(NewSessionRequest(cwd=str(script.parent), mcp_servers=[]))
await conn.initialize(protocol_version=1)
session = await conn.new_session(cwd=str(script.parent), mcp_servers=[])
await conn.prompt(
PromptRequest(
session_id=session.session_id,
prompt=[text_block("Hello from spawn!")],
)
session_id=session.session_id,
prompt=[text_block("Hello from spawn!")],
)

asyncio.run(main())
Expand All @@ -111,12 +110,12 @@ _Swap the echo demo for your own `Agent` subclass._
Create your own agent by subclassing `acp.Agent`. The pattern mirrors the echo example:

```python
from acp import Agent, PromptRequest, PromptResponse
from acp import Agent, PromptResponse


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

Expand Down
90 changes: 58 additions & 32 deletions examples/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,31 @@
from acp import (
Agent,
AgentSideConnection,
AuthenticateRequest,
AuthenticateResponse,
CancelNotification,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
LoadSessionResponse,
NewSessionRequest,
NewSessionResponse,
PromptRequest,
PromptResponse,
SetSessionModeRequest,
SetSessionModeResponse,
session_notification,
stdio_streams,
text_block,
update_agent_message,
PROTOCOL_VERSION,
)
from acp.schema import AgentCapabilities, AgentMessageChunk, Implementation
from acp.schema import (
AgentCapabilities,
AgentMessageChunk,
AudioContentBlock,
ClientCapabilities,
EmbeddedResourceContentBlock,
HttpMcpServer,
ImageContentBlock,
Implementation,
ResourceContentBlock,
SseMcpServer,
StdioMcpServer,
TextContentBlock,
)


class ExampleAgent(Agent):
Expand All @@ -35,54 +40,75 @@ def __init__(self, conn: AgentSideConnection) -> None:

async def _send_agent_message(self, session_id: str, content: Any) -> None:
update = content if isinstance(content, AgentMessageChunk) else update_agent_message(content)
await self._conn.sessionUpdate(session_notification(session_id, update))

async def initialize(self, params: InitializeRequest) -> InitializeResponse: # noqa: ARG002
await self._conn.session_update(session_id, update)

async def initialize(
self,
protocol_version: int,
client_capabilities: ClientCapabilities | None = None,
client_info: Implementation | None = None,
**kwargs: Any,
) -> InitializeResponse:
logging.info("Received initialize request")
return InitializeResponse(
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.method_id)
async def authenticate(self, method_id: str, **kwargs: Any) -> AuthenticateResponse | None:
logging.info("Received authenticate request %s", method_id)
return AuthenticateResponse()

async def newSession(self, params: NewSessionRequest) -> NewSessionResponse: # noqa: ARG002
async def new_session(
self, cwd: str, mcp_servers: list[HttpMcpServer | SseMcpServer | StdioMcpServer], **kwargs: Any
) -> NewSessionResponse:
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(session_id=session_id, modes=None)

async def loadSession(self, params: LoadSessionRequest) -> LoadSessionResponse | None: # noqa: ARG002
logging.info("Received load session request %s", params.session_id)
self._sessions.add(params.session_id)
async def load_session(
self, cwd: str, mcp_servers: list[HttpMcpServer | SseMcpServer | StdioMcpServer], session_id: str, **kwargs: Any
) -> LoadSessionResponse | None:
logging.info("Received load session request %s", session_id)
self._sessions.add(session_id)
return LoadSessionResponse()

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

async def prompt(self, params: PromptRequest) -> PromptResponse:
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.session_id, text_block("Client sent:"))
for block in params.prompt:
await self._send_agent_message(params.session_id, block)
async def prompt(
self,
prompt: list[
TextContentBlock
| ImageContentBlock
| AudioContentBlock
| ResourceContentBlock
| EmbeddedResourceContentBlock
],
session_id: str,
**kwargs: Any,
) -> PromptResponse:
logging.info("Received prompt request for session %s", session_id)
if session_id not in self._sessions:
self._sessions.add(session_id)

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")

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

async def extMethod(self, method: str, params: dict) -> dict: # noqa: ARG002
async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
logging.info("Received extension method call: %s", method)
return {"example": "response"}

async def extNotification(self, method: str, params: dict) -> None: # noqa: ARG002
async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
logging.info("Received extension notification: %s", method)


Expand Down
92 changes: 69 additions & 23 deletions examples/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import sys
from pathlib import Path
from typing import Any

from acp import (
Client,
Expand All @@ -13,49 +14,98 @@
NewSessionRequest,
PromptRequest,
RequestError,
SessionNotification,
text_block,
PROTOCOL_VERSION,
)
from acp.schema import (
AgentMessageChunk,
AgentPlanUpdate,
AgentThoughtChunk,
AudioContentBlock,
AvailableCommandsUpdate,
ClientCapabilities,
CreateTerminalResponse,
CurrentModeUpdate,
EmbeddedResourceContentBlock,
EnvVariable,
ImageContentBlock,
Implementation,
KillTerminalCommandResponse,
PermissionOption,
ReadTextFileResponse,
ReleaseTerminalResponse,
RequestPermissionResponse,
ResourceContentBlock,
TerminalOutputResponse,
TextContentBlock,
ToolCall,
ToolCallProgress,
ToolCallStart,
UserMessageChunk,
WaitForTerminalExitResponse,
WriteTextFileResponse,
)


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

async def writeTextFile(self, params): # type: ignore[override]
async def write_text_file(
self, content: str, path: str, session_id: str, **kwargs: Any
) -> WriteTextFileResponse | None:
raise RequestError.method_not_found("fs/write_text_file")

async def readTextFile(self, params): # type: ignore[override]
async def read_text_file(
self, path: str, session_id: str, limit: int | None = None, line: int | None = None, **kwargs: Any
) -> ReadTextFileResponse:
raise RequestError.method_not_found("fs/read_text_file")

async def createTerminal(self, params): # type: ignore[override]
async def create_terminal(
self,
command: str,
session_id: str,
args: list[str] | None = None,
cwd: str | None = None,
env: list[EnvVariable] | None = None,
output_byte_limit: int | None = None,
**kwargs: Any,
) -> CreateTerminalResponse:
raise RequestError.method_not_found("terminal/create")

async def terminalOutput(self, params): # type: ignore[override]
async def terminal_output(self, session_id: str, terminal_id: str, **kwargs: Any) -> TerminalOutputResponse:
raise RequestError.method_not_found("terminal/output")

async def releaseTerminal(self, params): # type: ignore[override]
async def release_terminal(
self, session_id: str, terminal_id: str, **kwargs: Any
) -> ReleaseTerminalResponse | None:
raise RequestError.method_not_found("terminal/release")

async def waitForTerminalExit(self, params): # type: ignore[override]
async def wait_for_terminal_exit(
self, session_id: str, terminal_id: str, **kwargs: Any
) -> WaitForTerminalExitResponse:
raise RequestError.method_not_found("terminal/wait_for_exit")

async def killTerminal(self, params): # type: ignore[override]
async def kill_terminal(
self, session_id: str, terminal_id: str, **kwargs: Any
) -> KillTerminalCommandResponse | None:
raise RequestError.method_not_found("terminal/kill")

async def sessionUpdate(self, params: SessionNotification) -> None:
update = params.update
async def session_update(
self,
session_id: str,
update: UserMessageChunk
| AgentMessageChunk
| AgentThoughtChunk
| ToolCallStart
| ToolCallProgress
| AgentPlanUpdate
| AvailableCommandsUpdate
| CurrentModeUpdate,
**kwargs: Any,
) -> None:
if not isinstance(update, AgentMessageChunk):
return

Expand All @@ -76,10 +126,10 @@ async def sessionUpdate(self, params: SessionNotification) -> None:

print(f"| Agent: {text}")

async def extMethod(self, method: str, params: dict) -> dict: # noqa: ARG002
async def ext_method(self, method: str, params: dict) -> dict:
raise RequestError.method_not_found(method)

async def extNotification(self, method: str, params: dict) -> None: # noqa: ARG002
async def ext_notification(self, method: str, params: dict) -> None:
raise RequestError.method_not_found(method)


Expand All @@ -103,10 +153,8 @@ async def interactive_loop(conn: ClientSideConnection, session_id: str) -> None:

try:
await conn.prompt(
PromptRequest(
session_id=session_id,
prompt=[text_block(line)],
)
session_id=session_id,
prompt=[text_block(line)],
)
except Exception as exc: # noqa: BLE001
logging.error("Prompt failed: %s", exc)
Expand Down Expand Up @@ -145,13 +193,11 @@ async def main(argv: list[str]) -> int:
conn = ClientSideConnection(lambda _agent: client_impl, proc.stdin, proc.stdout)

await conn.initialize(
InitializeRequest(
protocol_version=PROTOCOL_VERSION,
client_capabilities=ClientCapabilities(),
client_info=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(mcp_servers=[], cwd=os.getcwd()))
session = await conn.new_session(mcp_servers=[], cwd=os.getcwd())

await interactive_loop(conn, session.session_id)

Expand Down
6 changes: 3 additions & 3 deletions examples/duet.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ async def main() -> int:
conn,
process,
):
await conn.initialize(InitializeRequest(protocolVersion=PROTOCOL_VERSION, clientCapabilities=None))
session = await conn.newSession(NewSessionRequest(mcpServers=[], cwd=str(root)))
await client_module.interactive_loop(conn, session.sessionId)
await conn.initialize(protocol_version=PROTOCOL_VERSION, client_capabilities=None)
session = await conn.new_session(mcp_servers=[], cwd=str(root))
await client_module.interactive_loop(conn, session.session_id)

return process.returncode or 0

Expand Down
Loading