acp-sdk 0.7.2__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.2 → acp_sdk-0.8.0}/PKG-INFO +1 -1
  2. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/pyproject.toml +1 -1
  3. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/client/client.py +12 -10
  4. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/models/models.py +11 -3
  5. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/models/schemas.py +9 -1
  6. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/app.py +11 -4
  7. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/bundle.py +12 -3
  8. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/session.py +2 -2
  9. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/e2e/test_suites/test_discovery.py +6 -0
  10. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/e2e/test_suites/test_runs.py +22 -4
  11. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/unit/client/test_client.py +38 -2
  12. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/unit/client/test_utils.py +2 -1
  13. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/unit/models/test_models.py +29 -7
  14. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/.gitignore +0 -0
  15. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/.python-version +0 -0
  16. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/README.md +0 -0
  17. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/docs/_sidebar.md +0 -0
  18. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/docs/client.md +0 -0
  19. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/docs/index.html +0 -0
  20. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/docs/models.md +0 -0
  21. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/docs/server.md +0 -0
  22. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/pytest.ini +0 -0
  23. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/__init__.py +0 -0
  24. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/client/__init__.py +0 -0
  25. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/client/types.py +0 -0
  26. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/client/utils.py +0 -0
  27. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/instrumentation.py +0 -0
  28. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/models/__init__.py +0 -0
  29. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/models/errors.py +0 -0
  30. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/py.typed +0 -0
  31. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/__init__.py +0 -0
  32. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/agent.py +0 -0
  33. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/context.py +0 -0
  34. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/errors.py +0 -0
  35. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/logging.py +0 -0
  36. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/server.py +0 -0
  37. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/telemetry.py +0 -0
  38. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/types.py +0 -0
  39. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/server/utils.py +0 -0
  40. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/src/acp_sdk/version.py +0 -0
  41. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/conftest.py +0 -0
  42. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/e2e/__init__.py +0 -0
  43. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/e2e/config.py +0 -0
  44. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/e2e/fixtures/__init__.py +0 -0
  45. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/e2e/fixtures/client.py +0 -0
  46. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/e2e/fixtures/server.py +0 -0
  47. {acp_sdk-0.7.2 → acp_sdk-0.8.0}/tests/e2e/test_suites/__init__.py +0 -0
  48. {acp_sdk-0.7.2 → 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.2
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.2"
3
+ version = "0.8.0"
4
4
  description = "Agent Communication Protocol SDK"
5
5
  license = "Apache-2.0"
6
6
  readme = "README.md"
@@ -23,11 +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
- RunCreatedEvent,
29
29
  RunCreateRequest,
30
30
  RunCreateResponse,
31
+ RunEventsListResponse,
31
32
  RunId,
32
33
  RunMode,
33
34
  RunResumeRequest,
@@ -121,9 +122,10 @@ class Client:
121
122
  return Agent(**response.model_dump())
122
123
 
123
124
  async def ping(self) -> bool:
124
- response = await self._client.get("/healthcheck")
125
+ response = await self._client.get("/ping")
125
126
  self._raise_error(response)
126
- return response.json() == "OK"
127
+ PingResponse.model_validate(response.json())
128
+ return
127
129
 
128
130
  async def run_sync(self, input: Input, *, agent: AgentName) -> Run:
129
131
  response = await self._client.post(
@@ -137,7 +139,6 @@ class Client:
137
139
  )
138
140
  self._raise_error(response)
139
141
  response = RunCreateResponse.model_validate(response.json())
140
- self._set_session(response)
141
142
  return Run(**response.model_dump())
142
143
 
143
144
  async def run_async(self, input: Input, *, agent: AgentName) -> Run:
@@ -152,7 +153,6 @@ class Client:
152
153
  )
153
154
  self._raise_error(response)
154
155
  response = RunCreateResponse.model_validate(response.json())
155
- self._set_session(response)
156
156
  return Run(**response.model_dump())
157
157
 
158
158
  async def run_stream(self, input: Input, *, agent: AgentName) -> AsyncIterator[Event]:
@@ -168,8 +168,6 @@ class Client:
168
168
  ).model_dump_json(),
169
169
  ) as event_source:
170
170
  async for event in self._validate_stream(event_source):
171
- if isinstance(event, RunCreatedEvent):
172
- self._set_session(event.run)
173
171
  yield event
174
172
 
175
173
  async def run_status(self, *, run_id: RunId) -> Run:
@@ -177,6 +175,13 @@ class Client:
177
175
  self._raise_error(response)
178
176
  return Run.model_validate(response.json())
179
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
+
180
185
  async def run_cancel(self, *, run_id: RunId) -> Run:
181
186
  response = await self._client.post(f"/runs/{run_id}/cancel")
182
187
  self._raise_error(response)
@@ -227,6 +232,3 @@ class Client:
227
232
  response.raise_for_status()
228
233
  except httpx.HTTPError:
229
234
  raise ACPError(Error.model_validate(response.json()))
230
-
231
- def _set_session(self, run: Run) -> None:
232
- self._session_id = run.session_id
@@ -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,15 +106,15 @@ 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:
113
115
  agent = find_agent(request.agent_name)
114
116
 
115
- session = sessions.get(request.session_id, Session()) if request.session_id else Session()
117
+ session = sessions.get(request.session_id, Session(id=request.session_id)) if request.session_id else Session()
116
118
  nonlocal executor
117
119
  bundle = RunBundle(
118
120
  agent=agent,
@@ -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
@@ -7,8 +7,8 @@ from acp_sdk.server.bundle import RunBundle
7
7
 
8
8
 
9
9
  class Session:
10
- def __init__(self) -> None:
11
- self.id: SessionId = uuid.uuid4()
10
+ def __init__(self, id: SessionId | None = None) -> None:
11
+ self.id: SessionId = id or uuid.uuid4()
12
12
  self.bundles: list[RunBundle] = []
13
13
 
14
14
  def append(self, bundle: RunBundle) -> None:
@@ -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)
@@ -143,3 +165,17 @@ async def test_session(httpx_mock: HTTPXMock) -> None:
143
165
 
144
166
  body = json.loads(requests[1].content)
145
167
  assert body["session_id"] is None
168
+
169
+
170
+ @pytest.mark.asyncio
171
+ async def test_no_session(httpx_mock: HTTPXMock) -> None:
172
+ httpx_mock.add_response(url="http://test/runs", method="POST", content=mock_run.model_dump_json(), is_reusable=True)
173
+
174
+ async with Client(base_url="http://test") as client:
175
+ await client.run_sync("Howdy!", agent=mock_run.agent_name)
176
+ await client.run_sync("Howdy!", agent=mock_run.agent_name)
177
+
178
+ requests = httpx_mock.get_requests()
179
+
180
+ body = json.loads(requests[1].content)
181
+ assert body["session_id"] is None
@@ -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