agent-client-protocol 0.11.0__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/PKG-INFO +4 -1
- agent_client_protocol-0.12.0/examples/http_client.py +54 -0
- agent_client_protocol-0.12.0/examples/http_server.py +81 -0
- agent_client_protocol-0.12.0/examples/ws_client.py +53 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/pyproject.toml +9 -1
- agent_client_protocol-0.12.0/scripts/__init__.py +1 -0
- agent_client_protocol-0.12.0/scripts/gen_all.py +184 -0
- agent_client_protocol-0.12.0/scripts/gen_meta.py +43 -0
- agent_client_protocol-0.12.0/scripts/gen_schema.py +1096 -0
- agent_client_protocol-0.12.0/scripts/gen_signature.py +169 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/__init__.py +6 -0
- agent_client_protocol-0.12.0/src/acp/_cookies.py +53 -0
- agent_client_protocol-0.12.0/src/acp/_deserialize.py +43 -0
- agent_client_protocol-0.12.0/src/acp/_sse.py +84 -0
- agent_client_protocol-0.12.0/src/acp/_transport.py +151 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/agent/connection.py +12 -4
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/client/connection.py +12 -4
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/client/router.py +3 -5
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/connection.py +45 -45
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/core.py +13 -4
- agent_client_protocol-0.12.0/src/acp/http/__init__.py +30 -0
- agent_client_protocol-0.12.0/src/acp/http/asgi.py +175 -0
- agent_client_protocol-0.12.0/src/acp/http/client.py +238 -0
- agent_client_protocol-0.12.0/src/acp/http/protocol.py +106 -0
- agent_client_protocol-0.12.0/src/acp/http/server.py +461 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/meta.py +1 -1
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/schema.py +1065 -229
- agent_client_protocol-0.12.0/src/acp/ws/__init__.py +23 -0
- agent_client_protocol-0.12.0/src/acp/ws/client.py +117 -0
- agent_client_protocol-0.12.0/src/acp/ws/server.py +79 -0
- agent_client_protocol-0.12.0/tests/http/conftest.py +62 -0
- agent_client_protocol-0.12.0/tests/http/test_cookies.py +42 -0
- agent_client_protocol-0.12.0/tests/http/test_fixes.py +186 -0
- agent_client_protocol-0.12.0/tests/http/test_http_client.py +213 -0
- agent_client_protocol-0.12.0/tests/http/test_http_server.py +233 -0
- agent_client_protocol-0.12.0/tests/http/test_loopback.py +131 -0
- agent_client_protocol-0.12.0/tests/http/test_protocol.py +55 -0
- agent_client_protocol-0.12.0/tests/http/test_sse.py +63 -0
- agent_client_protocol-0.12.0/tests/http/test_websocket.py +182 -0
- agent_client_protocol-0.12.0/tests/http/test_ws_cookies.py +55 -0
- agent_client_protocol-0.12.0/tests/test_deserialize.py +62 -0
- agent_client_protocol-0.12.0/tests/test_elicitation_catchall.py +83 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_gen_all.py +86 -1
- agent_client_protocol-0.12.0/tests/test_request_error_logging.py +108 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_rpc.py +2 -2
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/LICENSE +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/README.md +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/agent.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/client.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/duet.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/echo_agent.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/gemini.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/agent/__init__.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/agent/router.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/client/__init__.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/contrib/__init__.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/contrib/permissions.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/contrib/session_state.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/contrib/tool_calls.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/exceptions.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/helpers.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/interfaces.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/py.typed +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/router.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/stdio.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/__init__.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/dispatcher.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/queue.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/sender.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/state.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/supervisor.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/telemetry.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/transports.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/utils.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/conftest.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/contrib/test_contrib_permissions.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/contrib/test_contrib_session_state.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/contrib/test_contrib_tool_calls.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/cancel_notification.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_audio.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_image.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_resource_blob.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_resource_link.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_resource_text.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_text.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/fs_read_text_file_request.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/fs_read_text_file_response.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/fs_write_text_file_request.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/initialize_request.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/initialize_response.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/new_session_request.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/new_session_response.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/permission_outcome_cancelled.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/permission_outcome_selected.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/prompt_request.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/request_permission_request.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/request_permission_response_selected.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_agent_message_chunk.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_agent_thought_chunk.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_config_option_update.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_plan.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_edit.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_locations_rawinput.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_read.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_update_content.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_update_more_fields.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_user_message_chunk.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/set_session_config_option_request.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/set_session_config_option_response.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/tool_content_content_text.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/tool_content_diff.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/tool_content_diff_no_old.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/tool_content_terminal.json +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/__init__.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/test_cancel_prompt_flow.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/test_mcp_servers_optional.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/test_permission_flow.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/test_stdio_limits.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_compatibility.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_connection_recovery.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_core.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_gemini_example.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_golden.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_unstable.py +0 -0
- {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: agent-client-protocol
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: A Python implement of Agent Client Protocol (ACP, by Zed Industries)
|
|
5
5
|
Keywords: python
|
|
6
6
|
Author-Email: Chojan Shang <psiace@apache.org>, Frost Ming <me@frostming.com>
|
|
@@ -21,6 +21,9 @@ Requires-Dist: pydantic>=2.7
|
|
|
21
21
|
Provides-Extra: logfire
|
|
22
22
|
Requires-Dist: logfire>=0.14; extra == "logfire"
|
|
23
23
|
Requires-Dist: opentelemetry-sdk>=1.28.0; extra == "logfire"
|
|
24
|
+
Provides-Extra: http
|
|
25
|
+
Requires-Dist: httpx[http2]>=0.27; extra == "http"
|
|
26
|
+
Requires-Dist: websockets>=12.0; extra == "http"
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
25
28
|
|
|
26
29
|
<a href="https://agentclientprotocol.com/">
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10,<3.15"
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "agent-client-protocol[http]",
|
|
5
|
+
# ]
|
|
6
|
+
# ///
|
|
7
|
+
"""Connect to a remote ACP agent over Streamable HTTP (experimental).
|
|
8
|
+
|
|
9
|
+
Start the server first (``uv run examples/http_server.py``), then run this.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from acp import connect_to_agent, text_block
|
|
16
|
+
from acp.http import create_http_stream
|
|
17
|
+
from acp.interfaces import Client
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExampleClient(Client):
|
|
21
|
+
async def request_permission(self, session_id: str, tool_call: Any, options: Any, **kwargs: Any) -> Any:
|
|
22
|
+
# Auto-allow the first option.
|
|
23
|
+
return {"outcome": {"outcome": "selected", "optionId": options[0]["optionId"]}}
|
|
24
|
+
|
|
25
|
+
async def session_update(self, session_id: str, update: Any, **kwargs: Any) -> None:
|
|
26
|
+
content = getattr(update, "content", None)
|
|
27
|
+
text = getattr(content, "text", None) if content is not None else None
|
|
28
|
+
if text:
|
|
29
|
+
print(f"<< {text}")
|
|
30
|
+
|
|
31
|
+
async def write_text_file(self, *args: Any, **kwargs: Any) -> None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
async def read_text_file(self, *args: Any, **kwargs: Any) -> Any:
|
|
35
|
+
return {"content": ""}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def main() -> None:
|
|
39
|
+
transport = create_http_stream("http://localhost:8000/acp")
|
|
40
|
+
conn = connect_to_agent(ExampleClient(), transport)
|
|
41
|
+
try:
|
|
42
|
+
init = await conn.initialize(protocol_version=1)
|
|
43
|
+
print(f"initialized (protocol v{init.protocol_version})")
|
|
44
|
+
session = await conn.new_session(cwd=".", mcp_servers=[])
|
|
45
|
+
print(f"session: {session.session_id}")
|
|
46
|
+
result = await conn.prompt(session_id=session.session_id, prompt=[text_block("hello over http")])
|
|
47
|
+
print(f"stop reason: {result.stop_reason}")
|
|
48
|
+
finally:
|
|
49
|
+
await conn.close()
|
|
50
|
+
await transport.close()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10,<3.15"
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "agent-client-protocol[http]",
|
|
5
|
+
# "hypercorn>=0.17",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
"""Serve an ACP agent over Streamable HTTP + WebSocket (experimental).
|
|
9
|
+
|
|
10
|
+
Run with an HTTP/2-capable ASGI server for spec-compliant Streamable HTTP. This
|
|
11
|
+
example uses Hypercorn; Uvicorn works for WebSocket but does not serve HTTP/2.
|
|
12
|
+
|
|
13
|
+
uv run examples/http_server.py
|
|
14
|
+
# then, in another terminal:
|
|
15
|
+
uv run examples/http_client.py
|
|
16
|
+
uv run examples/ws_client.py
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from typing import Any
|
|
21
|
+
from uuid import uuid4
|
|
22
|
+
|
|
23
|
+
from acp import (
|
|
24
|
+
Agent,
|
|
25
|
+
InitializeResponse,
|
|
26
|
+
NewSessionResponse,
|
|
27
|
+
PromptResponse,
|
|
28
|
+
text_block,
|
|
29
|
+
update_agent_message,
|
|
30
|
+
)
|
|
31
|
+
from acp.http.asgi import create_asgi_app
|
|
32
|
+
from acp.interfaces import Client
|
|
33
|
+
from acp.schema import ClientCapabilities, Implementation
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EchoAgent(Agent):
|
|
37
|
+
_conn: Client
|
|
38
|
+
|
|
39
|
+
def on_connect(self, conn: Client) -> None:
|
|
40
|
+
self._conn = conn
|
|
41
|
+
|
|
42
|
+
async def initialize(
|
|
43
|
+
self,
|
|
44
|
+
protocol_version: int,
|
|
45
|
+
client_capabilities: ClientCapabilities | None = None,
|
|
46
|
+
client_info: Implementation | None = None,
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> InitializeResponse:
|
|
49
|
+
return InitializeResponse(protocol_version=protocol_version)
|
|
50
|
+
|
|
51
|
+
async def new_session(self, cwd: str = "", **kwargs: Any) -> NewSessionResponse:
|
|
52
|
+
return NewSessionResponse(session_id=uuid4().hex)
|
|
53
|
+
|
|
54
|
+
async def prompt(self, session_id: str, prompt: list[Any], **kwargs: Any) -> PromptResponse:
|
|
55
|
+
for block in prompt:
|
|
56
|
+
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
|
|
57
|
+
await self._conn.session_update(
|
|
58
|
+
session_id=session_id,
|
|
59
|
+
update=update_agent_message(text_block(f"echo: {text}")),
|
|
60
|
+
)
|
|
61
|
+
return PromptResponse(stop_reason="end_turn")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# One agent instance per connection.
|
|
65
|
+
app = create_asgi_app(lambda conn: EchoAgent())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def main() -> None:
|
|
69
|
+
import hypercorn.asyncio
|
|
70
|
+
from hypercorn.config import Config
|
|
71
|
+
|
|
72
|
+
config = Config()
|
|
73
|
+
config.bind = ["localhost:8000"]
|
|
74
|
+
# Enable HTTP/2 (Streamable HTTP requires it). Hypercorn negotiates h2c/h2.
|
|
75
|
+
config.alpn_protocols = ["h2", "http/1.1"]
|
|
76
|
+
print("Serving ACP agent on http://localhost:8000/acp (HTTP + WS)")
|
|
77
|
+
await hypercorn.asyncio.serve(app, config)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10,<3.15"
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "agent-client-protocol[http]",
|
|
5
|
+
# ]
|
|
6
|
+
# ///
|
|
7
|
+
"""Connect to a remote ACP agent over WebSocket (experimental).
|
|
8
|
+
|
|
9
|
+
Start the server first (``uv run examples/http_server.py``), then run this.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from acp import connect_to_agent, text_block
|
|
16
|
+
from acp.interfaces import Client
|
|
17
|
+
from acp.ws import create_websocket_stream
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExampleClient(Client):
|
|
21
|
+
async def request_permission(self, session_id: str, tool_call: Any, options: Any, **kwargs: Any) -> Any:
|
|
22
|
+
return {"outcome": {"outcome": "selected", "optionId": options[0]["optionId"]}}
|
|
23
|
+
|
|
24
|
+
async def session_update(self, session_id: str, update: Any, **kwargs: Any) -> None:
|
|
25
|
+
content = getattr(update, "content", None)
|
|
26
|
+
text = getattr(content, "text", None) if content is not None else None
|
|
27
|
+
if text:
|
|
28
|
+
print(f"<< {text}")
|
|
29
|
+
|
|
30
|
+
async def write_text_file(self, *args: Any, **kwargs: Any) -> None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
async def read_text_file(self, *args: Any, **kwargs: Any) -> Any:
|
|
34
|
+
return {"content": ""}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def main() -> None:
|
|
38
|
+
transport = await create_websocket_stream("ws://localhost:8000/acp")
|
|
39
|
+
conn = connect_to_agent(ExampleClient(), transport)
|
|
40
|
+
try:
|
|
41
|
+
init = await conn.initialize(protocol_version=1)
|
|
42
|
+
print(f"initialized (protocol v{init.protocol_version})")
|
|
43
|
+
session = await conn.new_session(cwd=".", mcp_servers=[])
|
|
44
|
+
print(f"session: {session.session_id}")
|
|
45
|
+
result = await conn.prompt(session_id=session.session_id, prompt=[text_block("hello over websocket")])
|
|
46
|
+
print(f"stop reason: {result.stop_reason}")
|
|
47
|
+
finally:
|
|
48
|
+
await conn.close()
|
|
49
|
+
await transport.close()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
asyncio.run(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-client-protocol"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.12.0"
|
|
4
4
|
description = "A Python implement of Agent Client Protocol (ACP, by Zed Industries)"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Chojan Shang", email = "psiace@apache.org" },
|
|
@@ -36,6 +36,10 @@ logfire = [
|
|
|
36
36
|
"logfire>=0.14",
|
|
37
37
|
"opentelemetry-sdk>=1.28.0",
|
|
38
38
|
]
|
|
39
|
+
http = [
|
|
40
|
+
"httpx[http2]>=0.27",
|
|
41
|
+
"websockets>=12.0",
|
|
42
|
+
]
|
|
39
43
|
|
|
40
44
|
[dependency-groups]
|
|
41
45
|
dev = [
|
|
@@ -51,6 +55,9 @@ dev = [
|
|
|
51
55
|
"mkdocstrings[python]>=0.26.1",
|
|
52
56
|
"python-dotenv>=1.1.1",
|
|
53
57
|
"prek>=0.2.17",
|
|
58
|
+
"httpx[http2]>=0.27",
|
|
59
|
+
"websockets>=12.0",
|
|
60
|
+
"uvicorn>=0.30",
|
|
54
61
|
]
|
|
55
62
|
|
|
56
63
|
[build-system]
|
|
@@ -63,6 +70,7 @@ build-backend = "pdm.backend"
|
|
|
63
70
|
source-includes = [
|
|
64
71
|
"examples/",
|
|
65
72
|
"tests/",
|
|
73
|
+
"scripts/",
|
|
66
74
|
]
|
|
67
75
|
|
|
68
76
|
[tool.ty.environment]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility scripts for generating ACP bindings."""
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
15
|
+
if str(ROOT) not in sys.path:
|
|
16
|
+
sys.path.append(str(ROOT))
|
|
17
|
+
|
|
18
|
+
from scripts import gen_meta, gen_schema, gen_signature # noqa: E402 pylint: disable=wrong-import-position
|
|
19
|
+
|
|
20
|
+
SCHEMA_DIR = ROOT / "schema"
|
|
21
|
+
SCHEMA_JSON = SCHEMA_DIR / "schema.json"
|
|
22
|
+
META_JSON = SCHEMA_DIR / "meta.json"
|
|
23
|
+
VERSION_FILE = SCHEMA_DIR / "VERSION"
|
|
24
|
+
|
|
25
|
+
DEFAULT_REPO = "agentclientprotocol/agent-client-protocol"
|
|
26
|
+
LEGACY_SCHEMA_PATHS = ("schema/schema.unstable.json", "schema/meta.unstable.json")
|
|
27
|
+
V1_SCHEMA_PATHS = ("schema/v1/schema.unstable.json", "schema/v1/meta.unstable.json")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_args() -> argparse.Namespace:
|
|
31
|
+
parser = argparse.ArgumentParser(description="Regenerate schema.py and meta.py from the ACP schema.")
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--version",
|
|
34
|
+
"-v",
|
|
35
|
+
help=(
|
|
36
|
+
"Git ref (tag/branch) of agentclientprotocol/agent-client-protocol to fetch the schema from. "
|
|
37
|
+
"If omitted, uses the cached schema files or falls back to 'main' when missing."
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--repo",
|
|
42
|
+
default=os.environ.get("ACP_SCHEMA_REPO", DEFAULT_REPO),
|
|
43
|
+
help="Source repository providing schema.json/meta.json (default: %(default)s)",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--no-download",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Skip downloading schema files even when a version is provided.",
|
|
49
|
+
)
|
|
50
|
+
parser.set_defaults(format_output=True)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--no-format",
|
|
53
|
+
dest="format_output",
|
|
54
|
+
action="store_false",
|
|
55
|
+
help="Skip formatting generated Python files after regeneration.",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--force",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Force schema download even if the requested ref is already cached locally.",
|
|
61
|
+
)
|
|
62
|
+
return parser.parse_args()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> None:
|
|
66
|
+
args = parse_args()
|
|
67
|
+
|
|
68
|
+
version = args.version or os.environ.get("ACP_SCHEMA_VERSION")
|
|
69
|
+
repo = args.repo
|
|
70
|
+
should_download = _should_download(args, version)
|
|
71
|
+
|
|
72
|
+
if should_download:
|
|
73
|
+
ref = resolve_ref(version)
|
|
74
|
+
download_schema(repo, ref)
|
|
75
|
+
else:
|
|
76
|
+
ref = resolve_ref(version) if version else _cached_ref()
|
|
77
|
+
|
|
78
|
+
if not (SCHEMA_JSON.exists() and META_JSON.exists()):
|
|
79
|
+
print("schema/schema.json or schema/meta.json missing; run with --version to fetch them.", file=sys.stderr)
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
gen_schema.generate_schema()
|
|
83
|
+
gen_meta.generate_meta()
|
|
84
|
+
gen_signature.gen_signature(ROOT / "src" / "acp")
|
|
85
|
+
if args.format_output:
|
|
86
|
+
format_generated_files()
|
|
87
|
+
|
|
88
|
+
if ref:
|
|
89
|
+
print(f"Generated schema using ref: {ref}")
|
|
90
|
+
else:
|
|
91
|
+
print("Generated schema using local schema files")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_generated_files() -> None:
|
|
95
|
+
files = [
|
|
96
|
+
ROOT / "src" / "acp" / "schema.py",
|
|
97
|
+
ROOT / "src" / "acp" / "meta.py",
|
|
98
|
+
ROOT / "src" / "acp" / "interfaces.py",
|
|
99
|
+
ROOT / "src" / "acp" / "agent" / "connection.py",
|
|
100
|
+
ROOT / "src" / "acp" / "client" / "connection.py",
|
|
101
|
+
]
|
|
102
|
+
subprocess.check_call([sys.executable, "-m", "ruff", "check", "--fix", *(str(path) for path in files)]) # noqa: S603
|
|
103
|
+
subprocess.check_call([sys.executable, "-m", "ruff", "format", *(str(path) for path in files)]) # noqa: S603
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _should_download(args: argparse.Namespace, version: str | None) -> bool:
|
|
107
|
+
env_override = os.environ.get("ACP_SCHEMA_DOWNLOAD")
|
|
108
|
+
if env_override is not None:
|
|
109
|
+
return env_override.lower() in {"1", "true", "yes"}
|
|
110
|
+
if args.no_download:
|
|
111
|
+
return False
|
|
112
|
+
if version:
|
|
113
|
+
if not SCHEMA_JSON.exists() or not META_JSON.exists():
|
|
114
|
+
return True
|
|
115
|
+
cached = _cached_ref()
|
|
116
|
+
if args.force:
|
|
117
|
+
return True
|
|
118
|
+
return cached != resolve_ref(version)
|
|
119
|
+
return not (SCHEMA_JSON.exists() and META_JSON.exists())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_ref(version: str | None) -> str:
|
|
123
|
+
if not version:
|
|
124
|
+
return "refs/heads/main"
|
|
125
|
+
if version.startswith("refs/"):
|
|
126
|
+
return version
|
|
127
|
+
if re.fullmatch(r"schema-v\d+\.\d+\.\d+", version):
|
|
128
|
+
return f"refs/tags/{version}"
|
|
129
|
+
if re.fullmatch(r"v?\d+\.\d+\.\d+", version):
|
|
130
|
+
value = version if version.startswith("v") else f"v{version}"
|
|
131
|
+
return f"refs/tags/{value}"
|
|
132
|
+
return f"refs/heads/{version}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def download_schema(repo: str, ref: str) -> None:
|
|
136
|
+
SCHEMA_DIR.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
try:
|
|
138
|
+
schema_data, meta_data = fetch_schema_pair(repo, ref)
|
|
139
|
+
except RuntimeError as exc: # pragma: no cover - network error path
|
|
140
|
+
print(exc, file=sys.stderr)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
SCHEMA_JSON.write_text(json.dumps(schema_data, indent=2) + "\n", encoding="utf-8")
|
|
144
|
+
META_JSON.write_text(json.dumps(meta_data, indent=2) + "\n", encoding="utf-8")
|
|
145
|
+
VERSION_FILE.write_text(ref + "\n", encoding="utf-8")
|
|
146
|
+
print(f"Fetched schema and meta from {repo}@{ref}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def fetch_schema_pair(repo: str, ref: str) -> tuple[dict, dict]:
|
|
150
|
+
errors = []
|
|
151
|
+
for schema_path, meta_path in schema_source_paths(ref):
|
|
152
|
+
schema_url = f"https://raw.githubusercontent.com/{repo}/{ref}/{schema_path}"
|
|
153
|
+
meta_url = f"https://raw.githubusercontent.com/{repo}/{ref}/{meta_path}"
|
|
154
|
+
try:
|
|
155
|
+
return fetch_json(schema_url), fetch_json(meta_url)
|
|
156
|
+
except RuntimeError as exc:
|
|
157
|
+
errors.append(str(exc))
|
|
158
|
+
|
|
159
|
+
attempted = "\n".join(f"- {error}" for error in errors)
|
|
160
|
+
raise RuntimeError(f"Failed to fetch schema and meta from {repo}@{ref}. Attempts:\n{attempted}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def schema_source_paths(ref: str) -> tuple[tuple[str, str], ...]:
|
|
164
|
+
if re.fullmatch(r"refs/tags/schema-v\d+\.\d+\.\d+", ref):
|
|
165
|
+
return (V1_SCHEMA_PATHS, LEGACY_SCHEMA_PATHS)
|
|
166
|
+
return (LEGACY_SCHEMA_PATHS, V1_SCHEMA_PATHS)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fetch_json(url: str) -> dict:
|
|
170
|
+
try:
|
|
171
|
+
with urllib.request.urlopen(url) as response: # noqa: S310 - trusted source configured by repo
|
|
172
|
+
return json.loads(response.read().decode("utf-8"))
|
|
173
|
+
except urllib.error.URLError as exc:
|
|
174
|
+
raise RuntimeError(f"Failed to fetch {url}: {exc}") from exc
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _cached_ref() -> str | None:
|
|
178
|
+
if VERSION_FILE.exists():
|
|
179
|
+
return VERSION_FILE.read_text(encoding="utf-8").strip() or None
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
8
|
+
SCHEMA_DIR = ROOT / "schema"
|
|
9
|
+
VERSION_FILE = SCHEMA_DIR / "VERSION"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
generate_meta()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_meta() -> None:
|
|
17
|
+
meta_json = SCHEMA_DIR / "meta.json"
|
|
18
|
+
out_py = ROOT / "src" / "acp" / "meta.py"
|
|
19
|
+
if not meta_json.exists():
|
|
20
|
+
raise SystemExit("schema/meta.json not found. Run gen_schema.py first.")
|
|
21
|
+
|
|
22
|
+
data = json.loads(meta_json.read_text("utf-8"))
|
|
23
|
+
agent_methods = data.get("agentMethods", {})
|
|
24
|
+
client_methods = data.get("clientMethods", {})
|
|
25
|
+
version = data.get("version", 1)
|
|
26
|
+
header_lines = ["# Generated from schema/meta.json. Do not edit by hand."]
|
|
27
|
+
if VERSION_FILE.exists():
|
|
28
|
+
ref = VERSION_FILE.read_text("utf-8").strip()
|
|
29
|
+
if ref:
|
|
30
|
+
header_lines.append(f"# Schema ref: {ref}")
|
|
31
|
+
|
|
32
|
+
out_py.write_text(
|
|
33
|
+
"\n".join(header_lines)
|
|
34
|
+
+ "\n"
|
|
35
|
+
+ f"AGENT_METHODS = {agent_methods!r}\n"
|
|
36
|
+
+ f"CLIENT_METHODS = {client_methods!r}\n"
|
|
37
|
+
+ f"PROTOCOL_VERSION = {int(version)}\n",
|
|
38
|
+
encoding="utf-8",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|