acp-sdk 0.10.1__tar.gz → 0.11.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 (61) hide show
  1. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/PKG-INFO +2 -1
  2. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/pyproject.toml +2 -1
  3. acp_sdk-0.11.0/src/acp_sdk/client/client.py +324 -0
  4. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/models/__init__.py +1 -0
  5. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/models/models.py +75 -9
  6. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/models/schemas.py +18 -3
  7. acp_sdk-0.11.0/src/acp_sdk/models/types.py +9 -0
  8. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/__init__.py +5 -1
  9. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/agent.py +8 -84
  10. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/app.py +87 -29
  11. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/context.py +11 -3
  12. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/executor.py +139 -14
  13. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/server.py +17 -4
  14. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/memory_store.py +4 -4
  15. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/utils.py +1 -1
  16. acp_sdk-0.11.0/src/acp_sdk/shared/__init__.py +2 -0
  17. acp_sdk-0.11.0/src/acp_sdk/shared/resources.py +46 -0
  18. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/fixtures/server.py +44 -3
  19. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/test_suites/test_discovery.py +4 -4
  20. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/test_suites/test_runs.py +3 -11
  21. acp_sdk-0.11.0/tests/e2e/test_suites/test_sessions.py +45 -0
  22. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/client/test_client.py +42 -7
  23. acp_sdk-0.10.1/src/acp_sdk/client/client.py +0 -233
  24. acp_sdk-0.10.1/src/acp_sdk/server/session.py +0 -24
  25. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/.gitignore +0 -0
  26. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/.python-version +0 -0
  27. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/README.md +0 -0
  28. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/.gitignore +0 -0
  29. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/Makefile +0 -0
  30. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/conf.py +0 -0
  31. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/index.rst +0 -0
  32. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/docs/make.bat +0 -0
  33. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/pytest.ini +0 -0
  34. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/__init__.py +0 -0
  35. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/client/__init__.py +0 -0
  36. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/client/types.py +0 -0
  37. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/client/utils.py +0 -0
  38. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/instrumentation.py +0 -0
  39. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/models/errors.py +0 -0
  40. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/py.typed +0 -0
  41. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/errors.py +0 -0
  42. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/logging.py +0 -0
  43. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/__init__.py +0 -0
  44. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/postgresql_store.py +0 -0
  45. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/redis_store.py +0 -0
  46. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/store.py +0 -0
  47. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/store/utils.py +0 -0
  48. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/telemetry.py +0 -0
  49. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/server/types.py +0 -0
  50. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/src/acp_sdk/version.py +0 -0
  51. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/conftest.py +0 -0
  52. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/__init__.py +0 -0
  53. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/config.py +0 -0
  54. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/fixtures/__init__.py +0 -0
  55. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/fixtures/client.py +0 -0
  56. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/e2e/test_suites/__init__.py +0 -0
  57. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/client/test_utils.py +0 -0
  58. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/models/__init__.py +0 -0
  59. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/models/test_models.py +0 -0
  60. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/server/__init__.py +0 -0
  61. {acp_sdk-0.10.1 → acp_sdk-0.11.0}/tests/unit/server/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acp-sdk
3
- Version: 0.10.1
3
+ Version: 0.11.0
4
4
  Summary: Agent Communication Protocol SDK
5
5
  Author: IBM Corp.
6
6
  Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
@@ -11,6 +11,7 @@ Requires-Dist: fastapi[standard]>=0.115
11
11
  Requires-Dist: httpx-sse>=0.4
12
12
  Requires-Dist: httpx>=0.26
13
13
  Requires-Dist: janus>=2.0
14
+ Requires-Dist: obstore>=0.6
14
15
  Requires-Dist: opentelemetry-api>=1.31
15
16
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31
16
17
  Requires-Dist: opentelemetry-instrumentation-fastapi>=0.52b1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acp-sdk"
3
- version = "0.10.1"
3
+ version = "0.11.0"
4
4
  description = "Agent Communication Protocol SDK"
5
5
  license = "Apache-2.0"
6
6
  readme = "README.md"
@@ -21,6 +21,7 @@ dependencies = [
21
21
  "cachetools>=5.5",
22
22
  "redis>=6.1",
23
23
  "psycopg[binary]>=3.2",
24
+ "obstore>=0.6",
24
25
  ]
25
26
 
26
27
  [build-system]
@@ -0,0 +1,324 @@
1
+ import asyncio
2
+ import logging
3
+ import ssl
4
+ import typing
5
+ from collections.abc import AsyncIterator
6
+ from types import TracebackType
7
+ from typing import Self
8
+
9
+ import httpx
10
+ from httpx_sse import EventSource, aconnect_sse
11
+ from pydantic import TypeAdapter
12
+
13
+ from acp_sdk.client.types import Input
14
+ from acp_sdk.client.utils import input_to_messages
15
+ from acp_sdk.instrumentation import get_tracer
16
+ from acp_sdk.models import (
17
+ ACPError,
18
+ AgentManifest,
19
+ AgentName,
20
+ AgentReadResponse,
21
+ AgentsListResponse,
22
+ AwaitResume,
23
+ Error,
24
+ ErrorCode,
25
+ ErrorEvent,
26
+ Event,
27
+ PingResponse,
28
+ Run,
29
+ RunCancelResponse,
30
+ RunCreateRequest,
31
+ RunCreateResponse,
32
+ RunEventsListResponse,
33
+ RunId,
34
+ RunMode,
35
+ RunResumeRequest,
36
+ RunResumeResponse,
37
+ Session,
38
+ SessionReadResponse,
39
+ )
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class Client:
45
+ def __init__(
46
+ self,
47
+ *,
48
+ session: Session | None = None,
49
+ client: httpx.AsyncClient | None = None,
50
+ manage_client: bool = True,
51
+ auth: httpx._types.AuthTypes | None = None,
52
+ params: httpx._types.QueryParamTypes | None = None,
53
+ headers: httpx._types.HeaderTypes | None = None,
54
+ cookies: httpx._types.CookieTypes | None = None,
55
+ timeout: httpx._types.TimeoutTypes = None,
56
+ verify: ssl.SSLContext | str | bool = True,
57
+ cert: httpx._types.CertTypes | None = None,
58
+ http1: bool = True,
59
+ http2: bool = False,
60
+ proxy: httpx._types.ProxyTypes | None = None,
61
+ mounts: None | (typing.Mapping[str, httpx.AsyncBaseTransport | None]) = None,
62
+ follow_redirects: bool = False,
63
+ limits: httpx.Limits = httpx._config.DEFAULT_LIMITS,
64
+ max_redirects: int = httpx._config.DEFAULT_MAX_REDIRECTS,
65
+ event_hooks: None | (typing.Mapping[str, list[httpx._client.EventHook]]) = None,
66
+ base_url: httpx.URL | str = "",
67
+ transport: httpx.AsyncBaseTransport | None = None,
68
+ trust_env: bool = True,
69
+ ) -> None:
70
+ self._session = session
71
+ self._session_last_refresh_base_url: httpx.URL | None = None
72
+ self._session_refresh_lock = asyncio.Lock()
73
+
74
+ self._client = client or httpx.AsyncClient(
75
+ auth=auth,
76
+ params=params,
77
+ headers=headers,
78
+ cookies=cookies,
79
+ timeout=timeout,
80
+ verify=verify,
81
+ cert=cert,
82
+ http1=http1,
83
+ http2=http2,
84
+ proxy=proxy,
85
+ mounts=mounts,
86
+ follow_redirects=follow_redirects,
87
+ limits=limits,
88
+ max_redirects=max_redirects,
89
+ event_hooks=event_hooks,
90
+ base_url=base_url,
91
+ transport=transport,
92
+ trust_env=trust_env,
93
+ )
94
+ self._manage_client = manage_client
95
+
96
+ @property
97
+ def client(self) -> httpx.AsyncClient:
98
+ return self._client
99
+
100
+ async def __aenter__(self) -> Self:
101
+ if self._manage_client:
102
+ await self._client.__aenter__()
103
+ self._session_span_manager = (
104
+ (
105
+ get_tracer()
106
+ .start_as_current_span("session", attributes={"acp.session": str(self._session.id)})
107
+ .__enter__()
108
+ )
109
+ if self._session
110
+ else None
111
+ )
112
+ return self
113
+
114
+ async def __aexit__(
115
+ self,
116
+ exc_type: type[BaseException] | None = None,
117
+ exc_value: BaseException | None = None,
118
+ traceback: TracebackType | None = None,
119
+ ) -> None:
120
+ if self._session_span_manager:
121
+ self._session_span_manager.__exit__(exc_type, exc_value, traceback)
122
+ if self._manage_client:
123
+ await self._client.__aexit__(exc_type, exc_value, traceback)
124
+
125
+ def session(self, session: Session | None = None) -> Self:
126
+ return Client(client=self._client, manage_client=False, session=session or Session())
127
+
128
+ async def agents(self, *, base_url: httpx.URL | str | None = None) -> AsyncIterator[AgentManifest]:
129
+ response = await self._client.get(self._create_url("/agents", base_url=base_url))
130
+ self._raise_error(response)
131
+ for agent in AgentsListResponse.model_validate(response.json()).agents:
132
+ yield agent
133
+
134
+ async def agent(self, *, name: AgentName, base_url: httpx.URL | str | None = None) -> AgentManifest:
135
+ response = await self._client.get(self._create_url(f"/agents/{name}", base_url=base_url))
136
+ self._raise_error(response)
137
+ response = AgentReadResponse.model_validate(response.json())
138
+ return AgentManifest(**response.model_dump())
139
+
140
+ async def ping(self, *, base_url: httpx.URL | str | None = None) -> bool:
141
+ response = await self._client.get(self._create_url("/ping", base_url=base_url))
142
+ self._raise_error(response)
143
+ PingResponse.model_validate(response.json())
144
+ return
145
+
146
+ async def run_sync(self, input: Input, *, agent: AgentName, base_url: httpx.URL | str | None = None) -> Run:
147
+ response = await self._client.post(
148
+ self._create_url("/runs", base_url=base_url),
149
+ content=RunCreateRequest(
150
+ agent_name=agent,
151
+ input=input_to_messages(input),
152
+ mode=RunMode.SYNC,
153
+ **(await self._prepare_session_for_run(base_url=base_url)),
154
+ ).model_dump_json(),
155
+ )
156
+ self._raise_error(response)
157
+ response = RunCreateResponse.model_validate(response.json())
158
+ return Run(**response.model_dump())
159
+
160
+ async def run_async(self, input: Input, *, agent: AgentName, base_url: httpx.URL | str | None = None) -> Run:
161
+ response = await self._client.post(
162
+ self._create_url("/runs", base_url=base_url),
163
+ content=RunCreateRequest(
164
+ agent_name=agent,
165
+ input=input_to_messages(input),
166
+ mode=RunMode.ASYNC,
167
+ **(await self._prepare_session_for_run(base_url=base_url)),
168
+ ).model_dump_json(),
169
+ )
170
+ self._raise_error(response)
171
+ response = RunCreateResponse.model_validate(response.json())
172
+ return Run(**response.model_dump())
173
+
174
+ async def run_stream(
175
+ self, input: Input, *, agent: AgentName, base_url: httpx.URL | str | None = None
176
+ ) -> AsyncIterator[Event]:
177
+ async with aconnect_sse(
178
+ self._client,
179
+ "POST",
180
+ self._create_url("/runs", base_url=base_url),
181
+ content=RunCreateRequest(
182
+ agent_name=agent,
183
+ input=input_to_messages(input),
184
+ mode=RunMode.STREAM,
185
+ session=await self._prepare_session_for_run(base_url=base_url),
186
+ ).model_dump_json(),
187
+ ) as event_source:
188
+ async for event in self._validate_stream(event_source):
189
+ yield event
190
+
191
+ async def run_status(self, *, run_id: RunId, base_url: httpx.URL | str | None = None) -> Run:
192
+ response = await self._client.get(self._create_url(f"/runs/{run_id}", base_url=base_url))
193
+ self._raise_error(response)
194
+ return Run.model_validate(response.json())
195
+
196
+ async def run_events(self, *, run_id: RunId, base_url: httpx.URL | str | None = None) -> AsyncIterator[Event]:
197
+ response = await self._client.get(self._create_url(f"/runs/{run_id}/events", base_url=base_url))
198
+ self._raise_error(response)
199
+ response = RunEventsListResponse.model_validate(response.json())
200
+ for event in response.events:
201
+ yield event
202
+
203
+ async def run_cancel(self, *, run_id: RunId, base_url: httpx.URL | str | None = None) -> Run:
204
+ response = await self._client.post(self._create_url(f"/runs/{run_id}/cancel", base_url=base_url))
205
+ self._raise_error(response)
206
+ response = RunCancelResponse.model_validate(response.json())
207
+ return Run(**response.model_dump())
208
+
209
+ async def run_resume_sync(
210
+ self, await_resume: AwaitResume, *, run_id: RunId, base_url: httpx.URL | str | None = None
211
+ ) -> Run:
212
+ response = await self._client.post(
213
+ self._create_url(f"/runs/{run_id}", base_url=base_url),
214
+ content=RunResumeRequest(await_resume=await_resume, mode=RunMode.SYNC).model_dump_json(),
215
+ )
216
+ self._raise_error(response)
217
+ response = RunResumeResponse.model_validate(response.json())
218
+ return Run(**response.model_dump())
219
+
220
+ async def run_resume_async(
221
+ self, await_resume: AwaitResume, *, run_id: RunId, base_url: httpx.URL | str | None = None
222
+ ) -> Run:
223
+ response = await self._client.post(
224
+ self._create_url(f"/runs/{run_id}", base_url=base_url),
225
+ content=RunResumeRequest(await_resume=await_resume, mode=RunMode.ASYNC).model_dump_json(),
226
+ )
227
+ self._raise_error(response)
228
+ response = RunResumeResponse.model_validate(response.json())
229
+ return Run(**response.model_dump())
230
+
231
+ async def run_resume_stream(
232
+ self, await_resume: AwaitResume, *, run_id: RunId, base_url: httpx.URL | str | None = None
233
+ ) -> AsyncIterator[Event]:
234
+ async with aconnect_sse(
235
+ self._client,
236
+ "POST",
237
+ self._create_url(f"/runs/{run_id}", base_url=base_url),
238
+ content=RunResumeRequest(await_resume=await_resume, mode=RunMode.STREAM).model_dump_json(),
239
+ ) as event_source:
240
+ async for event in self._validate_stream(event_source):
241
+ yield event
242
+
243
+ async def refresh_session(
244
+ self, *, base_url: httpx.URL | str | None = None, timeout: httpx._types.TimeoutTypes = 5000
245
+ ) -> Session:
246
+ if not self._session:
247
+ raise RuntimeError("Client is not in a session")
248
+
249
+ async with self._session_refresh_lock:
250
+ url = self._create_url(
251
+ f"/sessions/{self._session.id}",
252
+ base_url=base_url or self._session_last_refresh_base_url,
253
+ )
254
+
255
+ try:
256
+ response = await self._client.get(url, timeout=timeout)
257
+ response = SessionReadResponse.model_validate(response.json())
258
+ self._session = Session(**response.model_dump())
259
+ except ACPError as e:
260
+ if e.error.code == ErrorCode.NOT_FOUND:
261
+ pass
262
+ raise e
263
+
264
+ return self._session
265
+
266
+ async def _validate_stream(
267
+ self,
268
+ event_source: EventSource,
269
+ ) -> AsyncIterator[Event]:
270
+ if event_source.response.is_error:
271
+ await event_source.response.aread()
272
+ self._raise_error(event_source.response)
273
+ async for event in event_source.aiter_sse():
274
+ event: Event = TypeAdapter(Event).validate_json(event.data)
275
+ if isinstance(event, ErrorEvent):
276
+ raise ACPError(error=event.error)
277
+ yield event
278
+
279
+ def _raise_error(self, response: httpx.Response) -> None:
280
+ try:
281
+ response.raise_for_status()
282
+ except httpx.HTTPError:
283
+ raise ACPError(Error.model_validate(response.json()))
284
+
285
+ def _create_base_url(self, base_url: httpx.URL | str | None) -> httpx.URL:
286
+ base_url = httpx.URL(base_url or self._client.base_url)
287
+ if not base_url.raw_path.endswith(b"/"):
288
+ base_url = base_url.copy_with(raw_path=base_url.raw_path + b"/")
289
+ return base_url
290
+
291
+ def _create_url(self, endpoint: str, base_url: httpx.URL | str | None) -> httpx.URL:
292
+ merge_url = httpx.URL(endpoint)
293
+
294
+ if not merge_url.is_relative_url:
295
+ raise ValueError("Endpoint must be a relative URL")
296
+
297
+ base_url = self._create_base_url(base_url)
298
+ merge_raw_path = base_url.raw_path + merge_url.raw_path.lstrip(b"/")
299
+ return base_url.copy_with(raw_path=merge_raw_path)
300
+
301
+ async def _prepare_session_for_run(self, *, base_url: httpx.URL | str | None) -> dict:
302
+ if not self._session:
303
+ return {}
304
+
305
+ target_base_url = self._create_base_url(base_url=base_url)
306
+ try:
307
+ if not self._session_last_refresh_base_url:
308
+ return {"session": self._session}
309
+ if self._session_last_refresh_base_url == target_base_url:
310
+ # Same server, no need to forward session
311
+ return {"session_id": self._session.id}
312
+
313
+ session = await self.refresh_session()
314
+ return {"session": session}
315
+ except ACPError as e:
316
+ if e.error.code == ErrorCode.NOT_FOUND:
317
+ return {"session": self._session}
318
+ raise e
319
+ finally:
320
+ await self._update_session_refresh_url(target_base_url)
321
+
322
+ async def _update_session_refresh_url(self, url: httpx.URL) -> None:
323
+ async with self._session_refresh_lock:
324
+ self._session_last_refresh_base_url = url
@@ -1,3 +1,4 @@
1
1
  from acp_sdk.models.errors import * # noqa: F403
2
2
  from acp_sdk.models.models import * # noqa: F403
3
3
  from acp_sdk.models.schemas import * # noqa: F403
4
+ from acp_sdk.models.types import * # noqa: F403
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import uuid
3
+ from collections.abc import AsyncIterator
3
4
  from datetime import datetime, timezone
4
5
  from enum import Enum
5
6
  from typing import Any, Literal, Optional, Union
@@ -7,6 +8,8 @@ from typing import Any, Literal, Optional, Union
7
8
  from pydantic import AnyUrl, BaseModel, ConfigDict, Field
8
9
 
9
10
  from acp_sdk.models.errors import ACPError, Error
11
+ from acp_sdk.models.types import AgentName, ResourceId, ResourceUrl, RunId, SessionId
12
+ from acp_sdk.shared import ResourceLoader, ResourceStore
10
13
 
11
14
 
12
15
  class AnyModel(BaseModel):
@@ -74,6 +77,40 @@ class Metadata(BaseModel):
74
77
  model_config = ConfigDict(extra="allow")
75
78
 
76
79
 
80
+ class CitationMetadata(BaseModel):
81
+ """
82
+ Represents an inline citation, providing info about information source. This
83
+ is supposed to be rendered as an inline icon, optionally marking a text
84
+ range it belongs to.
85
+
86
+ If CitationMetadata is included together with content in the message part,
87
+ the citation belongs to that content and renders at the MessagePart position.
88
+ This way may be used for non-text content, like images and files.
89
+
90
+ Alternatively, `start_index` and `end_index` may define a text range,
91
+ counting characters in the current Message across all MessageParts with
92
+ content type `text/*`, where the citation will be rendered. If one of
93
+ `start_index` and `end_index` is missing or their values are equal, the
94
+ citation renders only as an inline icon at that position.
95
+
96
+ If both `start_index` and `end_index` are not present and MessagePart has no
97
+ content, the citation renders as inline icon only at the MessagePart position.
98
+
99
+ Properties:
100
+ - url: URL of the source document.
101
+ - title: Title of the source document.
102
+ - description: Accompanying text, which may be a general description of the
103
+ source document, or a specific snippet.
104
+ """
105
+
106
+ kind: Literal["citation"] = "citation"
107
+ start_index: Optional[int]
108
+ end_index: Optional[int]
109
+ url: Optional[str]
110
+ title: Optional[str]
111
+ description: Optional[str]
112
+
113
+
77
114
  class MessagePart(BaseModel):
78
115
  name: Optional[str] = None
79
116
  content_type: Optional[str] = "text/plain"
@@ -83,9 +120,9 @@ class MessagePart(BaseModel):
83
120
 
84
121
  model_config = ConfigDict(extra="allow")
85
122
 
123
+ metadata: Optional[CitationMetadata] = Field(discriminator="kind", default=None)
124
+
86
125
  def model_post_init(self, __context: Any) -> None:
87
- if self.content is None and self.content_url is None:
88
- raise ValueError("Either content or content_url must be provided")
89
126
  if self.content is not None and self.content_url is not None:
90
127
  raise ValueError("Only one of content or content_url can be provided")
91
128
 
@@ -95,6 +132,7 @@ class Artifact(MessagePart):
95
132
 
96
133
 
97
134
  class Message(BaseModel):
135
+ role: Literal["user"] | Literal["agent"] | str = Field("user", pattern=r"^(user|agent(\/[a-zA-Z0-9_\-]+)?)$")
98
136
  parts: list[MessagePart]
99
137
  created_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc))
100
138
  completed_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -102,7 +140,10 @@ class Message(BaseModel):
102
140
  def __add__(self, other: "Message") -> "Message":
103
141
  if not isinstance(other, Message):
104
142
  raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
143
+ if self.role != other.role:
144
+ raise ValueError("Cannot concatenate messages with different roles")
105
145
  return Message(
146
+ role=self.role,
106
147
  parts=self.parts + other.parts,
107
148
  created_at=min(self.created_at, other.created_at) if self.created_at and other.created_at else None,
108
149
  completed_at=max(self.completed_at, other.completed_at)
@@ -146,11 +187,6 @@ class Message(BaseModel):
146
187
  return Message(parts=parts, created_at=self.created_at, completed_at=self.completed_at)
147
188
 
148
189
 
149
- AgentName = str
150
- SessionId = uuid.UUID
151
- RunId = uuid.UUID
152
-
153
-
154
190
  class RunMode(str, Enum):
155
191
  SYNC = "sync"
156
192
  ASYNC = "async"
@@ -280,11 +316,41 @@ Event = Union[
280
316
  RunCancelledEvent,
281
317
  RunFailedEvent,
282
318
  RunCompletedEvent,
283
- MessagePartEvent,
284
319
  ]
285
320
 
286
321
 
287
- class Agent(BaseModel):
322
+ class AgentManifest(BaseModel):
288
323
  name: str
289
324
  description: str | None = None
290
325
  metadata: Metadata = Metadata()
326
+
327
+
328
+ class Session(BaseModel):
329
+ id: SessionId = Field(default_factory=uuid.uuid4)
330
+ history: list[ResourceUrl] = Field(default_factory=list)
331
+ state: ResourceUrl | None = None
332
+
333
+ loader: ResourceLoader | None = Field(None, exclude=True)
334
+ store: ResourceStore | None = Field(None, exclude=True)
335
+
336
+ model_config = ConfigDict(arbitrary_types_allowed=True)
337
+
338
+ async def load_history(self, *, loader: ResourceLoader | None = None) -> AsyncIterator[Message]:
339
+ loader = loader or self.loader or ResourceLoader()
340
+ for url in self.history:
341
+ data = await loader.load(url)
342
+ yield Message.model_validate_json(data)
343
+
344
+ async def load_state(self, *, loader: ResourceLoader | None = None) -> bytes:
345
+ loader = loader or self.loader or ResourceLoader()
346
+ data = await loader.load(self.state)
347
+ return data
348
+
349
+ async def store_state(self, data: bytes, *, store: ResourceStore | None = None) -> ResourceUrl:
350
+ store = store or self.store
351
+ if not store:
352
+ raise ValueError("Store must be specified")
353
+
354
+ id = ResourceId()
355
+ await store.store(id, data)
356
+ return await store.url(id)
@@ -1,6 +1,16 @@
1
1
  from pydantic import BaseModel
2
2
 
3
- from acp_sdk.models.models import Agent, AgentName, AwaitResume, Event, Message, Run, RunMode, SessionId
3
+ from acp_sdk.models.models import (
4
+ AgentManifest,
5
+ AgentName,
6
+ AwaitResume,
7
+ Event,
8
+ Message,
9
+ Run,
10
+ RunMode,
11
+ Session,
12
+ SessionId,
13
+ )
4
14
 
5
15
 
6
16
  class PingResponse(BaseModel):
@@ -8,16 +18,17 @@ class PingResponse(BaseModel):
8
18
 
9
19
 
10
20
  class AgentsListResponse(BaseModel):
11
- agents: list[Agent]
21
+ agents: list[AgentManifest]
12
22
 
13
23
 
14
- class AgentReadResponse(Agent):
24
+ class AgentReadResponse(AgentManifest):
15
25
  pass
16
26
 
17
27
 
18
28
  class RunCreateRequest(BaseModel):
19
29
  agent_name: AgentName
20
30
  session_id: SessionId | None = None
31
+ session: Session | None = None
21
32
  input: list[Message]
22
33
  mode: RunMode = RunMode.SYNC
23
34
 
@@ -45,3 +56,7 @@ class RunCancelResponse(Run):
45
56
 
46
57
  class RunEventsListResponse(BaseModel):
47
58
  events: list[Event]
59
+
60
+
61
+ class SessionReadResponse(Session):
62
+ pass
@@ -0,0 +1,9 @@
1
+ from uuid import UUID
2
+
3
+ from pydantic import AnyHttpUrl
4
+
5
+ AgentName = str
6
+ SessionId = UUID
7
+ RunId = UUID
8
+ ResourceUrl = AnyHttpUrl
9
+ ResourceId = UUID
@@ -1,7 +1,11 @@
1
- from acp_sdk.server.agent import Agent as Agent
1
+ from acp_sdk.server.agent import AgentManifest as AgentManifest
2
2
  from acp_sdk.server.agent import agent as agent
3
3
  from acp_sdk.server.app import create_app as create_app
4
4
  from acp_sdk.server.context import Context as Context
5
5
  from acp_sdk.server.server import Server as Server
6
+ from acp_sdk.server.store import MemoryStore as MemoryStore
7
+ from acp_sdk.server.store import PostgreSQLStore as PostgreSQLStore
8
+ from acp_sdk.server.store import RedisStore as RedisStore
9
+ from acp_sdk.server.store import Store as Store
6
10
  from acp_sdk.server.types import RunYield as RunYield
7
11
  from acp_sdk.server.types import RunYieldResume as RunYieldResume