acp-sdk 0.1.0rc7__tar.gz → 0.2.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 (51) hide show
  1. acp_sdk-0.2.0/PKG-INFO +113 -0
  2. acp_sdk-0.2.0/README.md +91 -0
  3. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/examples/clients/advanced.py +3 -3
  4. acp_sdk-0.2.0/examples/clients/session.py +18 -0
  5. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/examples/clients/simple.py +5 -3
  6. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/examples/clients/stream.py +4 -2
  7. acp_sdk-0.2.0/examples/servers/awaiting.py +25 -0
  8. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/examples/servers/echo.py +4 -4
  9. acp_sdk-0.2.0/examples/servers/standalone.py +25 -0
  10. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/pyproject.toml +4 -2
  11. acp_sdk-0.2.0/pytest.ini +5 -0
  12. acp_sdk-0.2.0/src/acp_sdk/__init__.py +2 -0
  13. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/client/client.py +53 -14
  14. acp_sdk-0.2.0/src/acp_sdk/models/models.py +170 -0
  15. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/models/schemas.py +1 -1
  16. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/__init__.py +1 -0
  17. acp_sdk-0.2.0/src/acp_sdk/server/agent.py +178 -0
  18. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/app.py +17 -15
  19. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/bundle.py +62 -33
  20. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/server.py +11 -88
  21. acp_sdk-0.2.0/src/acp_sdk/server/session.py +21 -0
  22. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/telemetry.py +7 -2
  23. acp_sdk-0.2.0/src/acp_sdk/server/types.py +6 -0
  24. acp_sdk-0.2.0/src/acp_sdk/version.py +3 -0
  25. acp_sdk-0.2.0/tests/conftest.py +4 -0
  26. acp_sdk-0.2.0/tests/e2e/config.py +2 -0
  27. acp_sdk-0.2.0/tests/e2e/fixtures/__init__.py +0 -0
  28. acp_sdk-0.2.0/tests/e2e/fixtures/client.py +12 -0
  29. acp_sdk-0.2.0/tests/e2e/fixtures/server.py +83 -0
  30. acp_sdk-0.2.0/tests/e2e/test_suites/__init__.py +0 -0
  31. acp_sdk-0.2.0/tests/e2e/test_suites/test_runs.py +191 -0
  32. acp_sdk-0.1.0rc7/PKG-INFO +0 -74
  33. acp_sdk-0.1.0rc7/README.md +0 -55
  34. acp_sdk-0.1.0rc7/examples/servers/awaiting.py +0 -23
  35. acp_sdk-0.1.0rc7/examples/servers/multi-echo.py +0 -57
  36. acp_sdk-0.1.0rc7/src/acp_sdk/__init__.py +0 -1
  37. acp_sdk-0.1.0rc7/src/acp_sdk/models/models.py +0 -181
  38. acp_sdk-0.1.0rc7/src/acp_sdk/server/agent.py +0 -105
  39. acp_sdk-0.1.0rc7/src/acp_sdk/server/types.py +0 -6
  40. acp_sdk-0.1.0rc7/tests/test_e2e.py +0 -113
  41. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/.gitignore +0 -0
  42. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/.python-version +0 -0
  43. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/client/__init__.py +0 -0
  44. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/models/__init__.py +0 -0
  45. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/models/errors.py +0 -0
  46. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/py.typed +0 -0
  47. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/context.py +0 -0
  48. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/errors.py +0 -0
  49. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/logging.py +0 -0
  50. {acp_sdk-0.1.0rc7 → acp_sdk-0.2.0}/src/acp_sdk/server/utils.py +0 -0
  51. {acp_sdk-0.1.0rc7/tests → acp_sdk-0.2.0/tests/e2e}/__init__.py +0 -0
acp_sdk-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: acp-sdk
3
+ Version: 0.2.0
4
+ Summary: Agent Communication Protocol SDK
5
+ Author: IBM Corp.
6
+ Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: <4.0,>=3.11
9
+ Requires-Dist: opentelemetry-api>=1.31.1
10
+ Requires-Dist: pydantic>=2.11.1
11
+ Provides-Extra: client
12
+ Requires-Dist: httpx-sse>=0.4.0; extra == 'client'
13
+ Requires-Dist: httpx>=0.28.1; extra == 'client'
14
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.52b1; extra == 'client'
15
+ Provides-Extra: server
16
+ Requires-Dist: fastapi[standard]>=0.115.8; extra == 'server'
17
+ Requires-Dist: janus>=2.0.0; extra == 'server'
18
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31.1; extra == 'server'
19
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.52b1; extra == 'server'
20
+ Requires-Dist: opentelemetry-sdk>=1.31.1; extra == 'server'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Agent Communication Protocol SDK for Python
24
+
25
+ Agent Communication Protocol SDK for Python provides allows developers to serve and consume agents over the Agent Communication Protocol.
26
+
27
+ ## Prerequisites
28
+
29
+ ✅ Python >= 3.11
30
+
31
+ ## Installation
32
+
33
+ Install to use client:
34
+
35
+ ```shell
36
+ pip install acp-sdk[client]
37
+ ```
38
+
39
+ Install to use server:
40
+
41
+ ```shell
42
+ pip install acp-sdk[server]
43
+ ```
44
+
45
+ Install to use models only:
46
+
47
+ ```shell
48
+ pip install acp-sdk
49
+ ```
50
+
51
+ ## Overview
52
+
53
+ ### Core
54
+
55
+ The core of the SDK exposes [pydantic](https://docs.pydantic.dev/) data models corresponding to REST API requests, responses, resources, events and errors.
56
+
57
+
58
+ ### Client
59
+
60
+ The `client` submodule exposes [httpx](https://www.python-httpx.org/) based client with simple methods for communication over ACP.
61
+
62
+ ```python
63
+ async with Client(base_url="http://localhost:8000") as client:
64
+ run = await client.run_sync(agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!")])])
65
+ print(run)
66
+
67
+ ```
68
+
69
+ ### Server
70
+
71
+ The `server` submodule exposes `Agent` class and `agent` decorator together with [fastapi](https://fastapi.tiangolo.com/) application factory, making it easy to expose agents over ACP. Additionaly, it exposes [uvicorn](https://www.uvicorn.org/) based server to serve agents with set up logging, [opentelemetry](https://opentelemetry.io/) and more.
72
+
73
+ ```python
74
+ server = Server()
75
+
76
+ @server.agent()
77
+ async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
78
+ """Echoes everything"""
79
+ for message in inputs:
80
+ yield {"thought": "I should echo everyting"}
81
+ await asyncio.sleep(0.5)
82
+ yield message
83
+
84
+
85
+ server.run()
86
+ ```
87
+
88
+ ➡️ Explore more in our [examples library](/python/examples).
89
+
90
+ ## Architecture
91
+
92
+ The architecture of the SDK is outlined in the following segment. It focuses on central parts of the SDK without going into much detail.
93
+
94
+ ### Models
95
+
96
+ The core of the SDK contains pydantic models for requests, responses, resources, events and errors. Users of the SDK are meant to use these models directly or indirectly.
97
+
98
+ ### Server
99
+
100
+ The server module consists of 3 parts:
101
+
102
+ 1. Agent interface
103
+ 2. FastAPI application factory
104
+ 3. Uvicorn based server
105
+
106
+ Each part builds on top of the previous one. Not all parts need to be used, e.g. users are advised to bring their own ASGI server for production deployments.
107
+
108
+ ### Client
109
+
110
+ The client module consists of httpx based client with session support. The client is meant to be thin and mimic the REST API. Exception is session management which has been abstracted into a context manager.
111
+
112
+
113
+
@@ -0,0 +1,91 @@
1
+ # Agent Communication Protocol SDK for Python
2
+
3
+ Agent Communication Protocol SDK for Python provides allows developers to serve and consume agents over the Agent Communication Protocol.
4
+
5
+ ## Prerequisites
6
+
7
+ ✅ Python >= 3.11
8
+
9
+ ## Installation
10
+
11
+ Install to use client:
12
+
13
+ ```shell
14
+ pip install acp-sdk[client]
15
+ ```
16
+
17
+ Install to use server:
18
+
19
+ ```shell
20
+ pip install acp-sdk[server]
21
+ ```
22
+
23
+ Install to use models only:
24
+
25
+ ```shell
26
+ pip install acp-sdk
27
+ ```
28
+
29
+ ## Overview
30
+
31
+ ### Core
32
+
33
+ The core of the SDK exposes [pydantic](https://docs.pydantic.dev/) data models corresponding to REST API requests, responses, resources, events and errors.
34
+
35
+
36
+ ### Client
37
+
38
+ The `client` submodule exposes [httpx](https://www.python-httpx.org/) based client with simple methods for communication over ACP.
39
+
40
+ ```python
41
+ async with Client(base_url="http://localhost:8000") as client:
42
+ run = await client.run_sync(agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!")])])
43
+ print(run)
44
+
45
+ ```
46
+
47
+ ### Server
48
+
49
+ The `server` submodule exposes `Agent` class and `agent` decorator together with [fastapi](https://fastapi.tiangolo.com/) application factory, making it easy to expose agents over ACP. Additionaly, it exposes [uvicorn](https://www.uvicorn.org/) based server to serve agents with set up logging, [opentelemetry](https://opentelemetry.io/) and more.
50
+
51
+ ```python
52
+ server = Server()
53
+
54
+ @server.agent()
55
+ async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
56
+ """Echoes everything"""
57
+ for message in inputs:
58
+ yield {"thought": "I should echo everyting"}
59
+ await asyncio.sleep(0.5)
60
+ yield message
61
+
62
+
63
+ server.run()
64
+ ```
65
+
66
+ ➡️ Explore more in our [examples library](/python/examples).
67
+
68
+ ## Architecture
69
+
70
+ The architecture of the SDK is outlined in the following segment. It focuses on central parts of the SDK without going into much detail.
71
+
72
+ ### Models
73
+
74
+ The core of the SDK contains pydantic models for requests, responses, resources, events and errors. Users of the SDK are meant to use these models directly or indirectly.
75
+
76
+ ### Server
77
+
78
+ The server module consists of 3 parts:
79
+
80
+ 1. Agent interface
81
+ 2. FastAPI application factory
82
+ 3. Uvicorn based server
83
+
84
+ Each part builds on top of the previous one. Not all parts need to be used, e.g. users are advised to bring their own ASGI server for production deployments.
85
+
86
+ ### Client
87
+
88
+ The client module consists of httpx based client with session support. The client is meant to be thin and mimic the REST API. Exception is session management which has been abstracted into a context manager.
89
+
90
+
91
+
@@ -2,7 +2,7 @@ import asyncio
2
2
 
3
3
  import httpx
4
4
  from acp_sdk.client.client import Client
5
- from acp_sdk.models import Message, TextMessagePart
5
+ from acp_sdk.models import Message, MessagePart
6
6
 
7
7
 
8
8
  async def example() -> None:
@@ -13,8 +13,8 @@ async def example() -> None:
13
13
  # Additional client configuration
14
14
  )
15
15
  ) as client:
16
- run = await client.run_sync(agent="echo", input=Message(TextMessagePart(content="Howdy!")))
17
- print(run.output)
16
+ run = await client.run_sync(agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!")])])
17
+ print(run)
18
18
 
19
19
 
20
20
  if __name__ == "__main__":
@@ -0,0 +1,18 @@
1
+ import asyncio
2
+
3
+ from acp_sdk.client import Client
4
+ from acp_sdk.models import (
5
+ Message,
6
+ MessagePart,
7
+ )
8
+
9
+
10
+ async def example() -> None:
11
+ async with Client(base_url="http://localhost:8000") as client, client.session() as session:
12
+ run = await session.run_sync(agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!")])])
13
+ run = await session.run_sync(agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy again!")])])
14
+ print(run)
15
+
16
+
17
+ if __name__ == "__main__":
18
+ asyncio.run(example())
@@ -3,14 +3,16 @@ import asyncio
3
3
  from acp_sdk.client import Client
4
4
  from acp_sdk.models import (
5
5
  Message,
6
- TextMessagePart,
6
+ MessagePart,
7
7
  )
8
8
 
9
9
 
10
10
  async def example() -> None:
11
11
  async with Client(base_url="http://localhost:8000") as client:
12
- run = await client.run_sync(agent="echo", input=Message(TextMessagePart(content="Howdy!")))
13
- print(run.output)
12
+ run = await client.run_sync(
13
+ agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!", content_type="text/plain")])]
14
+ )
15
+ print(run.outputs)
14
16
 
15
17
 
16
18
  if __name__ == "__main__":
@@ -1,12 +1,14 @@
1
1
  import asyncio
2
2
 
3
3
  from acp_sdk.client import Client
4
- from acp_sdk.models import Message, TextMessagePart
4
+ from acp_sdk.models import Message, MessagePart
5
5
 
6
6
 
7
7
  async def example() -> None:
8
8
  async with Client(base_url="http://localhost:8000") as client:
9
- async for event in client.run_stream(agent="echo", input=Message(TextMessagePart(content="Howdy!"))):
9
+ async for event in client.run_stream(
10
+ agent="echo", inputs=[Message(parts=[MessagePart(content="Howdy!", content_type="text/plain")])]
11
+ ):
10
12
  print(event)
11
13
 
12
14
 
@@ -0,0 +1,25 @@
1
+ from collections.abc import AsyncGenerator
2
+ from typing import Any
3
+
4
+ from acp_sdk.models import (
5
+ AwaitRequest,
6
+ AwaitResume,
7
+ Message,
8
+ MessagePart,
9
+ )
10
+ from acp_sdk.server import Context, Server
11
+
12
+ server = Server()
13
+
14
+
15
+ @server.agent()
16
+ async def awaiting(
17
+ inputs: list[Message], context: Context
18
+ ) -> AsyncGenerator[Message | AwaitRequest | Any, AwaitResume]:
19
+ """Greets and awaits for more data"""
20
+ yield MessagePart(content="Hello!", content_type="text/plain")
21
+ data = yield AwaitRequest()
22
+ yield MessagePart(content=f"Thanks for {data}", content_type="text/plain")
23
+
24
+
25
+ server.run()
@@ -10,13 +10,13 @@ server = Server()
10
10
 
11
11
 
12
12
  @server.agent()
13
- async def echo(input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
13
+ async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
14
14
  """Echoes everything"""
15
- for part in input:
15
+ for message in inputs:
16
16
  await asyncio.sleep(0.5)
17
17
  yield {"thought": "I should echo everyting"}
18
18
  await asyncio.sleep(0.5)
19
- yield Message(part)
19
+ yield message
20
20
 
21
21
 
22
- server()
22
+ server.run()
@@ -0,0 +1,25 @@
1
+ from collections.abc import AsyncGenerator
2
+
3
+ from acp_sdk.models import (
4
+ Message,
5
+ )
6
+ from acp_sdk.server import RunYield, RunYieldResume, agent, create_app
7
+
8
+ # This example demonstrates how to serve agents with you own server
9
+
10
+
11
+ @agent()
12
+ async def echo(inputs: list[Message]) -> AsyncGenerator[RunYield, RunYieldResume]:
13
+ """Echoes everything"""
14
+ for message in inputs:
15
+ yield message
16
+
17
+
18
+ app = create_app(echo)
19
+
20
+ # The app can now be used with any ASGI server
21
+
22
+ # Run with
23
+ # 1. fastapi run examples/servers/standalone.py
24
+ # 2. uvicorn examples.servers.standalone:app
25
+ # ...
@@ -1,9 +1,11 @@
1
1
  [project]
2
2
  name = "acp-sdk"
3
- version = "0.1.0rc7"
3
+ version = "0.2.0"
4
4
  description = "Agent Communication Protocol SDK"
5
+ license = "Apache-2.0"
5
6
  readme = "README.md"
6
- authors = []
7
+ authors = [{ name = "IBM Corp." }]
8
+ maintainers = [{ name = "Tomas Pilar", email = "thomas7pilar@gmail.com" }]
7
9
  requires-python = ">=3.11, <4.0"
8
10
  dependencies = ["opentelemetry-api>=1.31.1", "pydantic>=2.11.1"]
9
11
 
@@ -0,0 +1,5 @@
1
+ [pytest]
2
+ testpaths = tests/e2e
3
+ python_files = test_*.py
4
+ python_functions = test_*
5
+ addopts = -v --strict-markers
@@ -0,0 +1,2 @@
1
+ from acp_sdk.models import * # noqa: F403
2
+ from acp_sdk.version import __version__ as __version__
@@ -1,4 +1,6 @@
1
- from collections.abc import AsyncIterator
1
+ import uuid
2
+ from collections.abc import AsyncGenerator, AsyncIterator
3
+ from contextlib import asynccontextmanager
2
4
  from types import TracebackType
3
5
  from typing import Self
4
6
 
@@ -15,22 +17,31 @@ from acp_sdk.models import (
15
17
  AgentsListResponse,
16
18
  AwaitResume,
17
19
  Error,
20
+ Event,
18
21
  Message,
19
22
  Run,
20
23
  RunCancelResponse,
24
+ RunCreatedEvent,
21
25
  RunCreateRequest,
22
26
  RunCreateResponse,
23
- RunEvent,
24
27
  RunId,
25
28
  RunMode,
26
29
  RunResumeRequest,
27
30
  RunResumeResponse,
31
+ SessionId,
28
32
  )
29
33
 
30
34
 
31
35
  class Client:
32
- def __init__(self, *, base_url: httpx.URL | str = "", client: httpx.AsyncClient | None = None) -> None:
36
+ def __init__(
37
+ self,
38
+ *,
39
+ base_url: httpx.URL | str = "",
40
+ session_id: SessionId | None = None,
41
+ client: httpx.AsyncClient | None = None,
42
+ ) -> None:
33
43
  self.base_url = base_url
44
+ self.session_id = session_id
34
45
 
35
46
  self._client = self._init_client(client)
36
47
 
@@ -51,6 +62,10 @@ class Client:
51
62
  ) -> None:
52
63
  await self._client.__aexit__(exc_type, exc_value, traceback)
53
64
 
65
+ @asynccontextmanager
66
+ async def session(self, session_id: SessionId | None = None) -> AsyncGenerator[Self]:
67
+ yield Client(client=self._client, session_id=session_id or uuid.uuid4())
68
+
54
69
  async def agents(self) -> AsyncIterator[Agent]:
55
70
  response = await self._client.get("/agents")
56
71
  self._raise_error(response)
@@ -62,30 +77,51 @@ class Client:
62
77
  self._raise_error(response)
63
78
  return AgentReadResponse.model_validate(response.json())
64
79
 
65
- async def run_sync(self, *, agent: AgentName, input: Message) -> Run:
80
+ async def run_sync(self, *, agent: AgentName, inputs: list[Message]) -> Run:
66
81
  response = await self._client.post(
67
82
  "/runs",
68
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.SYNC).model_dump(),
83
+ content=RunCreateRequest(
84
+ agent_name=agent,
85
+ inputs=inputs,
86
+ mode=RunMode.SYNC,
87
+ session_id=self.session_id,
88
+ ).model_dump_json(),
69
89
  )
70
90
  self._raise_error(response)
71
- return RunCreateResponse.model_validate(response.json())
91
+ response = RunCreateResponse.model_validate(response.json())
92
+ self._set_session(response)
93
+ return response
72
94
 
73
- async def run_async(self, *, agent: AgentName, input: Message) -> Run:
95
+ async def run_async(self, *, agent: AgentName, inputs: list[Message]) -> Run:
74
96
  response = await self._client.post(
75
97
  "/runs",
76
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.ASYNC).model_dump(),
98
+ content=RunCreateRequest(
99
+ agent_name=agent,
100
+ inputs=inputs,
101
+ mode=RunMode.ASYNC,
102
+ session_id=self.session_id,
103
+ ).model_dump_json(),
77
104
  )
78
105
  self._raise_error(response)
79
- return RunCreateResponse.model_validate(response.json())
106
+ response = RunCreateResponse.model_validate(response.json())
107
+ self._set_session(response)
108
+ return response
80
109
 
81
- async def run_stream(self, *, agent: AgentName, input: Message) -> AsyncIterator[RunEvent]:
110
+ async def run_stream(self, *, agent: AgentName, inputs: list[Message]) -> AsyncIterator[Event]:
82
111
  async with aconnect_sse(
83
112
  self._client,
84
113
  "POST",
85
114
  "/runs",
86
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.STREAM).model_dump(),
115
+ content=RunCreateRequest(
116
+ agent_name=agent,
117
+ inputs=inputs,
118
+ mode=RunMode.STREAM,
119
+ session_id=self.session_id,
120
+ ).model_dump_json(),
87
121
  ) as event_source:
88
122
  async for event in self._validate_stream(event_source):
123
+ if isinstance(event, RunCreatedEvent):
124
+ self._set_session(event.run)
89
125
  yield event
90
126
 
91
127
  async def run_status(self, *, run_id: RunId) -> Run:
@@ -114,7 +150,7 @@ class Client:
114
150
  self._raise_error(response)
115
151
  return RunResumeResponse.model_validate(response.json())
116
152
 
117
- async def run_resume_stream(self, *, run_id: RunId, await_: AwaitResume) -> AsyncIterator[RunEvent]:
153
+ async def run_resume_stream(self, *, run_id: RunId, await_: AwaitResume) -> AsyncIterator[Event]:
118
154
  async with aconnect_sse(
119
155
  self._client,
120
156
  "POST",
@@ -127,9 +163,9 @@ class Client:
127
163
  async def _validate_stream(
128
164
  self,
129
165
  event_source: EventSource,
130
- ) -> AsyncIterator[RunEvent]:
166
+ ) -> AsyncIterator[Event]:
131
167
  async for event in event_source.aiter_sse():
132
- event = TypeAdapter(RunEvent).validate_json(event.data)
168
+ event = TypeAdapter(Event).validate_json(event.data)
133
169
  yield event
134
170
 
135
171
  def _raise_error(self, response: httpx.Response) -> None:
@@ -137,3 +173,6 @@ class Client:
137
173
  response.raise_for_status()
138
174
  except httpx.HTTPError:
139
175
  raise ACPError(Error.model_validate(response.json()))
176
+
177
+ def _set_session(self, run: Run) -> None:
178
+ self.session_id = run.session_id
@@ -0,0 +1,170 @@
1
+ import uuid
2
+ from enum import Enum
3
+ from typing import Any, Literal, Optional, Union
4
+
5
+ from pydantic import AnyUrl, BaseModel, ConfigDict, Field
6
+
7
+ from acp_sdk.models.errors import Error
8
+
9
+
10
+ class Metadata(BaseModel):
11
+ model_config = ConfigDict(extra="allow")
12
+
13
+
14
+ class AnyModel(BaseModel):
15
+ model_config = ConfigDict(extra="allow")
16
+
17
+
18
+ class MessagePart(BaseModel):
19
+ name: Optional[str] = None
20
+ content_type: str
21
+ content: Optional[str] = None
22
+ content_encoding: Optional[Literal["plain", "base64"]] = "plain"
23
+ content_url: Optional[AnyUrl] = None
24
+
25
+ model_config = ConfigDict(extra="forbid")
26
+
27
+ def model_post_init(self, __context: Any) -> None:
28
+ if self.content is None and self.content_url is None:
29
+ raise ValueError("Either content or content_url must be provided")
30
+ if self.content is not None and self.content_url is not None:
31
+ raise ValueError("Only one of content or content_url can be provided")
32
+
33
+
34
+ class Artifact(MessagePart):
35
+ name: str
36
+
37
+
38
+ class Message(BaseModel):
39
+ parts: list[MessagePart]
40
+
41
+ def __add__(self, other: "Message") -> "Message":
42
+ if not isinstance(other, Message):
43
+ raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
44
+ return Message(*(self.parts + other.parts))
45
+
46
+ def __str__(self) -> str:
47
+ return "".join(
48
+ part.content for part in self.parts if part.content is not None and part.content_type == "text/plain"
49
+ )
50
+
51
+
52
+ AgentName = str
53
+ SessionId = uuid.UUID
54
+ RunId = uuid.UUID
55
+
56
+
57
+ class RunMode(str, Enum):
58
+ SYNC = "sync"
59
+ ASYNC = "async"
60
+ STREAM = "stream"
61
+
62
+
63
+ class RunStatus(str, Enum):
64
+ CREATED = "created"
65
+ IN_PROGRESS = "in-progress"
66
+ AWAITING = "awaiting"
67
+ CANCELLING = "cancelling"
68
+ CANCELLED = "cancelled"
69
+ COMPLETED = "completed"
70
+ FAILED = "failed"
71
+
72
+ @property
73
+ def is_terminal(self) -> bool:
74
+ terminal_states = {RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED}
75
+ return self in terminal_states
76
+
77
+
78
+ class AwaitRequest(BaseModel):
79
+ type: Literal["placeholder"] = "placeholder"
80
+
81
+
82
+ class AwaitResume(BaseModel):
83
+ pass
84
+
85
+
86
+ class Run(BaseModel):
87
+ run_id: RunId = Field(default_factory=uuid.uuid4)
88
+ agent_name: AgentName
89
+ session_id: SessionId | None = None
90
+ status: RunStatus = RunStatus.CREATED
91
+ await_request: AwaitRequest | None = None
92
+ outputs: list[Message] = []
93
+ error: Error | None = None
94
+
95
+
96
+ class MessageCreatedEvent(BaseModel):
97
+ type: Literal["message.created"] = "message.created"
98
+ message: Message
99
+
100
+
101
+ class MessagePartEvent(BaseModel):
102
+ type: Literal["message.part"] = "message.part"
103
+ part: MessagePart
104
+
105
+
106
+ class ArtifactEvent(BaseModel):
107
+ type: Literal["message.part"] = "message.part"
108
+ part: Artifact
109
+
110
+
111
+ class MessageCompletedEvent(BaseModel):
112
+ type: Literal["message.completed"] = "message.completed"
113
+ message: Message
114
+
115
+
116
+ class AwaitEvent(BaseModel):
117
+ type: Literal["await"] = "await"
118
+ await_request: AwaitRequest | None = None
119
+
120
+
121
+ class GenericEvent(BaseModel):
122
+ type: Literal["generic"] = "generic"
123
+ generic: AnyModel
124
+
125
+
126
+ class RunCreatedEvent(BaseModel):
127
+ type: Literal["run.created"] = "run.created"
128
+ run: Run
129
+
130
+
131
+ class RunInProgressEvent(BaseModel):
132
+ type: Literal["run.in-progress"] = "run.in-progress"
133
+ run: Run
134
+
135
+
136
+ class RunFailedEvent(BaseModel):
137
+ type: Literal["run.failed"] = "run.failed"
138
+ run: Run
139
+
140
+
141
+ class RunCancelledEvent(BaseModel):
142
+ type: Literal["run.cancelled"] = "run.cancelled"
143
+ run: Run
144
+
145
+
146
+ class RunCompletedEvent(BaseModel):
147
+ type: Literal["run.completed"] = "run.completed"
148
+ run: Run
149
+
150
+
151
+ Event = Union[
152
+ RunCreatedEvent,
153
+ RunInProgressEvent,
154
+ MessageCreatedEvent,
155
+ ArtifactEvent,
156
+ MessagePartEvent,
157
+ MessageCompletedEvent,
158
+ AwaitEvent,
159
+ GenericEvent,
160
+ RunCancelledEvent,
161
+ RunFailedEvent,
162
+ RunCompletedEvent,
163
+ MessagePartEvent,
164
+ ]
165
+
166
+
167
+ class Agent(BaseModel):
168
+ name: str
169
+ description: str | None = None
170
+ metadata: Metadata = Metadata()