acp-sdk 0.7.3__tar.gz → 0.8.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 (48) hide show
  1. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/PKG-INFO +1 -1
  2. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/pyproject.toml +1 -1
  3. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/client/client.py +12 -2
  4. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/models/models.py +11 -3
  5. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/models/schemas.py +9 -1
  6. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/app.py +10 -3
  7. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/bundle.py +12 -3
  8. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/e2e/test_suites/test_discovery.py +6 -0
  9. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/e2e/test_suites/test_runs.py +22 -4
  10. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/unit/client/test_client.py +24 -2
  11. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/unit/client/test_utils.py +2 -1
  12. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/unit/models/test_models.py +29 -7
  13. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/.gitignore +0 -0
  14. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/.python-version +0 -0
  15. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/README.md +0 -0
  16. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/docs/_sidebar.md +0 -0
  17. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/docs/client.md +0 -0
  18. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/docs/index.html +0 -0
  19. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/docs/models.md +0 -0
  20. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/docs/server.md +0 -0
  21. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/pytest.ini +0 -0
  22. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/__init__.py +0 -0
  23. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/client/__init__.py +0 -0
  24. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/client/types.py +0 -0
  25. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/client/utils.py +0 -0
  26. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/instrumentation.py +0 -0
  27. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/models/__init__.py +0 -0
  28. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/models/errors.py +0 -0
  29. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/py.typed +0 -0
  30. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/__init__.py +0 -0
  31. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/agent.py +0 -0
  32. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/context.py +0 -0
  33. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/errors.py +0 -0
  34. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/logging.py +0 -0
  35. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/server.py +0 -0
  36. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/session.py +0 -0
  37. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/telemetry.py +0 -0
  38. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/types.py +0 -0
  39. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/server/utils.py +0 -0
  40. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/src/acp_sdk/version.py +0 -0
  41. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/conftest.py +0 -0
  42. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/e2e/__init__.py +0 -0
  43. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/e2e/config.py +0 -0
  44. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/e2e/fixtures/__init__.py +0 -0
  45. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/e2e/fixtures/client.py +0 -0
  46. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/e2e/fixtures/server.py +0 -0
  47. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/e2e/test_suites/__init__.py +0 -0
  48. {acp_sdk-0.7.3 → acp_sdk-0.8.0}/tests/unit/models/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acp-sdk
3
- Version: 0.7.3
3
+ Version: 0.8.0
4
4
  Summary: Agent Communication Protocol SDK
5
5
  Author: IBM Corp.
6
6
  Maintainer-email: Tomas Pilar <thomas7pilar@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acp-sdk"
3
- version = "0.7.3"
3
+ version = "0.8.0"
4
4
  description = "Agent Communication Protocol SDK"
5
5
  license = "Apache-2.0"
6
6
  readme = "README.md"
@@ -23,10 +23,12 @@ from acp_sdk.models import (
23
23
  AwaitResume,
24
24
  Error,
25
25
  Event,
26
+ PingResponse,
26
27
  Run,
27
28
  RunCancelResponse,
28
29
  RunCreateRequest,
29
30
  RunCreateResponse,
31
+ RunEventsListResponse,
30
32
  RunId,
31
33
  RunMode,
32
34
  RunResumeRequest,
@@ -120,9 +122,10 @@ class Client:
120
122
  return Agent(**response.model_dump())
121
123
 
122
124
  async def ping(self) -> bool:
123
- response = await self._client.get("/healthcheck")
125
+ response = await self._client.get("/ping")
124
126
  self._raise_error(response)
125
- return response.json() == "OK"
127
+ PingResponse.model_validate(response.json())
128
+ return
126
129
 
127
130
  async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
128
131
  response = await self._client.post(
@@ -172,6 +175,13 @@ class Client:
172
175
  self._raise_error(response)
173
176
  return Run.model_validate(response.json())
174
177
 
178
+ async def run_events(self, *, run_id: RunId) -> AsyncIterator[Event]:
179
+ response = await self._client.get(f"/runs/{run_id}/events")
180
+ self._raise_error(response)
181
+ response = RunEventsListResponse.model_validate(response.json())
182
+ for event in response.events:
183
+ yield event
184
+
175
185
  async def run_cancel(self, *, run_id: RunId) -> Run:
176
186
  response = await self._client.post(f"/runs/{run_id}/cancel")
177
187
  self._raise_error(response)
@@ -1,5 +1,5 @@
1
1
  import uuid
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
3
  from enum import Enum
4
4
  from typing import Any, Literal, Optional, Union
5
5
 
@@ -95,11 +95,17 @@ class Artifact(MessagePart):
95
95
 
96
96
  class Message(BaseModel):
97
97
  parts: list[MessagePart]
98
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
99
+ completed_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc))
98
100
 
99
101
  def __add__(self, other: "Message") -> "Message":
100
102
  if not isinstance(other, Message):
101
103
  raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
102
- return Message(parts=self.parts + other.parts)
104
+ return Message(
105
+ parts=self.parts + other.parts,
106
+ created_at=min(self.created_at, other.created_at),
107
+ completed_at=max(self.completed_at, other.completed_at),
108
+ )
103
109
 
104
110
  def __str__(self) -> str:
105
111
  return "".join(
@@ -134,7 +140,7 @@ class Message(BaseModel):
134
140
  parts[-1] = join(parts[-1], part)
135
141
  else:
136
142
  parts.append(part)
137
- return Message(parts=parts)
143
+ return Message(parts=parts, created_at=self.created_at, completed_at=self.completed_at)
138
144
 
139
145
 
140
146
  AgentName = str
@@ -185,6 +191,8 @@ class Run(BaseModel):
185
191
  await_request: AwaitRequest | None = None
186
192
  output: list[Message] = []
187
193
  error: Error | None = None
194
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
195
+ finished_at: datetime | None = None
188
196
 
189
197
 
190
198
  class MessageCreatedEvent(BaseModel):
@@ -1,6 +1,10 @@
1
1
  from pydantic import BaseModel
2
2
 
3
- from acp_sdk.models.models import Agent, AgentName, AwaitResume, Message, Run, RunMode, SessionId
3
+ from acp_sdk.models.models import Agent, AgentName, AwaitResume, Event, Message, Run, RunMode, SessionId
4
+
5
+
6
+ class PingResponse(BaseModel):
7
+ pass
4
8
 
5
9
 
6
10
  class AgentsListResponse(BaseModel):
@@ -37,3 +41,7 @@ class RunReadResponse(Run):
37
41
 
38
42
  class RunCancelResponse(Run):
39
43
  pass
44
+
45
+
46
+ class RunEventsListResponse(BaseModel):
47
+ events: list[Event]
@@ -21,6 +21,7 @@ from acp_sdk.models import (
21
21
  RunCancelResponse,
22
22
  RunCreateRequest,
23
23
  RunCreateResponse,
24
+ RunEventsListResponse,
24
25
  RunId,
25
26
  RunMode,
26
27
  RunReadResponse,
@@ -29,6 +30,7 @@ from acp_sdk.models import (
29
30
  SessionId,
30
31
  )
31
32
  from acp_sdk.models.errors import ACPError
33
+ from acp_sdk.models.schemas import PingResponse
32
34
  from acp_sdk.server.agent import Agent
33
35
  from acp_sdk.server.bundle import RunBundle
34
36
  from acp_sdk.server.errors import (
@@ -104,9 +106,9 @@ def create_app(
104
106
  agent = find_agent(name)
105
107
  return AgentModel(name=agent.name, description=agent.description, metadata=agent.metadata)
106
108
 
107
- @app.get("/healthcheck")
108
- async def healthcheck() -> str:
109
- return "OK"
109
+ @app.get("/ping")
110
+ async def ping() -> PingResponse:
111
+ return PingResponse()
110
112
 
111
113
  @app.post("/runs")
112
114
  async def create_run(request: RunCreateRequest) -> RunCreateResponse:
@@ -155,6 +157,11 @@ def create_app(
155
157
  bundle = find_run_bundle(run_id)
156
158
  return bundle.run
157
159
 
160
+ @app.get("/runs/{run_id}/events")
161
+ async def list_run_events(run_id: RunId) -> RunEventsListResponse:
162
+ bundle = find_run_bundle(run_id)
163
+ return RunEventsListResponse(events=bundle.events)
164
+
158
165
  @app.post("/runs/{run_id}")
159
166
  async def resume_run(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
160
167
  bundle = find_run_bundle(run_id)
@@ -2,6 +2,7 @@ import asyncio
2
2
  import logging
3
3
  from collections.abc import AsyncGenerator
4
4
  from concurrent.futures import ThreadPoolExecutor
5
+ from datetime import datetime, timezone
5
6
 
6
7
  from pydantic import BaseModel, ValidationError
7
8
 
@@ -43,6 +44,7 @@ class RunBundle:
43
44
  self.history = history
44
45
 
45
46
  self.stream_queue: asyncio.Queue[Event] = asyncio.Queue()
47
+ self.events: list[Event] = []
46
48
 
47
49
  self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
48
50
  self.await_or_terminate_event = asyncio.Event()
@@ -58,7 +60,9 @@ class RunBundle:
58
60
  self.stream_queue.task_done()
59
61
 
60
62
  async def emit(self, event: Event) -> None:
61
- await self.stream_queue.put(event)
63
+ freeze = event.model_copy(deep=True)
64
+ self.events.append(freeze)
65
+ await self.stream_queue.put(freeze)
62
66
 
63
67
  async def await_(self) -> AwaitResume:
64
68
  await self.stream_queue.put(None)
@@ -92,7 +96,9 @@ class RunBundle:
92
96
  async def flush_message() -> None:
93
97
  nonlocal in_message
94
98
  if in_message:
95
- await self.emit(MessageCompletedEvent(message=self.run.output[-1]))
99
+ message = self.run.output[-1]
100
+ message.completed_at = datetime.now(timezone.utc)
101
+ await self.emit(MessageCompletedEvent(message=message))
96
102
  in_message = False
97
103
 
98
104
  try:
@@ -114,7 +120,7 @@ class RunBundle:
114
120
  if isinstance(next, str):
115
121
  next = MessagePart(content=next)
116
122
  if not in_message:
117
- self.run.output.append(Message(parts=[]))
123
+ self.run.output.append(Message(parts=[], completed_at=None))
118
124
  in_message = True
119
125
  await self.emit(MessageCreatedEvent(message=self.run.output[-1]))
120
126
  self.run.output[-1].parts.append(next)
@@ -149,10 +155,12 @@ class RunBundle:
149
155
  except StopAsyncIteration:
150
156
  await flush_message()
151
157
  self.run.status = RunStatus.COMPLETED
158
+ self.run.finished_at = datetime.now(timezone.utc)
152
159
  await self.emit(RunCompletedEvent(run=self.run))
153
160
  run_logger.info("Run completed")
154
161
  except asyncio.CancelledError:
155
162
  self.run.status = RunStatus.CANCELLED
163
+ self.run.finished_at = datetime.now(timezone.utc)
156
164
  await self.emit(RunCancelledEvent(run=self.run))
157
165
  run_logger.info("Run cancelled")
158
166
  except Exception as e:
@@ -161,6 +169,7 @@ class RunBundle:
161
169
  else:
162
170
  self.run.error = Error(code=ErrorCode.SERVER_ERROR, message=str(e))
163
171
  self.run.status = RunStatus.FAILED
172
+ self.run.finished_at = datetime.now(timezone.utc)
164
173
  await self.emit(RunFailedEvent(run=self.run))
165
174
  run_logger.exception("Run failed")
166
175
  raise
@@ -4,6 +4,12 @@ from acp_sdk.models import Agent
4
4
  from acp_sdk.server import Server
5
5
 
6
6
 
7
+ @pytest.mark.asyncio
8
+ async def test_ping(server: Server, client: Client) -> None:
9
+ await client.ping()
10
+ assert True
11
+
12
+
7
13
  @pytest.mark.asyncio
8
14
  async def test_agents_list(server: Server, client: Client) -> None:
9
15
  async for agent in client.agents():
@@ -9,8 +9,8 @@ from acp_sdk.models import (
9
9
  ErrorCode,
10
10
  Message,
11
11
  MessageAwaitResume,
12
- MessageCreatedEvent,
13
12
  MessagePart,
13
+ MessagePartEvent,
14
14
  RunCompletedEvent,
15
15
  RunCreatedEvent,
16
16
  RunInProgressEvent,
@@ -51,6 +51,24 @@ async def test_run_status(server: Server, client: Client) -> None:
51
51
  assert run.status == RunStatus.COMPLETED
52
52
 
53
53
 
54
+ @pytest.mark.asyncio
55
+ async def test_run_events(server: Server, client: Client) -> None:
56
+ run = await client.run_sync(agent="echo", input=input)
57
+ events = [event async for event in client.run_events(run_id=run.run_id)]
58
+ assert isinstance(events[0], RunCreatedEvent)
59
+ assert isinstance(events[-1], RunCompletedEvent)
60
+
61
+
62
+ @pytest.mark.asyncio
63
+ async def test_run_events_are_stream(server: Server, client: Client) -> None:
64
+ stream = [event async for event in client.run_stream(agent="echo", input=input)]
65
+ print(stream)
66
+ assert isinstance(stream[0], RunCreatedEvent)
67
+ events = [event async for event in client.run_events(run_id=stream[0].run.run_id)]
68
+ print(events)
69
+ assert stream == events
70
+
71
+
54
72
  @pytest.mark.asyncio
55
73
  async def test_failure(server: Server, client: Client) -> None:
56
74
  run = await client.run_sync(agent="failer", input=input)
@@ -184,11 +202,11 @@ async def test_artifact_streaming(server: Server, client: Client) -> None:
184
202
  assert isinstance(events[0], RunCreatedEvent)
185
203
  assert isinstance(events[-1], RunCompletedEvent)
186
204
 
187
- message_events = [e for e in events if isinstance(e, MessageCreatedEvent)]
205
+ message_part_events = [e for e in events if isinstance(e, MessagePartEvent)]
188
206
  artifact_events = [e for e in events if isinstance(e, ArtifactEvent)]
189
207
 
190
- assert len(message_events) == 1
191
- assert message_events[0].message.parts[0].content == "Processing with artifacts"
208
+ assert len(message_part_events) == 1
209
+ assert message_part_events[0].part.content == "Processing with artifacts"
192
210
 
193
211
  assert len(artifact_events) == 3
194
212
 
@@ -3,8 +3,16 @@ import uuid
3
3
 
4
4
  import pytest
5
5
  from acp_sdk.client import Client
6
- from acp_sdk.models import Agent, AgentsListResponse, Message, MessagePart, Run, RunCompletedEvent
7
- from acp_sdk.models.models import MessageAwaitResume
6
+ from acp_sdk.models import (
7
+ Agent,
8
+ AgentsListResponse,
9
+ Message,
10
+ MessageAwaitResume,
11
+ MessagePart,
12
+ Run,
13
+ RunCompletedEvent,
14
+ RunEventsListResponse,
15
+ )
8
16
  from pytest_httpx import HTTPXMock
9
17
 
10
18
  mock_agent = Agent(name="mock")
@@ -128,6 +136,20 @@ async def test_run_resume_stream(httpx_mock: HTTPXMock) -> None:
128
136
  assert event == mock_event
129
137
 
130
138
 
139
+ @pytest.mark.asyncio
140
+ async def test_run_events(httpx_mock: HTTPXMock) -> None:
141
+ mock_event = RunCompletedEvent(run=mock_run)
142
+ httpx_mock.add_response(
143
+ url=f"http://test/runs/{mock_run.run_id}/events",
144
+ method="GET",
145
+ content=RunEventsListResponse(events=[mock_event]).model_dump_json(),
146
+ )
147
+
148
+ async with Client(base_url="http://test") as client:
149
+ async for event in client.run_events(run_id=mock_run.run_id):
150
+ assert event == mock_event
151
+
152
+
131
153
  @pytest.mark.asyncio
132
154
  async def test_session(httpx_mock: HTTPXMock) -> None:
133
155
  httpx_mock.add_response(url="http://test/runs", method="POST", content=mock_run.model_dump_json(), is_reusable=True)
@@ -18,7 +18,8 @@ from acp_sdk.models import Message, MessagePart
18
18
  )
19
19
  def test_input_to_messages(input: Input, messages: list[Message]) -> None:
20
20
  result = input_to_messages(input)
21
- assert result == messages
21
+ for r, m in zip(result, messages):
22
+ assert r.parts == m.parts
22
23
 
23
24
 
24
25
  @pytest.mark.parametrize(
@@ -1,18 +1,30 @@
1
1
  import pytest
2
2
  from acp_sdk.models.models import Message, MessagePart
3
3
 
4
+ timestamp = "2021-09-09T22:02:47.89Z"
5
+
4
6
 
5
7
  @pytest.mark.parametrize(
6
8
  "first,second,result",
7
9
  [
8
10
  (
9
- Message(parts=[MessagePart(content_type="text/plain", content="Foo")]),
10
- Message(parts=[MessagePart(content_type="text/plain", content="Bar")]),
11
+ Message(
12
+ parts=[MessagePart(content_type="text/plain", content="Foo")],
13
+ created_at=timestamp,
14
+ completed_at=timestamp,
15
+ ),
16
+ Message(
17
+ parts=[MessagePart(content_type="text/plain", content="Bar")],
18
+ created_at=timestamp,
19
+ completed_at=timestamp,
20
+ ),
11
21
  Message(
12
22
  parts=[
13
23
  MessagePart(content_type="text/plain", content="Foo"),
14
24
  MessagePart(content_type="text/plain", content="Bar"),
15
- ]
25
+ ],
26
+ created_at=timestamp,
27
+ completed_at=timestamp,
16
28
  ),
17
29
  )
18
30
  ],
@@ -29,9 +41,15 @@ def test_message_add(first: Message, second: Message, result: Message) -> None:
29
41
  parts=[
30
42
  MessagePart(content_type="text/plain", content="Foo"),
31
43
  MessagePart(content_type="text/plain", content="Bar"),
32
- ]
44
+ ],
45
+ created_at=timestamp,
46
+ completed_at=timestamp,
47
+ ),
48
+ Message(
49
+ parts=[MessagePart(content_type="text/plain", content="FooBar")],
50
+ created_at=timestamp,
51
+ completed_at=timestamp,
33
52
  ),
34
- Message(parts=[MessagePart(content_type="text/plain", content="FooBar")]),
35
53
  ),
36
54
  (
37
55
  Message(
@@ -40,14 +58,18 @@ def test_message_add(first: Message, second: Message, result: Message) -> None:
40
58
  MessagePart(content_type="text/html", content="<head>"),
41
59
  MessagePart(content_type="text/plain", content="Foo"),
42
60
  MessagePart(content_type="text/plain", content="Bar"),
43
- ]
61
+ ],
62
+ created_at=timestamp,
63
+ completed_at=timestamp,
44
64
  ),
45
65
  Message(
46
66
  parts=[
47
67
  MessagePart(content_type="text/plain", content="Foo"),
48
68
  MessagePart(content_type="text/html", content="<head>"),
49
69
  MessagePart(content_type="text/plain", content="FooBar"),
50
- ]
70
+ ],
71
+ created_at=timestamp,
72
+ completed_at=timestamp,
51
73
  ),
52
74
  ),
53
75
  ],
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
File without changes