Skip to content

Commit ad12f83

Browse files
committed
feat: the first glance for python sdk
1 parent 88b784b commit ad12f83

File tree

23 files changed

+5271
-27
lines changed

23 files changed

+5271
-27
lines changed

.github/workflows/on-release-main.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,3 @@ jobs:
6464

6565
- name: Deploy documentation
6666
run: uv run mkdocs gh-deploy --force
67-

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ repos:
1919
hooks:
2020
- id: ruff-check
2121
args: [ --exit-non-zero-on-fix ]
22+
exclude: ^src/acp/(meta|schema)\.py$
2223
- id: ruff-format
24+
exclude: ^src/acp/(meta|schema)\.py$

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ install: ## Install the virtual environment and install the pre-commit hooks
44
@uv sync
55
@uv run pre-commit install
66

7+
.PHONY: gen-all
8+
gen-all: ## Generate all code from schema
9+
@echo "🚀 Generating all code"
10+
@uv run scripts/gen_all.py
11+
712
.PHONY: check
813
check: ## Run code quality tools.
914
@echo "🚀 Checking lock file consistency with 'pyproject.toml'"
1015
@uv lock --locked
1116
@echo "🚀 Linting code: Running pre-commit"
1217
@uv run pre-commit run -a
1318
@echo "🚀 Static type checking: Running ty"
14-
@uv run ty check
19+
@uv run ty check --exclude "src/acp/meta.py" --exclude "src/acp/schema.py" --exclude "examples/*.py"
1520
@echo "🚀 Checking for obsolete dependencies: Running deptry"
1621
@uv run deptry src
1722

docs/modules.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

examples/agent.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import asyncio
2+
3+
from acp import (
4+
Agent,
5+
AgentSideConnection,
6+
AuthenticateRequest,
7+
CancelNotification,
8+
InitializeRequest,
9+
InitializeResponse,
10+
LoadSessionRequest,
11+
NewSessionRequest,
12+
NewSessionResponse,
13+
PromptRequest,
14+
PromptResponse,
15+
stdio_streams,
16+
)
17+
18+
19+
class EchoAgent(Agent):
20+
async def initialize(self, params: InitializeRequest) -> InitializeResponse:
21+
# Avoid serializer warnings by omitting defaults
22+
return InitializeResponse(protocolVersion=params.protocolVersion, agentCapabilities=None, authMethods=[])
23+
24+
async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
25+
return NewSessionResponse(sessionId="sess-1")
26+
27+
async def loadSession(self, params: LoadSessionRequest) -> None:
28+
return None
29+
30+
async def authenticate(self, params: AuthenticateRequest) -> None:
31+
return None
32+
33+
async def prompt(self, params: PromptRequest) -> PromptResponse:
34+
# Normally you'd stream updates via sessionUpdate
35+
return PromptResponse(stopReason="end_turn")
36+
37+
async def cancel(self, params: CancelNotification) -> None:
38+
return None
39+
40+
41+
async def main() -> None:
42+
reader, writer = await stdio_streams()
43+
# For an agent process, local writes go to client stdin (writer=stdout)
44+
AgentSideConnection(lambda _conn: EchoAgent(), writer, reader)
45+
# Keep running; in a real agent you would await tasks or add your own loop
46+
await asyncio.Event().wait()
47+
48+
49+
if __name__ == "__main__":
50+
asyncio.run(main())

examples/client.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import asyncio
2+
import os
3+
import sys
4+
5+
from acp import (
6+
Client,
7+
PROTOCOL_VERSION,
8+
ClientSideConnection,
9+
InitializeRequest,
10+
NewSessionRequest,
11+
PromptRequest,
12+
ReadTextFileRequest,
13+
ReadTextFileResponse,
14+
RequestPermissionRequest,
15+
RequestPermissionResponse,
16+
SessionNotification,
17+
WriteTextFileRequest,
18+
stdio_streams,
19+
)
20+
21+
22+
class MinimalClient(Client):
23+
async def writeTextFile(self, params: WriteTextFileRequest) -> None:
24+
print(f"write {params.path}", file=sys.stderr)
25+
26+
async def readTextFile(self, params: ReadTextFileRequest) -> ReadTextFileResponse:
27+
return ReadTextFileResponse(content="example")
28+
29+
async def requestPermission(self, params: RequestPermissionRequest) -> RequestPermissionResponse:
30+
return RequestPermissionResponse.model_validate({"outcome": {"outcome": "selected", "optionId": "allow"}})
31+
32+
async def sessionUpdate(self, params: SessionNotification) -> None:
33+
print(f"session update: {params}", file=sys.stderr)
34+
35+
# Optional terminal methods (not implemented in this minimal client)
36+
async def createTerminal(self, params) -> None:
37+
pass
38+
39+
async def terminalOutput(self, params) -> None:
40+
pass
41+
42+
async def releaseTerminal(self, params) -> None:
43+
pass
44+
45+
async def waitForTerminalExit(self, params) -> None:
46+
pass
47+
48+
async def killTerminal(self, params) -> None:
49+
pass
50+
51+
52+
async def main() -> None:
53+
reader, writer = await stdio_streams()
54+
client_conn = ClientSideConnection(lambda _agent: MinimalClient(), writer, reader)
55+
# 1) initialize
56+
resp = await client_conn.initialize(InitializeRequest(protocolVersion=PROTOCOL_VERSION))
57+
print(f"Initialized with protocol version: {resp.protocolVersion}", file=sys.stderr)
58+
# 2) new session
59+
new_sess = await client_conn.newSession(NewSessionRequest(mcpServers=[], cwd=os.getcwd()))
60+
# 3) prompt
61+
await client_conn.prompt(
62+
PromptRequest(
63+
sessionId=new_sess.sessionId,
64+
prompt=[{"type": "text", "text": "Hello from client"}],
65+
)
66+
)
67+
# Small grace period to allow duplex messages to flush
68+
await asyncio.sleep(0.2)
69+
70+
71+
if __name__ == "__main__":
72+
asyncio.run(main())

examples/duet.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import asyncio
2+
import contextlib
3+
import json
4+
import os
5+
import signal
6+
import sys
7+
from pathlib import Path
8+
9+
10+
async def _relay(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, tag: str):
11+
try:
12+
while True:
13+
line = await reader.readline()
14+
if not line:
15+
break
16+
# Mirror to the other end unchanged
17+
writer.write(line)
18+
try:
19+
await writer.drain()
20+
except ConnectionError:
21+
break
22+
# Try to pretty-print the JSON-RPC message for visibility
23+
try:
24+
obj = json.loads(line.decode("utf-8", errors="replace"))
25+
pretty = json.dumps(obj, ensure_ascii=False, indent=2)
26+
print(f"[{tag}] {pretty}", file=sys.stderr)
27+
except Exception:
28+
# Non-JSON (shouldn't happen on the protocol stream)
29+
print(f"[{tag}] {line!r}", file=sys.stderr)
30+
finally:
31+
try:
32+
writer.close()
33+
await writer.wait_closed()
34+
except Exception:
35+
pass
36+
37+
38+
async def main() -> None:
39+
root = Path(__file__).resolve().parent
40+
agent_path = str(root / "agent.py")
41+
client_path = str(root / "client.py")
42+
43+
# Ensure PYTHONPATH includes project src for `from acp import ...`
44+
env = os.environ.copy()
45+
src_dir = str((root.parent / "src").resolve())
46+
env["PYTHONPATH"] = src_dir + os.pathsep + env.get("PYTHONPATH", "")
47+
48+
agent = await asyncio.create_subprocess_exec(
49+
sys.executable,
50+
agent_path,
51+
stdin=asyncio.subprocess.PIPE,
52+
stdout=asyncio.subprocess.PIPE,
53+
stderr=sys.stderr,
54+
env=env,
55+
)
56+
client = await asyncio.create_subprocess_exec(
57+
sys.executable,
58+
client_path,
59+
stdin=asyncio.subprocess.PIPE,
60+
stdout=asyncio.subprocess.PIPE,
61+
stderr=sys.stderr,
62+
env=env,
63+
)
64+
65+
assert agent.stdout and agent.stdin and client.stdout and client.stdin
66+
67+
# Wire: agent.stdout -> client.stdin, client.stdout -> agent.stdin
68+
t1 = asyncio.create_task(_relay(agent.stdout, client.stdin, "agent→client"))
69+
t2 = asyncio.create_task(_relay(client.stdout, agent.stdin, "client→agent"))
70+
71+
# Handle shutdown
72+
stop = asyncio.Event()
73+
74+
def _on_sigint(*_):
75+
stop.set()
76+
77+
loop = asyncio.get_running_loop()
78+
try:
79+
loop.add_signal_handler(signal.SIGINT, _on_sigint)
80+
loop.add_signal_handler(signal.SIGTERM, _on_sigint)
81+
except NotImplementedError:
82+
pass
83+
84+
done, _ = await asyncio.wait(
85+
{
86+
t1,
87+
t2,
88+
asyncio.create_task(agent.wait()),
89+
asyncio.create_task(client.wait()),
90+
asyncio.create_task(stop.wait()),
91+
},
92+
return_when=asyncio.FIRST_COMPLETED,
93+
)
94+
95+
# Teardown
96+
for proc in (agent, client):
97+
if proc.returncode is None:
98+
with contextlib.suppress(ProcessLookupError):
99+
proc.terminate()
100+
try:
101+
await asyncio.wait_for(proc.wait(), 2)
102+
except asyncio.TimeoutError:
103+
with contextlib.suppress(ProcessLookupError):
104+
proc.kill()
105+
for task in (t1, t2):
106+
task.cancel()
107+
with contextlib.suppress(asyncio.CancelledError):
108+
await task
109+
110+
111+
if __name__ == "__main__":
112+
asyncio.run(main())

mkdocs.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ site_description: A Python implement of Agent Client Protocol (ACP, by Zed Indus
55
site_author: Chojan Shang
66
edit_uri: edit/main/docs/
77
repo_name: psiace/agent-client-protocol-python
8-
copyright: Maintained by <a href="https://psiace.com">psiace</a>.
8+
copyright: Maintained by <a href="https://github.com/psiace">psiace</a>.
99

1010
nav:
1111
- Home: index.md
12-
- Modules: modules.md
1312
plugins:
1413
- search
1514
- mkdocstrings:

pyproject.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ classifiers = [
1616
"Programming Language :: Python :: 3.13",
1717
"Topic :: Software Development :: Libraries :: Python Modules",
1818
]
19+
dependencies = [
20+
"pydantic>=2.7",
21+
]
22+
1923

2024
[project.urls]
2125
Homepage = "https://psiace.github.io/agent-client-protocol-python/"
@@ -24,7 +28,10 @@ Documentation = "https://psiace.github.io/agent-client-protocol-python/"
2428

2529
[dependency-groups]
2630
dev = [
31+
"datamodel-code-generator>=0.25",
32+
2733
"pytest>=7.2.0",
34+
"pytest-asyncio>=0.21.0",
2835
"pre-commit>=2.20.0",
2936
"tox-uv>=1.11.3",
3037
"deptry>=0.23.0",
@@ -97,6 +104,13 @@ ignore = [
97104

98105
[tool.ruff.lint.per-file-ignores]
99106
"tests/*" = ["S101"]
107+
"examples/*" = ["ALL"]
108+
"src/acp/meta.py" = ["ALL"]
109+
"src/acp/schema.py" = ["ALL"]
100110

101111
[tool.ruff.format]
102112
preview = true
113+
exclude = [
114+
"src/acp/meta.py",
115+
"src/acp/schema.py",
116+
]

schema/meta.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"agentMethods": {
3+
"authenticate": "authenticate",
4+
"initialize": "initialize",
5+
"session_cancel": "session/cancel",
6+
"session_load": "session/load",
7+
"session_new": "session/new",
8+
"session_prompt": "session/prompt"
9+
},
10+
"clientMethods": {
11+
"fs_read_text_file": "fs/read_text_file",
12+
"fs_write_text_file": "fs/write_text_file",
13+
"session_request_permission": "session/request_permission",
14+
"session_update": "session/update",
15+
"terminal_create": "terminal/create",
16+
"terminal_kill": "terminal/kill",
17+
"terminal_output": "terminal/output",
18+
"terminal_release": "terminal/release",
19+
"terminal_wait_for_exit": "terminal/wait_for_exit"
20+
},
21+
"version": 1
22+
}

0 commit comments

Comments
 (0)