Skip to content

Commit d17e4ea

Browse files
author
Chojan Shang
committed
refactor: polish all code to align spec impl
Signed-off-by: Chojan Shang <chojan.shang@vesoft.com>
1 parent 1839086 commit d17e4ea

27 files changed

+611
-1513
lines changed

AGENTS.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
# Repository Guidelines
22

33
## Project Structure & Module Organization
4-
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.
4+
The runtime package lives in `src/acp`, exposing the top-level agent entrypoints, transport adapters, and the generated `schema.py`. Regenerate protocol artifacts via `scripts/gen_all.py`, which refreshes both `schema/` and `src/acp/schema.py`. Examples demonstrating stdio bridges and quick-start flows are under `examples/`, while async-focused tests and fixtures sit in `tests/`. Documentation sources for MkDocs reside in `docs/`, and built artifacts land in `dist/` after release builds.
55

66
## Build, Test, and Development Commands
7-
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.
7+
- `make install`: Provisions a `uv`-managed virtualenv and installs pre-commit hooks.
8+
- `make check`: Runs lock verification, Ruff linting, `ty` type analysis, and deptry dependency checks.
9+
- `make test`: Executes `uv run python -m pytest --doctest-modules` across the suite.
10+
- `make gen-all`: Regenerates protocol schemas (set `ACP_SCHEMA_VERSION=<ref>` to target a specific upstream tag).
11+
- `make build` / `make build-and-publish`: Produce or ship distribution artifacts.
12+
- `make docs` and `make docs-test`: Serve or validate MkDocs documentation locally.
813

914
## Coding Style & Naming Conventions
10-
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.
15+
Target Python 3.10+ with 4-space indentation and type hints on public APIs. Ruff (configured via `pyproject.toml`) enforces formatting, 120-character lines, and linting; keep `ruff --fix` output clean before opening a PR. Prefer dataclasses and Pydantic models generated in `acp.schema` over ad-hoc dicts. Place shared utilities in `_`-prefixed internal modules and keep public surfaces lean.
1116

1217
## Testing Guidelines
13-
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.
18+
Pytest with `pytest-asyncio` powers the suite, and doctests are enabled for modules. Name test files `test_*.py`, keep fixtures in `tests/conftest.py`, and run `make test` before pushing. For deeper coverage investigation, run `tox -e py310` and review the HTML report in `.tox/py310/tmp/coverage`.
1419

1520
## Commit & Pull Request Guidelines
16-
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.
21+
Follow Conventional Commits (`feat:`, `fix:`, `docs:`) with narrow scopes and mention schema regeneration when applicable. PRs should describe exercised agent behaviors, link related issues, and attach `make check` (or targeted pytest) output. Update docs and examples whenever public agent APIs change, and include environment notes for new agent integrations.
1722

1823
## Agent Integration Tips
19-
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.
24+
Use `examples/echo_agent.py` as the minimal agent template, or look at `examples/client.py` and `examples/duet.py` for spawning patterns that rely on `spawn_agent_process`/`spawn_client_process`. Document any environment requirements in `README.md`, and verify round-trip messaging with the echo agent before extending transports.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,15 @@ Full example with streaming and lifecycle hooks lives in [examples/echo_agent.py
7171

7272
## Examples
7373

74-
- `examples/mini_swe_agent`: bridges mini-swe-agent into ACP, including a duet launcher and Textual TUI client
75-
- Additional transport helpers are documented in the [Mini SWE guide](docs/mini-swe-agent.md)
74+
- `examples/echo_agent.py`: self-contained streaming agent suitable for smoke tests
75+
- `examples/client.py`: interactive console client that can spawn any ACP agent subprocess
76+
- `examples/duet.py`: demo launcher that starts both the example client and agent together
7677

7778
## Documentation
7879

7980
- Project docs (MkDocs): https://psiace.github.io/agent-client-protocol-python/
8081
- Local sources: `docs/`
8182
- [Quickstart](docs/quickstart.md)
82-
- [Mini SWE Agent bridge](docs/mini-swe-agent.md)
8383

8484
## Development workflow
8585

docs/index.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,5 @@ Prefer a guided tour? Head to the [Quickstart](quickstart.md) for step-by-step i
2525
## Documentation map
2626

2727
- [Quickstart](quickstart.md): install, run, and extend the echo agent
28-
- [Mini SWE Agent guide](mini-swe-agent.md): bridge mini-swe-agent over ACP, including duet launcher and Textual client
2928

3029
Source code lives under `src/acp/`, while tests and additional examples are available in `tests/` and `examples/`. If you plan to contribute, see the repository README for the development workflow.

docs/mini-swe-agent.md

Lines changed: 0 additions & 54 deletions
This file was deleted.

docs/quickstart.md

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,62 @@ Open the Agents panel and start the session. Each message you send should be ech
5050

5151
Any ACP client that communicates over stdio can spawn the same script; no additional transport configuration is required.
5252

53+
### Programmatic launch
54+
55+
You can also embed the agent inside another Python process without shelling out manually. Use
56+
`acp.spawn_agent_process` to bootstrap the child and receive a `ClientSideConnection`:
57+
58+
```python
59+
import asyncio
60+
import sys
61+
from pathlib import Path
62+
63+
from acp import spawn_agent_process
64+
from acp.interfaces import Client
65+
from acp.schema import (
66+
DeniedOutcome,
67+
InitializeRequest,
68+
NewSessionRequest,
69+
PromptRequest,
70+
RequestPermissionRequest,
71+
RequestPermissionResponse,
72+
SessionNotification,
73+
TextContentBlock,
74+
)
75+
76+
77+
class SimpleClient(Client):
78+
async def requestPermission(self, params: RequestPermissionRequest) -> RequestPermissionResponse:
79+
return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
80+
81+
async def sessionUpdate(self, params: SessionNotification) -> None: # noqa: D401 - logging only
82+
print("update:", params)
83+
84+
# Optional client methods omitted for brevity
85+
86+
87+
async def main() -> None:
88+
script = Path("examples/echo_agent.py").resolve()
89+
90+
async with spawn_agent_process(lambda agent: SimpleClient(), sys.executable, str(script)) as (
91+
conn,
92+
_process,
93+
):
94+
await conn.initialize(InitializeRequest(protocolVersion=1))
95+
session = await conn.newSession(NewSessionRequest(cwd=str(script.parent), mcpServers=[]))
96+
await conn.prompt(
97+
PromptRequest(
98+
sessionId=session.sessionId,
99+
prompt=[TextContentBlock(type="text", text="Hello from spawn!")],
100+
)
101+
)
102+
103+
asyncio.run(main())
104+
```
105+
106+
Inside the context manager the subprocess is monitored, stdin/stdout are tied into ACP, and the
107+
connection cleans itself up on exit.
108+
53109
## 4. Extend the agent
54110

55111
Create your own agent by subclassing `acp.Agent`. The pattern mirrors the echo example:
@@ -65,14 +121,3 @@ class MyAgent(Agent):
65121
```
66122

67123
Hook it up with `AgentSideConnection` inside an async entrypoint and wire it to your client. Refer to [examples/echo_agent.py](https://github.com/psiace/agent-client-protocol-python/blob/main/examples/echo_agent.py) for the complete structure, including lifetime hooks (`initialize`, `newSession`) and streaming responses.
68-
69-
## Optional: Mini SWE Agent bridge
70-
71-
The repository also ships a bridge for [mini-swe-agent](https://github.com/groundx-ai/mini-swe-agent). To try it:
72-
73-
1. Install the dependency:
74-
```bash
75-
pip install mini-swe-agent
76-
```
77-
2. Configure Zed to run `examples/mini_swe_agent/agent.py` and supply environment variables such as `MINI_SWE_MODEL` and `OPENROUTER_API_KEY`.
78-
3. Review the [Mini SWE Agent guide](mini-swe-agent.md) for environment options, tool-call mapping, and a duet launcher that starts both the bridge and a Textual client (`python examples/mini_swe_agent/duet.py`).

examples/agent.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,16 @@ async def _send_chunk(self, session_id: str, content: Any) -> None:
4949

5050
async def initialize(self, params: InitializeRequest) -> InitializeResponse: # noqa: ARG002
5151
logging.info("Received initialize request")
52+
mcp_caps: McpCapabilities = McpCapabilities(http=False, sse=False)
53+
prompt_caps: PromptCapabilities = PromptCapabilities(audio=False, embeddedContext=False, image=False)
54+
agent_caps: AgentCapabilities = AgentCapabilities(
55+
loadSession=False,
56+
mcpCapabilities=mcp_caps,
57+
promptCapabilities=prompt_caps,
58+
)
5259
return InitializeResponse(
5360
protocolVersion=PROTOCOL_VERSION,
54-
agentCapabilities=AgentCapabilities(
55-
loadSession=False,
56-
mcpCapabilities=McpCapabilities(http=False, sse=False),
57-
promptCapabilities=PromptCapabilities(audio=False, embeddedContext=False, image=False),
58-
),
61+
agentCapabilities=agent_caps,
5962
)
6063

6164
async def authenticate(self, params: AuthenticateRequest) -> AuthenticateResponse | None: # noqa: ARG002

examples/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import asyncio.subprocess as aio_subprocess
23
import contextlib
34
import logging
45
import os
@@ -118,8 +119,8 @@ async def main(argv: list[str]) -> int:
118119
proc = await asyncio.create_subprocess_exec(
119120
spawn_program,
120121
*spawn_args,
121-
stdin=asyncio.subprocess.PIPE,
122-
stdout=asyncio.subprocess.PIPE,
122+
stdin=aio_subprocess.PIPE,
123+
stdout=aio_subprocess.PIPE,
123124
)
124125

125126
if proc.stdin is None or proc.stdout is None:

examples/duet.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
11
import asyncio
2+
import importlib.util
23
import os
34
import sys
45
from pathlib import Path
56

67

8+
def _load_client_module(path: Path):
9+
spec = importlib.util.spec_from_file_location("examples_client", path)
10+
if spec is None or spec.loader is None:
11+
raise RuntimeError(f"Unable to load client module from {path}")
12+
module = importlib.util.module_from_spec(spec)
13+
sys.modules.setdefault("examples_client", module)
14+
spec.loader.exec_module(module)
15+
return module
16+
17+
18+
from acp import PROTOCOL_VERSION, spawn_agent_process
19+
from acp.schema import InitializeRequest, NewSessionRequest
20+
21+
722
async def main() -> int:
823
root = Path(__file__).resolve().parent
9-
agent_path = str(root / "agent.py")
10-
client_path = str(root / "client.py")
24+
agent_path = root / "agent.py"
1125

12-
# Ensure PYTHONPATH includes project src for `from acp import ...`
1326
env = os.environ.copy()
1427
src_dir = str((root.parent / "src").resolve())
1528
env["PYTHONPATH"] = src_dir + os.pathsep + env.get("PYTHONPATH", "")
1629

17-
# Run the client and let it spawn the agent, wiring stdio automatically.
18-
proc = await asyncio.create_subprocess_exec(
19-
sys.executable,
20-
client_path,
21-
agent_path,
22-
env=env,
23-
)
24-
return await proc.wait()
30+
client_module = _load_client_module(root / "client.py")
31+
client = client_module.ExampleClient()
32+
33+
async with spawn_agent_process(lambda _agent: client, sys.executable, str(agent_path), env=env) as (
34+
conn,
35+
process,
36+
):
37+
await conn.initialize(InitializeRequest(protocolVersion=PROTOCOL_VERSION, clientCapabilities=None))
38+
session = await conn.newSession(NewSessionRequest(mcpServers=[], cwd=str(root)))
39+
await client_module.interactive_loop(conn, session.sessionId)
40+
41+
return process.returncode or 0
2542

2643

2744
if __name__ == "__main__":

examples/mini_swe_agent/README.md

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)