Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
12 changes: 10 additions & 2 deletions .github/workflows/on-release-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
types: [published]

jobs:

set-version:
runs-on: ubuntu-24.04
steps:
Expand Down Expand Up @@ -63,4 +62,13 @@ jobs:
uses: ./.github/actions/setup-python-env

- name: Deploy documentation
run: uv run mkdocs gh-deploy --force
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: acp-bot
GIT_AUTHOR_EMAIL: noreply@github.com
GIT_COMMITTER_NAME: acp-bot
GIT_COMMITTER_EMAIL: noreply@github.com
run: |
git config user.name "$GIT_AUTHOR_NAME"
git config user.email "$GIT_AUTHOR_EMAIL"
uv run mkdocs gh-deploy --force --remote-branch gh-pages --remote-name origin
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,10 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/

# .zed
.zed/

# others
reference/
.DS_Store
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Repository Guidelines

## Project Structure & Module Organization
The package code lives under `src/acp`, exposing the high-level Agent, transport helpers, and generated protocol schema. Generated artifacts such as `schema/` and `src/acp/schema.py` are refreshed via `scripts/gen_all.py` against the upstream ACP schema. Integration examples are in `examples/`, including `echo_agent.py` and the mini SWE bridge. Tests reside in `tests/` with async fixtures and doctests; documentation sources live in `docs/` and publish via MkDocs. Built distributions drop into `dist/` after builds.

## Build, Test, and Development Commands
Run `make install` to create a `uv` managed virtualenv and install pre-commit hooks. `make check` executes lock verification, Ruff linting, `ty` static checks, and deptry analysis. `make test` calls `uv run python -m pytest --doctest-modules`. For release prep use `make build` or `make build-and-publish`. `make gen-all` regenerates protocol models; export `ACP_SCHEMA_VERSION=<ref>` beforehand to fetch a specific upstream schema (defaults to the cached copy). `make docs` serves MkDocs locally; `make docs-test` ensures clean builds.

## Coding Style & Naming Conventions
Target Python 3.10+ with type hints and 120-character lines enforced by Ruff (`pyproject.toml`). Prefer dataclasses/pydantic models from the schema modules rather than bare dicts. Tests may ignore security lint (see per-file ignores) but still follow snake_case names. Keep public API modules under `acp/*` lean; place utilities in internal `_`-prefixed modules when needed.

## Testing Guidelines
Pytest is the main framework with `pytest-asyncio` for coroutine tests and doctests activated on modules. Name test files `test_*.py` and co-locate fixtures under `tests/conftest.py`. Aim to cover new protocol surfaces with integration-style tests using the async agent stubs. Generate coverage reports via `tox -e py310` when assessing CI parity.

## Commit & Pull Request Guidelines
Commit history follows Conventional Commits (`feat:`, `fix:`, `docs:`). Scope commits narrowly and include context on affected protocol version or tooling. PRs should describe agent behaviors exercised, link related issues, and mention schema regeneration if applicable. Attach test output (`make check` or targeted pytest) and screenshots only when UI-adjacent docs change. Update docs/examples when altering the public agent API.

## Agent Integration Tips
Leverage `examples/mini_swe_agent/` as a template when bridging other command executors. Use `AgentSideConnection` with `stdio_streams()` for ACP-compliant clients; document any extra environment variables in README updates.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A Python implementation of the Agent Client Protocol (ACP). Use it to build agents that communicate with ACP-capable clients (e.g. Zed) over stdio.

> This library version is compatible with the corresponding ACP version. However, we lack sufficient resources to release each version. Welcome to contribute!

- Package name: `agent-client-protocol` (import as `acp`)
- Repository: https://github.com/psiace/agent-client-protocol-python
- Docs: https://psiace.github.io/agent-client-protocol-python/
Expand All @@ -19,6 +21,7 @@ uv add agent-client-protocol

```bash
make install # set up venv
ACP_SCHEMA_VERSION=<ref> make gen-all # generate meta.py & schema.py
make check # lint + typecheck
make test # run tests
```
Expand All @@ -42,7 +45,7 @@ from acp import (
SessionNotification,
stdio_streams,
)
from acp.schema import ContentBlock1, SessionUpdate2
from acp.schema import TextContentBlock, AgentMessageChunk


class EchoAgent(Agent):
Expand All @@ -61,9 +64,9 @@ class EchoAgent(Agent):
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=text),
content=TextContentBlock(type="text", text=text),
),
)
)
Expand Down
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ from acp import (
SessionNotification,
stdio_streams,
)
from acp.schema import ContentBlock1, SessionUpdate2
from acp.schema import TextContentBlock, AgentMessageChunk


class EchoAgent(Agent):
Expand All @@ -44,9 +44,9 @@ class EchoAgent(Agent):
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=text),
content=TextContentBlock(type="text", text=text),
),
)
)
Expand Down
6 changes: 3 additions & 3 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ from acp import (
SessionNotification,
stdio_streams,
)
from acp.schema import ContentBlock1, SessionUpdate2
from acp.schema import TextContentBlock, AgentMessageChunk


class EchoAgent(Agent):
Expand All @@ -44,9 +44,9 @@ class EchoAgent(Agent):
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=text),
content=TextContentBlock(type="text", text=text),
),
)
)
Expand Down
12 changes: 6 additions & 6 deletions examples/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
stdio_streams,
PROTOCOL_VERSION,
)
from acp.schema import ContentBlock1, SessionUpdate2
from acp.schema import TextContentBlock, AgentMessageChunk


class ExampleAgent(Agent):
Expand Down Expand Up @@ -50,9 +50,9 @@ async def prompt(self, params: PromptRequest) -> PromptResponse:
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text="Client sent: "),
content=TextContentBlock(type="text", text="Client sent: "),
),
)
)
Expand All @@ -65,14 +65,14 @@ async def prompt(self, params: PromptRequest) -> PromptResponse:
else:
text = f"<{block.get('type', 'content')}>"
else:
# pydantic model ContentBlock1
# pydantic model TextContentBlock
text = getattr(block, "text", "<content>")
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=text),
content=TextContentBlock(type="text", text=text),
),
)
)
Expand Down
6 changes: 3 additions & 3 deletions examples/echo_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
SessionNotification,
stdio_streams,
)
from acp.schema import ContentBlock1, SessionUpdate2
from acp.schema import TextContentBlock, AgentMessageChunk


class EchoAgent(Agent):
Expand All @@ -31,9 +31,9 @@ async def prompt(self, params: PromptRequest) -> PromptResponse:
await self._conn.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=text),
content=TextContentBlock(type="text", text=text),
),
)
)
Expand Down
61 changes: 30 additions & 31 deletions examples/mini_swe_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,17 @@
PROTOCOL_VERSION,
)
from acp.schema import (
ContentBlock1,
AgentMessageChunk,
AgentThoughtChunk,
AllowedOutcome,
ContentToolCallContent,
PermissionOption,
RequestPermissionRequest,
RequestPermissionResponse,
RequestPermissionOutcome1,
RequestPermissionOutcome2,
SessionUpdate1,
SessionUpdate2,
SessionUpdate3,
SessionUpdate4,
SessionUpdate5,
ToolCallContent1,
TextContentBlock,
ToolCallStart,
ToolCallUpdate,
UserMessageChunk,
)


Expand Down Expand Up @@ -130,9 +128,9 @@ def _send_cost_hint(self) -> None:
cost = float(getattr(self.model, "cost", 0.0))
except Exception:
cost = 0.0
hint = SessionUpdate3(
hint = AgentThoughtChunk(
sessionUpdate="agent_thought_chunk",
content=ContentBlock1(type="text", text=f"__COST__:{cost:.2f}"),
content=TextContentBlock(type="text", text=f"__COST__:{cost:.2f}"),
)
try:
loop = asyncio.get_running_loop()
Expand All @@ -142,15 +140,15 @@ def _send_cost_hint(self) -> None:

async def on_tool_start(self, title: str, command: str, tool_call_id: str) -> None:
"""Send a tool_call start notification for a bash command."""
update = SessionUpdate4(
update = ToolCallStart(
sessionUpdate="tool_call",
toolCallId=tool_call_id,
title=title,
kind="execute",
status="pending",
content=[
ToolCallContent1(
type="content", content=ContentBlock1(type="text", text=f"```bash\n{command}\n```")
ContentToolCallContent(
type="content", content=TextContentBlock(type="text", text=f"```bash\n{command}\n```")
)
],
rawInput={"command": command},
Expand All @@ -166,13 +164,13 @@ async def on_tool_complete(
status: str = "completed",
) -> None:
"""Send a tool_call_update with the final output and return code."""
update = SessionUpdate5(
update = ToolCallUpdate(
sessionUpdate="tool_call_update",
toolCallId=tool_call_id,
status=status,
content=[
ToolCallContent1(
type="content", content=ContentBlock1(type="text", text=f"```ansi\n{output}\n```")
ContentToolCallContent(
type="content", content=TextContentBlock(type="text", text=f"```ansi\n{output}\n```")
)
],
rawOutput={"output": output, "returncode": returncode},
Expand All @@ -185,8 +183,8 @@ def add_message(self, role: str, content: str, **kwargs):
if not getattr(self, "_emit_updates", True) or role != "assistant":
return
text = str(content)
block = ContentBlock1(type="text", text=text)
update = SessionUpdate2(sessionUpdate="agent_message_chunk", content=block)
block = TextContentBlock(type="text", text=text)
update = AgentMessageChunk(sessionUpdate="agent_message_chunk", content=block)
try:
loop = asyncio.get_running_loop()
loop.create_task(self._send(update))
Expand All @@ -203,14 +201,15 @@ def _confirm_action_sync(self, tool_call_id: str, command: str) -> bool:
PermissionOption(optionId="reject-once", name="Reject", kind="reject_once"),
],
toolCall=ToolCallUpdate(
sessionUpdate="tool_call_update",
toolCallId=tool_call_id,
title="bash",
kind="execute",
status="pending",
content=[
ToolCallContent1(
ContentToolCallContent(
type="content",
content=ContentBlock1(type="text", text=f"```bash\n{command}\n```"),
content=TextContentBlock(type="text", text=f"```bash\n{command}\n```"),
)
],
rawInput={"command": command},
Expand All @@ -222,7 +221,7 @@ def _confirm_action_sync(self, tool_call_id: str, command: str) -> bool:
except Exception:
return False
out = resp.outcome
if isinstance(out, RequestPermissionOutcome2) and out.optionId in ("allow-once", "allow-always"):
if isinstance(out, AllowedOutcome) and out.optionId in ("allow-once", "allow-always"):
return True
return False

Expand Down Expand Up @@ -253,7 +252,7 @@ def execute_action(self, action: dict) -> dict: # type: ignore[override]
# Mark in progress
self._schedule(
self._send(
SessionUpdate5(
ToolCallUpdate(
sessionUpdate="tool_call_update",
toolCallId=tool_id,
status="in_progress",
Expand Down Expand Up @@ -428,9 +427,9 @@ async def prompt(self, params: PromptRequest) -> PromptResponse:
await self._client.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(
content=TextContentBlock(
type="text",
text=(
"mini-swe-agent load error: "
Expand Down Expand Up @@ -476,9 +475,9 @@ async def prompt(self, params: PromptRequest) -> PromptResponse:
await self._client.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text="Human mode: please submit a bash command."),
content=TextContentBlock(type="text", text="Human mode: please submit a bash command."),
),
)
)
Expand Down Expand Up @@ -508,9 +507,9 @@ async def prompt(self, params: PromptRequest) -> PromptResponse:
await self._client.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(
content=TextContentBlock(
type="text",
text=(
"Agent finished. Type a new task in the next message to continue, or do nothing to end."
Expand All @@ -528,9 +527,9 @@ async def prompt(self, params: PromptRequest) -> PromptResponse:
await self._client.sessionUpdate(
SessionNotification(
sessionId=params.sessionId,
update=SessionUpdate2(
update=AgentMessageChunk(
sessionUpdate="agent_message_chunk",
content=ContentBlock1(type="text", text=f"Error while processing: {e}"),
content=TextContentBlock(type="text", text=f"Error while processing: {e}"),
),
)
)
Expand Down
Loading