acp-sdk 0.4.0__tar.gz → 0.6.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 (45) hide show
  1. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/PKG-INFO +8 -6
  2. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/README.md +5 -4
  3. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/pyproject.toml +6 -2
  4. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/client/client.py +83 -18
  5. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/app.py +5 -3
  6. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/bundle.py +16 -8
  7. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/server.py +4 -1
  8. acp_sdk-0.6.0/src/acp_sdk/server/types.py +9 -0
  9. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/e2e/fixtures/server.py +5 -3
  10. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/e2e/test_suites/test_runs.py +47 -15
  11. acp_sdk-0.6.0/tests/unit/client/test_client.py +36 -0
  12. acp_sdk-0.4.0/src/acp_sdk/server/types.py +0 -6
  13. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/.gitignore +0 -0
  14. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/.python-version +0 -0
  15. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/docs/_sidebar.md +0 -0
  16. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/docs/client.md +0 -0
  17. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/docs/index.html +0 -0
  18. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/docs/models.md +0 -0
  19. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/docs/server.md +0 -0
  20. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/pytest.ini +0 -0
  21. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/__init__.py +0 -0
  22. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/client/__init__.py +0 -0
  23. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/instrumentation.py +0 -0
  24. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/models/__init__.py +0 -0
  25. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/models/errors.py +0 -0
  26. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/models/models.py +0 -0
  27. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/models/schemas.py +0 -0
  28. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/py.typed +0 -0
  29. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/__init__.py +0 -0
  30. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/agent.py +0 -0
  31. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/context.py +0 -0
  32. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/errors.py +0 -0
  33. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/logging.py +0 -0
  34. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/session.py +0 -0
  35. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/telemetry.py +0 -0
  36. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/server/utils.py +0 -0
  37. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/src/acp_sdk/version.py +0 -0
  38. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/conftest.py +0 -0
  39. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/e2e/__init__.py +0 -0
  40. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/e2e/config.py +0 -0
  41. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/e2e/fixtures/__init__.py +0 -0
  42. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/e2e/fixtures/client.py +0 -0
  43. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/e2e/test_suites/__init__.py +0 -0
  44. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/unit/models/__init__.py +0 -0
  45. {acp_sdk-0.4.0 → acp_sdk-0.6.0}/tests/unit/models/test_models.py +0 -0
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acp-sdk
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Agent Communication Protocol SDK
5
5
  Author: IBM Corp.
6
6
  Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
7
7
  License-Expression: Apache-2.0
8
8
  Requires-Python: <4.0,>=3.11
9
+ Requires-Dist: cachetools>=5.5.2
9
10
  Requires-Dist: fastapi[standard]>=0.115.8
10
11
  Requires-Dist: httpx-sse>=0.4.0
11
- Requires-Dist: httpx>=0.28.1
12
+ Requires-Dist: httpx>=0.26.0
12
13
  Requires-Dist: janus>=2.0.0
13
14
  Requires-Dist: opentelemetry-api>=1.31.1
14
15
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.31.1
@@ -28,11 +29,12 @@ Agent Communication Protocol SDK for Python provides allows developers to serve
28
29
 
29
30
  ## Installation
30
31
 
31
- Install with:
32
+ Install according to your Python package manager:
32
33
 
33
- ```shell
34
- pip install acp-sdk
35
- ```
34
+ - `uv add acp-sdk`
35
+ - `pip install acp-sdk`
36
+ - `poetry add acp-sdk`
37
+ - ...
36
38
 
37
39
  ## Quickstart
38
40
 
@@ -8,11 +8,12 @@ Agent Communication Protocol SDK for Python provides allows developers to serve
8
8
 
9
9
  ## Installation
10
10
 
11
- Install with:
11
+ Install according to your Python package manager:
12
12
 
13
- ```shell
14
- pip install acp-sdk
15
- ```
13
+ - `uv add acp-sdk`
14
+ - `pip install acp-sdk`
15
+ - `poetry add acp-sdk`
16
+ - ...
16
17
 
17
18
  ## Quickstart
18
19
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acp-sdk"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  description = "Agent Communication Protocol SDK"
5
5
  license = "Apache-2.0"
6
6
  readme = "README.md"
@@ -10,7 +10,7 @@ requires-python = ">=3.11, <4.0"
10
10
  dependencies = [
11
11
  "opentelemetry-api>=1.31.1",
12
12
  "pydantic>=2.11.1",
13
- "httpx>=0.28.1",
13
+ "httpx>=0.26.0",
14
14
  "httpx-sse>=0.4.0",
15
15
  "opentelemetry-instrumentation-httpx>=0.52b1",
16
16
  "fastapi[standard]>=0.115.8",
@@ -18,8 +18,12 @@ dependencies = [
18
18
  "opentelemetry-instrumentation-fastapi>=0.52b1",
19
19
  "opentelemetry-sdk>=1.31.1",
20
20
  "janus>=2.0.0",
21
+ "cachetools>=5.5.2",
21
22
  ]
22
23
 
23
24
  [build-system]
24
25
  requires = ["hatchling"]
25
26
  build-backend = "hatchling.build"
27
+
28
+ [dependency-groups]
29
+ dev = ["pytest-httpx>=0.35.0"]
@@ -1,3 +1,5 @@
1
+ import ssl
2
+ import typing
1
3
  import uuid
2
4
  from collections.abc import AsyncGenerator, AsyncIterator
3
5
  from contextlib import asynccontextmanager
@@ -31,20 +33,58 @@ from acp_sdk.models import (
31
33
  RunResumeResponse,
32
34
  SessionId,
33
35
  )
36
+ from acp_sdk.models.models import MessagePart
37
+
38
+ Input = list[Message] | Message | list[MessagePart] | MessagePart | list[str] | str
34
39
 
35
40
 
36
41
  class Client:
37
42
  def __init__(
38
43
  self,
39
44
  *,
40
- base_url: httpx.URL | str = "",
41
- timeout: httpx.Timeout | None = None,
42
45
  session_id: SessionId | None = None,
43
46
  client: httpx.AsyncClient | None = None,
44
47
  instrument: bool = True,
48
+ auth: httpx._types.AuthTypes | None = None,
49
+ params: httpx._types.QueryParamTypes | None = None,
50
+ headers: httpx._types.HeaderTypes | None = None,
51
+ cookies: httpx._types.CookieTypes | None = None,
52
+ timeout: httpx._types.TimeoutTypes = None,
53
+ verify: ssl.SSLContext | str | bool = True,
54
+ cert: httpx._types.CertTypes | None = None,
55
+ http1: bool = True,
56
+ http2: bool = False,
57
+ proxy: httpx._types.ProxyTypes | None = None,
58
+ mounts: None | (typing.Mapping[str, httpx.AsyncBaseTransport | None]) = None,
59
+ follow_redirects: bool = False,
60
+ limits: httpx.Limits = httpx._config.DEFAULT_LIMITS,
61
+ max_redirects: int = httpx._config.DEFAULT_MAX_REDIRECTS,
62
+ event_hooks: None | (typing.Mapping[str, list[httpx._client.EventHook]]) = None,
63
+ base_url: httpx.URL | str = "",
64
+ transport: httpx.AsyncBaseTransport | None = None,
65
+ trust_env: bool = True,
45
66
  ) -> None:
46
67
  self._session_id = session_id
47
- self._client = client or httpx.AsyncClient(base_url=base_url, timeout=timeout)
68
+ self._client = client or httpx.AsyncClient(
69
+ auth=auth,
70
+ params=params,
71
+ headers=headers,
72
+ cookies=cookies,
73
+ timeout=timeout,
74
+ verify=verify,
75
+ cert=cert,
76
+ http1=http1,
77
+ http2=http2,
78
+ proxy=proxy,
79
+ mounts=mounts,
80
+ follow_redirects=follow_redirects,
81
+ limits=limits,
82
+ max_redirects=max_redirects,
83
+ event_hooks=event_hooks,
84
+ base_url=base_url,
85
+ transport=transport,
86
+ trust_env=trust_env,
87
+ )
48
88
  if instrument:
49
89
  HTTPXClientInstrumentor.instrument_client(self._client)
50
90
 
@@ -79,14 +119,15 @@ class Client:
79
119
  async def agent(self, *, name: AgentName) -> Agent:
80
120
  response = await self._client.get(f"/agents/{name}")
81
121
  self._raise_error(response)
82
- return AgentReadResponse.model_validate(response.json())
122
+ response = AgentReadResponse.model_validate(response.json())
123
+ return Agent(**response.model_dump())
83
124
 
84
- async def run_sync(self, *, agent: AgentName, inputs: list[Message]) -> Run:
125
+ async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
85
126
  response = await self._client.post(
86
127
  "/runs",
87
128
  content=RunCreateRequest(
88
129
  agent_name=agent,
89
- inputs=inputs,
130
+ inputs=self._unify_inputs(input),
90
131
  mode=RunMode.SYNC,
91
132
  session_id=self._session_id,
92
133
  ).model_dump_json(),
@@ -94,14 +135,14 @@ class Client:
94
135
  self._raise_error(response)
95
136
  response = RunCreateResponse.model_validate(response.json())
96
137
  self._set_session(response)
97
- return response
138
+ return Run(**response.model_dump())
98
139
 
99
- async def run_async(self, *, agent: AgentName, inputs: list[Message]) -> Run:
140
+ async def run_async(self, input: Input, *, agent: AgentName) -> Run:
100
141
  response = await self._client.post(
101
142
  "/runs",
102
143
  content=RunCreateRequest(
103
144
  agent_name=agent,
104
- inputs=inputs,
145
+ inputs=self._unify_inputs(input),
105
146
  mode=RunMode.ASYNC,
106
147
  session_id=self._session_id,
107
148
  ).model_dump_json(),
@@ -109,16 +150,16 @@ class Client:
109
150
  self._raise_error(response)
110
151
  response = RunCreateResponse.model_validate(response.json())
111
152
  self._set_session(response)
112
- return response
153
+ return Run(**response.model_dump())
113
154
 
114
- async def run_stream(self, *, agent: AgentName, inputs: list[Message]) -> AsyncIterator[Event]:
155
+ async def run_stream(self, input: Input, *, agent: AgentName) -> AsyncIterator[Event]:
115
156
  async with aconnect_sse(
116
157
  self._client,
117
158
  "POST",
118
159
  "/runs",
119
160
  content=RunCreateRequest(
120
161
  agent_name=agent,
121
- inputs=inputs,
162
+ inputs=self._unify_inputs(input),
122
163
  mode=RunMode.STREAM,
123
164
  session_id=self._session_id,
124
165
  ).model_dump_json(),
@@ -136,25 +177,28 @@ class Client:
136
177
  async def run_cancel(self, *, run_id: RunId) -> Run:
137
178
  response = await self._client.post(f"/runs/{run_id}/cancel")
138
179
  self._raise_error(response)
139
- return RunCancelResponse.model_validate(response.json())
180
+ response = RunCancelResponse.model_validate(response.json())
181
+ return Run(**response.model_dump())
140
182
 
141
- async def run_resume_sync(self, *, run_id: RunId, await_resume: AwaitResume) -> Run:
183
+ async def run_resume_sync(self, await_resume: AwaitResume, *, run_id: RunId) -> Run:
142
184
  response = await self._client.post(
143
185
  f"/runs/{run_id}",
144
186
  content=RunResumeRequest(await_resume=await_resume, mode=RunMode.SYNC).model_dump_json(),
145
187
  )
146
188
  self._raise_error(response)
147
- return RunResumeResponse.model_validate(response.json())
189
+ response = RunResumeResponse.model_validate(response.json())
190
+ return Run(**response.model_dump())
148
191
 
149
- async def run_resume_async(self, *, run_id: RunId, await_resume: AwaitResume) -> Run:
192
+ async def run_resume_async(self, await_resume: AwaitResume, *, run_id: RunId) -> Run:
150
193
  response = await self._client.post(
151
194
  f"/runs/{run_id}",
152
195
  content=RunResumeRequest(await_resume=await_resume, mode=RunMode.ASYNC).model_dump_json(),
153
196
  )
154
197
  self._raise_error(response)
155
- return RunResumeResponse.model_validate(response.json())
198
+ response = RunResumeResponse.model_validate(response.json())
199
+ return Run(**response.model_dump())
156
200
 
157
- async def run_resume_stream(self, *, run_id: RunId, await_resume: AwaitResume) -> AsyncIterator[Event]:
201
+ async def run_resume_stream(self, await_resume: AwaitResume, *, run_id: RunId) -> AsyncIterator[Event]:
158
202
  async with aconnect_sse(
159
203
  self._client,
160
204
  "POST",
@@ -183,3 +227,24 @@ class Client:
183
227
 
184
228
  def _set_session(self, run: Run) -> None:
185
229
  self._session_id = run.session_id
230
+
231
+ def _unify_inputs(self, input: Input) -> list[Message]:
232
+ if isinstance(input, list):
233
+ if len(input) == 0:
234
+ return []
235
+ if all(isinstance(item, Message) for item in input):
236
+ return input
237
+ elif all(isinstance(item, MessagePart) for item in input):
238
+ return [Message(parts=input)]
239
+ elif all(isinstance(item, str) for item in input):
240
+ return [Message(parts=[MessagePart(content=content) for content in input])]
241
+ else:
242
+ raise RuntimeError("List with mixed types is not supported")
243
+ else:
244
+ if isinstance(input, str):
245
+ input = MessagePart(content=input)
246
+ if isinstance(input, MessagePart):
247
+ input = Message(parts=[input])
248
+ if isinstance(input, Message):
249
+ input = [input]
250
+ return input
@@ -1,8 +1,10 @@
1
1
  from collections.abc import AsyncGenerator
2
2
  from concurrent.futures import ThreadPoolExecutor
3
3
  from contextlib import asynccontextmanager
4
+ from datetime import datetime, timedelta
4
5
  from enum import Enum
5
6
 
7
+ from cachetools import TTLCache
6
8
  from fastapi import FastAPI, HTTPException, status
7
9
  from fastapi.encoders import jsonable_encoder
8
10
  from fastapi.responses import JSONResponse, StreamingResponse
@@ -45,7 +47,7 @@ class Headers(str, Enum):
45
47
  RUN_ID = "Run-ID"
46
48
 
47
49
 
48
- def create_app(*agents: Agent) -> FastAPI:
50
+ def create_app(*agents: Agent, run_limit: int = 1000, run_ttl: timedelta = timedelta(hours=1)) -> FastAPI:
49
51
  executor: ThreadPoolExecutor
50
52
 
51
53
  @asynccontextmanager
@@ -60,8 +62,8 @@ def create_app(*agents: Agent) -> FastAPI:
60
62
  FastAPIInstrumentor.instrument_app(app)
61
63
 
62
64
  agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
63
- runs: dict[RunId, RunBundle] = {}
64
- sessions: dict[SessionId, Session] = {}
65
+ runs: TTLCache[RunId, RunBundle] = TTLCache(maxsize=run_limit, ttl=run_ttl, timer=datetime.now)
66
+ sessions: TTLCache[SessionId, Session] = TTLCache(maxsize=run_limit, ttl=run_ttl, timer=datetime.now)
65
67
 
66
68
  app.exception_handler(ACPError)(acp_error_handler)
67
69
  app.exception_handler(StarletteHTTPException)(http_exception_handler)
@@ -3,7 +3,7 @@ import logging
3
3
  from collections.abc import AsyncGenerator
4
4
  from concurrent.futures import ThreadPoolExecutor
5
5
 
6
- from pydantic import ValidationError
6
+ from pydantic import BaseModel, ValidationError
7
7
 
8
8
  from acp_sdk.instrumentation import get_tracer
9
9
  from acp_sdk.models import (
@@ -88,6 +88,13 @@ class RunBundle:
88
88
  run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
89
89
 
90
90
  in_message = False
91
+
92
+ async def flush_message() -> None:
93
+ nonlocal in_message
94
+ if in_message:
95
+ await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
96
+ in_message = False
97
+
91
98
  try:
92
99
  await self.emit(RunCreatedEvent(run=self.run))
93
100
 
@@ -103,7 +110,9 @@ class RunBundle:
103
110
  while True:
104
111
  next = await generator.asend(await_resume)
105
112
 
106
- if isinstance(next, MessagePart):
113
+ if isinstance(next, (MessagePart, str)):
114
+ if isinstance(next, str):
115
+ next = MessagePart(content=next)
107
116
  if not in_message:
108
117
  self.run.outputs.append(Message(parts=[]))
109
118
  in_message = True
@@ -111,9 +120,7 @@ class RunBundle:
111
120
  self.run.outputs[-1].parts.append(next)
112
121
  await self.emit(MessagePartEvent(part=next))
113
122
  elif isinstance(next, Message):
114
- if in_message:
115
- await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
116
- in_message = False
123
+ await flush_message()
117
124
  self.run.outputs.append(next)
118
125
  await self.emit(MessageCreatedEvent(message=next))
119
126
  for part in next.parts:
@@ -130,7 +137,9 @@ class RunBundle:
130
137
  elif isinstance(next, Error):
131
138
  raise ACPError(error=next)
132
139
  elif next is None:
133
- pass # Do nothing
140
+ await flush_message()
141
+ elif isinstance(next, BaseModel):
142
+ await self.emit(GenericEvent(generic=AnyModel(**next.model_dump())))
134
143
  else:
135
144
  try:
136
145
  generic = AnyModel.model_validate(next)
@@ -138,8 +147,7 @@ class RunBundle:
138
147
  except ValidationError:
139
148
  raise TypeError("Invalid yield")
140
149
  except StopAsyncIteration:
141
- if in_message:
142
- await self.emit(MessageCompletedEvent(message=self.run.outputs[-1]))
150
+ await flush_message()
143
151
  self.run.status = RunStatus.COMPLETED
144
152
  await self.emit(RunCompletedEvent(run=self.run))
145
153
  run_logger.info("Run completed")
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import os
3
3
  from collections.abc import Awaitable
4
+ from datetime import timedelta
4
5
  from typing import Any, Callable
5
6
 
6
7
  import uvicorn
@@ -42,6 +43,8 @@ class Server:
42
43
  self,
43
44
  configure_logger: bool = True,
44
45
  configure_telemetry: bool = False,
46
+ run_limit: int = 1000,
47
+ run_ttl: timedelta = timedelta(hours=1),
45
48
  host: str = "127.0.0.1",
46
49
  port: int = 8000,
47
50
  uds: str | None = None,
@@ -105,7 +108,7 @@ class Server:
105
108
  configure_telemetry_func()
106
109
 
107
110
  config = uvicorn.Config(
108
- create_app(*self._agents),
111
+ create_app(*self._agents, run_limit=run_limit, run_ttl=run_ttl),
109
112
  host,
110
113
  port,
111
114
  uds,
@@ -0,0 +1,9 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from acp_sdk.models import AwaitRequest, AwaitResume, Message
6
+ from acp_sdk.models.models import MessagePart
7
+
8
+ RunYield = Message | MessagePart | str | AwaitRequest | BaseModel | dict[str | Any] | None
9
+ RunYieldResume = AwaitResume | None
@@ -1,6 +1,7 @@
1
1
  import base64
2
2
  import time
3
3
  from collections.abc import AsyncGenerator, AsyncIterator, Generator
4
+ from datetime import timedelta
4
5
  from threading import Thread
5
6
 
6
7
  import pytest
@@ -10,8 +11,9 @@ from acp_sdk.server import Context, Server
10
11
  from e2e.config import Config
11
12
 
12
13
 
13
- @pytest.fixture(scope="module")
14
- def server() -> Generator[None]:
14
+ @pytest.fixture(scope="module", params=[timedelta(minutes=1)])
15
+ def server(request: pytest.FixtureRequest) -> Generator[None]:
16
+ ttl = request.param
15
17
  server = Server()
16
18
 
17
19
  @server.agent()
@@ -74,7 +76,7 @@ def server() -> Generator[None]:
74
76
  content_encoding="base64",
75
77
  )
76
78
 
77
- thread = Thread(target=server.run, kwargs={"port": Config.PORT}, daemon=True)
79
+ thread = Thread(target=server.run, kwargs={"run_ttl": ttl, "port": Config.PORT}, daemon=True)
78
80
  thread.start()
79
81
 
80
82
  time.sleep(1)
@@ -1,4 +1,6 @@
1
+ import asyncio
1
2
  import base64
3
+ from datetime import timedelta
2
4
 
3
5
  import pytest
4
6
  from acp_sdk.client import Client
@@ -14,6 +16,7 @@ from acp_sdk.models import (
14
16
  RunInProgressEvent,
15
17
  RunStatus,
16
18
  )
19
+ from acp_sdk.models.errors import ACPError
17
20
  from acp_sdk.server import Server
18
21
 
19
22
  inputs = [Message(parts=[MessagePart(content="Hello!")])]
@@ -22,27 +25,27 @@ await_resume = MessageAwaitResume(message=Message(parts=[]))
22
25
 
23
26
  @pytest.mark.asyncio
24
27
  async def test_run_sync(server: Server, client: Client) -> None:
25
- run = await client.run_sync(agent="echo", inputs=inputs)
28
+ run = await client.run_sync(agent="echo", input=inputs)
26
29
  assert run.status == RunStatus.COMPLETED
27
30
  assert run.outputs == inputs
28
31
 
29
32
 
30
33
  @pytest.mark.asyncio
31
34
  async def test_run_async(server: Server, client: Client) -> None:
32
- run = await client.run_async(agent="echo", inputs=inputs)
35
+ run = await client.run_async(agent="echo", input=inputs)
33
36
  assert run.status == RunStatus.CREATED
34
37
 
35
38
 
36
39
  @pytest.mark.asyncio
37
40
  async def test_run_stream(server: Server, client: Client) -> None:
38
- event_stream = [event async for event in client.run_stream(agent="echo", inputs=inputs)]
41
+ event_stream = [event async for event in client.run_stream(agent="echo", input=inputs)]
39
42
  assert isinstance(event_stream[0], RunCreatedEvent)
40
43
  assert isinstance(event_stream[-1], RunCompletedEvent)
41
44
 
42
45
 
43
46
  @pytest.mark.asyncio
44
47
  async def test_run_status(server: Server, client: Client) -> None:
45
- run = await client.run_async(agent="echo", inputs=inputs)
48
+ run = await client.run_async(agent="echo", input=inputs)
46
49
  while run.status in (RunStatus.CREATED, RunStatus.IN_PROGRESS):
47
50
  run = await client.run_status(run_id=run.run_id)
48
51
  assert run.status == RunStatus.COMPLETED
@@ -50,7 +53,7 @@ async def test_run_status(server: Server, client: Client) -> None:
50
53
 
51
54
  @pytest.mark.asyncio
52
55
  async def test_failure(server: Server, client: Client) -> None:
53
- run = await client.run_sync(agent="failer", inputs=inputs)
56
+ run = await client.run_sync(agent="failer", input=inputs)
54
57
  assert run.status == RunStatus.FAILED
55
58
  assert run.error is not None
56
59
  assert run.error.code == ErrorCode.INVALID_INPUT
@@ -58,7 +61,7 @@ async def test_failure(server: Server, client: Client) -> None:
58
61
 
59
62
  @pytest.mark.asyncio
60
63
  async def test_run_cancel(server: Server, client: Client) -> None:
61
- run = await client.run_sync(agent="awaiter", inputs=inputs)
64
+ run = await client.run_sync(agent="awaiter", input=inputs)
62
65
  assert run.status == RunStatus.AWAITING
63
66
  run = await client.run_cancel(run_id=run.run_id)
64
67
  assert run.status == RunStatus.CANCELLING
@@ -66,7 +69,7 @@ async def test_run_cancel(server: Server, client: Client) -> None:
66
69
 
67
70
  @pytest.mark.asyncio
68
71
  async def test_run_resume_sync(server: Server, client: Client) -> None:
69
- run = await client.run_sync(agent="awaiter", inputs=inputs)
72
+ run = await client.run_sync(agent="awaiter", input=inputs)
70
73
  assert run.status == RunStatus.AWAITING
71
74
  assert run.await_request is not None
72
75
 
@@ -76,7 +79,7 @@ async def test_run_resume_sync(server: Server, client: Client) -> None:
76
79
 
77
80
  @pytest.mark.asyncio
78
81
  async def test_run_resume_async(server: Server, client: Client) -> None:
79
- run = await client.run_sync(agent="awaiter", inputs=inputs)
82
+ run = await client.run_sync(agent="awaiter", input=inputs)
80
83
  assert run.status == RunStatus.AWAITING
81
84
  assert run.await_request is not None
82
85
 
@@ -86,7 +89,7 @@ async def test_run_resume_async(server: Server, client: Client) -> None:
86
89
 
87
90
  @pytest.mark.asyncio
88
91
  async def test_run_resume_stream(server: Server, client: Client) -> None:
89
- run = await client.run_sync(agent="awaiter", inputs=inputs)
92
+ run = await client.run_sync(agent="awaiter", input=inputs)
90
93
  assert run.status == RunStatus.AWAITING
91
94
  assert run.await_request is not None
92
95
 
@@ -98,15 +101,15 @@ async def test_run_resume_stream(server: Server, client: Client) -> None:
98
101
  @pytest.mark.asyncio
99
102
  async def test_run_session(server: Server, client: Client) -> None:
100
103
  async with client.session() as session:
101
- run = await session.run_sync(agent="echo", inputs=inputs)
104
+ run = await session.run_sync(agent="echo", input=inputs)
102
105
  assert run.outputs == inputs
103
- run = await session.run_sync(agent="echo", inputs=inputs)
106
+ run = await session.run_sync(agent="echo", input=inputs)
104
107
  assert run.outputs == inputs + inputs + inputs
105
108
 
106
109
 
107
110
  @pytest.mark.asyncio
108
111
  async def test_mime_types(server: Server, client: Client) -> None:
109
- run = await client.run_sync(agent="mime_types", inputs=inputs)
112
+ run = await client.run_sync(agent="mime_types", input=inputs)
110
113
  assert run.status == RunStatus.COMPLETED
111
114
  assert len(run.outputs) == 1
112
115
 
@@ -127,7 +130,7 @@ async def test_mime_types(server: Server, client: Client) -> None:
127
130
 
128
131
  @pytest.mark.asyncio
129
132
  async def test_base64_encoding(server: Server, client: Client) -> None:
130
- run = await client.run_sync(agent="base64_encoding", inputs=inputs)
133
+ run = await client.run_sync(agent="base64_encoding", input=inputs)
131
134
  assert run.status == RunStatus.COMPLETED
132
135
  assert len(run.outputs) == 1
133
136
 
@@ -147,7 +150,7 @@ async def test_base64_encoding(server: Server, client: Client) -> None:
147
150
 
148
151
  @pytest.mark.asyncio
149
152
  async def test_artifacts(server: Server, client: Client) -> None:
150
- run = await client.run_sync(agent="artifact_producer", inputs=inputs)
153
+ run = await client.run_sync(agent="artifact_producer", input=inputs)
151
154
  assert run.status == RunStatus.COMPLETED
152
155
 
153
156
  assert len(run.outputs) == 1
@@ -176,7 +179,7 @@ async def test_artifacts(server: Server, client: Client) -> None:
176
179
 
177
180
  @pytest.mark.asyncio
178
181
  async def test_artifact_streaming(server: Server, client: Client) -> None:
179
- events = [event async for event in client.run_stream(agent="artifact_producer", inputs=inputs)]
182
+ events = [event async for event in client.run_stream(agent="artifact_producer", input=inputs)]
180
183
 
181
184
  assert isinstance(events[0], RunCreatedEvent)
182
185
  assert isinstance(events[-1], RunCompletedEvent)
@@ -193,3 +196,32 @@ async def test_artifact_streaming(server: Server, client: Client) -> None:
193
196
  assert "text/plain" in artifact_types
194
197
  assert "application/json" in artifact_types
195
198
  assert "image/png" in artifact_types
199
+
200
+
201
+ @pytest.mark.asyncio
202
+ @pytest.mark.parametrize("server", [timedelta(seconds=5)], indirect=True)
203
+ async def test_run_ttl(server: Server, client: Client) -> None:
204
+ run = await client.run_async(agent="echo", input=inputs)
205
+ run = await client.run_status(run_id=run.run_id)
206
+ await asyncio.sleep(6)
207
+ try:
208
+ run = await client.run_status(run_id=run.run_id)
209
+ raise AssertionError("Error expected")
210
+ except ACPError as e:
211
+ if e.error.code == ErrorCode.NOT_FOUND:
212
+ assert True
213
+ else:
214
+ raise AssertionError(f"Unexpected error code {e.error.code}")
215
+
216
+
217
+ @pytest.mark.asyncio
218
+ @pytest.mark.parametrize("server", [timedelta(seconds=5)], indirect=True)
219
+ async def test_session_ttl(server: Server, client: Client) -> None:
220
+ async with client.session() as session:
221
+ run = await session.run_sync(agent="echo", input=inputs)
222
+ await asyncio.sleep(3)
223
+ run = await session.run_sync(agent="echo", input=inputs)
224
+ assert len(run.outputs) == 3
225
+ await asyncio.sleep(3)
226
+ run = await session.run_sync(agent="echo", input=inputs)
227
+ assert len(run.outputs) == 7 # First run shall be forgotten
@@ -0,0 +1,36 @@
1
+ import pytest
2
+ from acp_sdk.client import Client
3
+ from acp_sdk.models import Message, MessagePart, Run, RunCompletedEvent
4
+ from pytest_httpx import HTTPXMock
5
+
6
+ mock_run = Run(agent_name="mock", outputs=[Message(parts=[MessagePart(content="Hello!")])])
7
+
8
+
9
+ @pytest.mark.asyncio
10
+ async def test_run_sync(httpx_mock: HTTPXMock) -> None:
11
+ httpx_mock.add_response(content=mock_run.model_dump_json())
12
+
13
+ async with Client(base_url="http://localhost:8000") as client:
14
+ run = await client.run_sync("Howdy!", agent="mock")
15
+ assert run == mock_run
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_run_async(httpx_mock: HTTPXMock) -> None:
20
+ httpx_mock.add_response(content=mock_run.model_dump_json())
21
+
22
+ async with Client(base_url="http://localhost:8000") as client:
23
+ run = await client.run_async("Howdy!", agent="mock")
24
+ assert run == mock_run
25
+
26
+
27
+ @pytest.mark.asyncio
28
+ async def test_run_stream(httpx_mock: HTTPXMock) -> None:
29
+ mock_event = RunCompletedEvent(run=mock_run)
30
+ httpx_mock.add_response(
31
+ headers={"content-type": "text/event-stream"}, content=f"data: {mock_event.model_dump_json()}\n\n"
32
+ )
33
+
34
+ async with Client(base_url="http://localhost:8000") as client:
35
+ async for event in client.run_stream("Howdy!", agent="mock"):
36
+ assert event == mock_event
@@ -1,6 +0,0 @@
1
- from typing import Any
2
-
3
- from acp_sdk.models import AwaitRequest, AwaitResume, Message
4
-
5
- RunYield = Message | AwaitRequest | dict[str | Any] | None
6
- RunYieldResume = AwaitResume | None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes