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.
Files changed (126) hide show
  1. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/PKG-INFO +4 -1
  2. agent_client_protocol-0.12.0/examples/http_client.py +54 -0
  3. agent_client_protocol-0.12.0/examples/http_server.py +81 -0
  4. agent_client_protocol-0.12.0/examples/ws_client.py +53 -0
  5. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/pyproject.toml +9 -1
  6. agent_client_protocol-0.12.0/scripts/__init__.py +1 -0
  7. agent_client_protocol-0.12.0/scripts/gen_all.py +184 -0
  8. agent_client_protocol-0.12.0/scripts/gen_meta.py +43 -0
  9. agent_client_protocol-0.12.0/scripts/gen_schema.py +1096 -0
  10. agent_client_protocol-0.12.0/scripts/gen_signature.py +169 -0
  11. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/__init__.py +6 -0
  12. agent_client_protocol-0.12.0/src/acp/_cookies.py +53 -0
  13. agent_client_protocol-0.12.0/src/acp/_deserialize.py +43 -0
  14. agent_client_protocol-0.12.0/src/acp/_sse.py +84 -0
  15. agent_client_protocol-0.12.0/src/acp/_transport.py +151 -0
  16. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/agent/connection.py +12 -4
  17. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/client/connection.py +12 -4
  18. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/client/router.py +3 -5
  19. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/connection.py +45 -45
  20. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/core.py +13 -4
  21. agent_client_protocol-0.12.0/src/acp/http/__init__.py +30 -0
  22. agent_client_protocol-0.12.0/src/acp/http/asgi.py +175 -0
  23. agent_client_protocol-0.12.0/src/acp/http/client.py +238 -0
  24. agent_client_protocol-0.12.0/src/acp/http/protocol.py +106 -0
  25. agent_client_protocol-0.12.0/src/acp/http/server.py +461 -0
  26. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/meta.py +1 -1
  27. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/schema.py +1065 -229
  28. agent_client_protocol-0.12.0/src/acp/ws/__init__.py +23 -0
  29. agent_client_protocol-0.12.0/src/acp/ws/client.py +117 -0
  30. agent_client_protocol-0.12.0/src/acp/ws/server.py +79 -0
  31. agent_client_protocol-0.12.0/tests/http/conftest.py +62 -0
  32. agent_client_protocol-0.12.0/tests/http/test_cookies.py +42 -0
  33. agent_client_protocol-0.12.0/tests/http/test_fixes.py +186 -0
  34. agent_client_protocol-0.12.0/tests/http/test_http_client.py +213 -0
  35. agent_client_protocol-0.12.0/tests/http/test_http_server.py +233 -0
  36. agent_client_protocol-0.12.0/tests/http/test_loopback.py +131 -0
  37. agent_client_protocol-0.12.0/tests/http/test_protocol.py +55 -0
  38. agent_client_protocol-0.12.0/tests/http/test_sse.py +63 -0
  39. agent_client_protocol-0.12.0/tests/http/test_websocket.py +182 -0
  40. agent_client_protocol-0.12.0/tests/http/test_ws_cookies.py +55 -0
  41. agent_client_protocol-0.12.0/tests/test_deserialize.py +62 -0
  42. agent_client_protocol-0.12.0/tests/test_elicitation_catchall.py +83 -0
  43. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_gen_all.py +86 -1
  44. agent_client_protocol-0.12.0/tests/test_request_error_logging.py +108 -0
  45. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_rpc.py +2 -2
  46. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/LICENSE +0 -0
  47. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/README.md +0 -0
  48. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/agent.py +0 -0
  49. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/client.py +0 -0
  50. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/duet.py +0 -0
  51. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/echo_agent.py +0 -0
  52. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/examples/gemini.py +0 -0
  53. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/agent/__init__.py +0 -0
  54. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/agent/router.py +0 -0
  55. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/client/__init__.py +0 -0
  56. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/contrib/__init__.py +0 -0
  57. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/contrib/permissions.py +0 -0
  58. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/contrib/session_state.py +0 -0
  59. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/contrib/tool_calls.py +0 -0
  60. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/exceptions.py +0 -0
  61. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/helpers.py +0 -0
  62. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/interfaces.py +0 -0
  63. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/py.typed +0 -0
  64. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/router.py +0 -0
  65. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/stdio.py +0 -0
  66. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/__init__.py +0 -0
  67. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/dispatcher.py +0 -0
  68. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/queue.py +0 -0
  69. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/sender.py +0 -0
  70. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/state.py +0 -0
  71. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/task/supervisor.py +0 -0
  72. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/telemetry.py +0 -0
  73. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/transports.py +0 -0
  74. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/src/acp/utils.py +0 -0
  75. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/conftest.py +0 -0
  76. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/contrib/test_contrib_permissions.py +0 -0
  77. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/contrib/test_contrib_session_state.py +0 -0
  78. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/contrib/test_contrib_tool_calls.py +0 -0
  79. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/cancel_notification.json +0 -0
  80. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_audio.json +0 -0
  81. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_image.json +0 -0
  82. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_resource_blob.json +0 -0
  83. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_resource_link.json +0 -0
  84. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_resource_text.json +0 -0
  85. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/content_text.json +0 -0
  86. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/fs_read_text_file_request.json +0 -0
  87. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/fs_read_text_file_response.json +0 -0
  88. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/fs_write_text_file_request.json +0 -0
  89. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/initialize_request.json +0 -0
  90. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/initialize_response.json +0 -0
  91. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/new_session_request.json +0 -0
  92. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/new_session_response.json +0 -0
  93. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/permission_outcome_cancelled.json +0 -0
  94. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/permission_outcome_selected.json +0 -0
  95. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/prompt_request.json +0 -0
  96. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/request_permission_request.json +0 -0
  97. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/request_permission_response_selected.json +0 -0
  98. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_agent_message_chunk.json +0 -0
  99. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_agent_thought_chunk.json +0 -0
  100. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_config_option_update.json +0 -0
  101. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_plan.json +0 -0
  102. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call.json +0 -0
  103. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_edit.json +0 -0
  104. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_locations_rawinput.json +0 -0
  105. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_read.json +0 -0
  106. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_update_content.json +0 -0
  107. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_tool_call_update_more_fields.json +0 -0
  108. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/session_update_user_message_chunk.json +0 -0
  109. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/set_session_config_option_request.json +0 -0
  110. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/set_session_config_option_response.json +0 -0
  111. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/tool_content_content_text.json +0 -0
  112. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/tool_content_diff.json +0 -0
  113. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/tool_content_diff_no_old.json +0 -0
  114. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/golden/tool_content_terminal.json +0 -0
  115. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/__init__.py +0 -0
  116. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/test_cancel_prompt_flow.py +0 -0
  117. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/test_mcp_servers_optional.py +0 -0
  118. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/test_permission_flow.py +0 -0
  119. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/real_user/test_stdio_limits.py +0 -0
  120. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_compatibility.py +0 -0
  121. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_connection_recovery.py +0 -0
  122. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_core.py +0 -0
  123. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_gemini_example.py +0 -0
  124. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_golden.py +0 -0
  125. {agent_client_protocol-0.11.0 → agent_client_protocol-0.12.0}/tests/test_unstable.py +0 -0
  126. {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.11.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.11.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()