acp-sdk 0.1.0rc6__tar.gz → 0.1.0rc8__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 (44) hide show
  1. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/PKG-INFO +12 -16
  2. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/README.md +11 -15
  3. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/clients/advanced.py +2 -2
  4. acp_sdk-0.1.0rc8/examples/clients/session.py +18 -0
  5. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/clients/simple.py +2 -2
  6. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/clients/stream.py +1 -1
  7. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/servers/awaiting.py +2 -2
  8. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/examples/servers/echo.py +4 -4
  9. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/pyproject.toml +1 -1
  10. acp_sdk-0.1.0rc8/pytest.ini +5 -0
  11. acp_sdk-0.1.0rc8/src/acp_sdk/__init__.py +2 -0
  12. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/client/client.py +49 -10
  13. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/models/models.py +14 -4
  14. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/models/schemas.py +4 -2
  15. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/agent.py +6 -11
  16. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/app.py +34 -19
  17. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/bundle.py +29 -14
  18. acp_sdk-0.1.0rc8/src/acp_sdk/server/server.py +265 -0
  19. acp_sdk-0.1.0rc8/src/acp_sdk/server/session.py +21 -0
  20. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/telemetry.py +7 -2
  21. acp_sdk-0.1.0rc8/src/acp_sdk/version.py +3 -0
  22. acp_sdk-0.1.0rc8/tests/conftest.py +4 -0
  23. acp_sdk-0.1.0rc8/tests/e2e/__init__.py +0 -0
  24. acp_sdk-0.1.0rc8/tests/e2e/config.py +2 -0
  25. acp_sdk-0.1.0rc8/tests/e2e/fixtures/__init__.py +0 -0
  26. acp_sdk-0.1.0rc8/tests/e2e/fixtures/client.py +12 -0
  27. acp_sdk-0.1.0rc8/tests/e2e/fixtures/server.py +38 -0
  28. acp_sdk-0.1.0rc8/tests/e2e/test_suites/__init__.py +0 -0
  29. acp_sdk-0.1.0rc8/tests/e2e/test_suites/test_runs.py +89 -0
  30. acp_sdk-0.1.0rc6/examples/servers/multi-echo.py +0 -57
  31. acp_sdk-0.1.0rc6/src/acp_sdk/__init__.py +0 -1
  32. acp_sdk-0.1.0rc6/src/acp_sdk/server/server.py +0 -122
  33. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/.gitignore +0 -0
  34. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/.python-version +0 -0
  35. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/client/__init__.py +0 -0
  36. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/models/__init__.py +0 -0
  37. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/models/errors.py +0 -0
  38. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/py.typed +0 -0
  39. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/__init__.py +0 -0
  40. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/context.py +0 -0
  41. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/errors.py +0 -0
  42. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/logging.py +0 -0
  43. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/types.py +0 -0
  44. {acp_sdk-0.1.0rc6 → acp_sdk-0.1.0rc8}/src/acp_sdk/server/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acp-sdk
3
- Version: 0.1.0rc6
3
+ Version: 0.1.0rc8
4
4
  Summary: Agent Communication Protocol SDK
5
5
  Requires-Python: <4.0,>=3.11
6
6
  Requires-Dist: opentelemetry-api>=1.31.1
@@ -47,7 +47,7 @@ The `client` submodule exposes [httpx]() based client with simple methods for co
47
47
 
48
48
  ```python
49
49
  async with Client(base_url="http://localhost:8000") as client:
50
- run = await client.run_sync(agent="echo", input=Message(TextMessagePart(content="Howdy!")))
50
+ run = await client.run_sync(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))])
51
51
  print(run.output)
52
52
  ```
53
53
 
@@ -56,23 +56,19 @@ async with Client(base_url="http://localhost:8000") as client:
56
56
  The `server` submodule exposes [fastapi] application factory that makes it easy to expose any agent over ACP.
57
57
 
58
58
  ```python
59
- class EchoAgent(Agent):
60
- @property
61
- def name(self) -> str:
62
- return "echo"
59
+ server = Server()
63
60
 
64
- @property
65
- def description(self) -> str:
66
- return "Echoes everything"
61
+ @server.agent()
62
+ async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
63
+ """Echoes everything"""
64
+ for message in inputs:
65
+ await asyncio.sleep(0.5)
66
+ yield {"thought": "I should echo everyting"}
67
+ await asyncio.sleep(0.5)
68
+ yield message
67
69
 
68
- async def run(self, input: Message, *, context: Context) -> AsyncGenerator[Message | Await, AwaitResume]:
69
- for part in input:
70
- await asyncio.sleep(0.5)
71
- yield {"thought": "I should echo everyting"}
72
- yield Message(part)
73
70
 
74
-
75
- serve(EchoAgent())
71
+ server.run()
76
72
  ```
77
73
 
78
74
  ➡️ Explore more in our [examples library](/python/examples).
@@ -28,7 +28,7 @@ The `client` submodule exposes [httpx]() based client with simple methods for co
28
28
 
29
29
  ```python
30
30
  async with Client(base_url="http://localhost:8000") as client:
31
- run = await client.run_sync(agent="echo", input=Message(TextMessagePart(content="Howdy!")))
31
+ run = await client.run_sync(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))])
32
32
  print(run.output)
33
33
  ```
34
34
 
@@ -37,23 +37,19 @@ async with Client(base_url="http://localhost:8000") as client:
37
37
  The `server` submodule exposes [fastapi] application factory that makes it easy to expose any agent over ACP.
38
38
 
39
39
  ```python
40
- class EchoAgent(Agent):
41
- @property
42
- def name(self) -> str:
43
- return "echo"
40
+ server = Server()
44
41
 
45
- @property
46
- def description(self) -> str:
47
- return "Echoes everything"
42
+ @server.agent()
43
+ async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
44
+ """Echoes everything"""
45
+ for message in inputs:
46
+ await asyncio.sleep(0.5)
47
+ yield {"thought": "I should echo everyting"}
48
+ await asyncio.sleep(0.5)
49
+ yield message
48
50
 
49
- async def run(self, input: Message, *, context: Context) -> AsyncGenerator[Message | Await, AwaitResume]:
50
- for part in input:
51
- await asyncio.sleep(0.5)
52
- yield {"thought": "I should echo everyting"}
53
- yield Message(part)
54
51
 
55
-
56
- serve(EchoAgent())
52
+ server.run()
57
53
  ```
58
54
 
59
55
  ➡️ Explore more in our [examples library](/python/examples).
@@ -13,8 +13,8 @@ async def example() -> None:
13
13
  # Additional client configuration
14
14
  )
15
15
  ) as client:
16
- run = await client.run_sync(agent="echo", input=Message(TextMessagePart(content="Howdy!")))
17
- print(run.output)
16
+ run = await client.run_sync(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))])
17
+ print(run.outputs)
18
18
 
19
19
 
20
20
  if __name__ == "__main__":
@@ -0,0 +1,18 @@
1
+ import asyncio
2
+
3
+ from acp_sdk.client import Client
4
+ from acp_sdk.models import (
5
+ Message,
6
+ TextMessagePart,
7
+ )
8
+
9
+
10
+ async def example() -> None:
11
+ async with Client(base_url="http://localhost:8000") as client, client.session() as session:
12
+ run = await session.run_sync(agent="historian", inputs=[Message(TextMessagePart(content="Howdy!"))])
13
+ run = await session.run_sync(agent="historian", inputs=[Message(TextMessagePart(content="Howdy again!"))])
14
+ print(run.outputs)
15
+
16
+
17
+ if __name__ == "__main__":
18
+ asyncio.run(example())
@@ -9,8 +9,8 @@ from acp_sdk.models import (
9
9
 
10
10
  async def example() -> None:
11
11
  async with Client(base_url="http://localhost:8000") as client:
12
- run = await client.run_sync(agent="echo", input=Message(TextMessagePart(content="Howdy!")))
13
- print(run.output)
12
+ run = await client.run_sync(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))])
13
+ print(run.outputs)
14
14
 
15
15
 
16
16
  if __name__ == "__main__":
@@ -6,7 +6,7 @@ from acp_sdk.models import Message, TextMessagePart
6
6
 
7
7
  async def example() -> None:
8
8
  async with Client(base_url="http://localhost:8000") as client:
9
- async for event in client.run_stream(agent="echo", input=Message(TextMessagePart(content="Howdy!"))):
9
+ async for event in client.run_stream(agent="echo", inputs=[Message(TextMessagePart(content="Howdy!"))]):
10
10
  print(event)
11
11
 
12
12
 
@@ -13,11 +13,11 @@ server = Server()
13
13
 
14
14
 
15
15
  @server.agent()
16
- async def awaiting(input: Message, context: Context) -> AsyncGenerator[Message | Await | Any, AwaitResume]:
16
+ async def awaiting(inputs: list[Message], context: Context) -> AsyncGenerator[Message | Await | Any, AwaitResume]:
17
17
  """Greets and awaits for more data"""
18
18
  yield Message(TextMessagePart(content="Hello!"))
19
19
  data = yield Await()
20
20
  yield Message(TextMessagePart(content=f"Thanks for {data}"))
21
21
 
22
22
 
23
- server()
23
+ server.run()
@@ -10,13 +10,13 @@ server = Server()
10
10
 
11
11
 
12
12
  @server.agent()
13
- async def echo(input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
13
+ async def echo(inputs: list[Message], context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
14
14
  """Echoes everything"""
15
- for part in input:
15
+ for message in inputs:
16
16
  await asyncio.sleep(0.5)
17
17
  yield {"thought": "I should echo everyting"}
18
18
  await asyncio.sleep(0.5)
19
- yield Message(part)
19
+ yield message
20
20
 
21
21
 
22
- server()
22
+ server.run()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acp-sdk"
3
- version = "0.1.0rc6"
3
+ version = "0.1.0rc8"
4
4
  description = "Agent Communication Protocol SDK"
5
5
  readme = "README.md"
6
6
  authors = []
@@ -0,0 +1,5 @@
1
+ [pytest]
2
+ testpaths = tests/e2e
3
+ python_files = test_*.py
4
+ python_functions = test_*
5
+ addopts = -v --strict-markers
@@ -0,0 +1,2 @@
1
+ from acp_sdk.models import * # noqa: F403
2
+ from acp_sdk.version import __version__ as __version__
@@ -1,4 +1,6 @@
1
- from collections.abc import AsyncIterator
1
+ import uuid
2
+ from collections.abc import AsyncGenerator, AsyncIterator
3
+ from contextlib import asynccontextmanager
2
4
  from types import TracebackType
3
5
  from typing import Self
4
6
 
@@ -14,6 +16,7 @@ from acp_sdk.models import (
14
16
  AgentReadResponse,
15
17
  AgentsListResponse,
16
18
  AwaitResume,
19
+ CreatedEvent,
17
20
  Error,
18
21
  Message,
19
22
  Run,
@@ -25,12 +28,20 @@ from acp_sdk.models import (
25
28
  RunMode,
26
29
  RunResumeRequest,
27
30
  RunResumeResponse,
31
+ SessionId,
28
32
  )
29
33
 
30
34
 
31
35
  class Client:
32
- def __init__(self, *, base_url: httpx.URL | str = "", client: httpx.AsyncClient | None = None) -> None:
36
+ def __init__(
37
+ self,
38
+ *,
39
+ base_url: httpx.URL | str = "",
40
+ session_id: SessionId | None = None,
41
+ client: httpx.AsyncClient | None = None,
42
+ ) -> None:
33
43
  self.base_url = base_url
44
+ self.session_id = session_id
34
45
 
35
46
  self._client = self._init_client(client)
36
47
 
@@ -51,6 +62,10 @@ class Client:
51
62
  ) -> None:
52
63
  await self._client.__aexit__(exc_type, exc_value, traceback)
53
64
 
65
+ @asynccontextmanager
66
+ async def session(self, session_id: SessionId | None = None) -> AsyncGenerator[Self]:
67
+ yield Client(client=self._client, session_id=session_id or uuid.uuid4())
68
+
54
69
  async def agents(self) -> AsyncIterator[Agent]:
55
70
  response = await self._client.get("/agents")
56
71
  self._raise_error(response)
@@ -62,30 +77,51 @@ class Client:
62
77
  self._raise_error(response)
63
78
  return AgentReadResponse.model_validate(response.json())
64
79
 
65
- async def run_sync(self, *, agent: AgentName, input: Message) -> Run:
80
+ async def run_sync(self, *, agent: AgentName, inputs: list[Message]) -> Run:
66
81
  response = await self._client.post(
67
82
  "/runs",
68
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.SYNC).model_dump(),
83
+ content=RunCreateRequest(
84
+ agent_name=agent,
85
+ inputs=inputs,
86
+ mode=RunMode.SYNC,
87
+ session_id=self.session_id,
88
+ ).model_dump_json(),
69
89
  )
70
90
  self._raise_error(response)
71
- return RunCreateResponse.model_validate(response.json())
91
+ response = RunCreateResponse.model_validate(response.json())
92
+ self._set_session(response)
93
+ return response
72
94
 
73
- async def run_async(self, *, agent: AgentName, input: Message) -> Run:
95
+ async def run_async(self, *, agent: AgentName, inputs: list[Message]) -> Run:
74
96
  response = await self._client.post(
75
97
  "/runs",
76
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.ASYNC).model_dump(),
98
+ content=RunCreateRequest(
99
+ agent_name=agent,
100
+ inputs=inputs,
101
+ mode=RunMode.ASYNC,
102
+ session_id=self.session_id,
103
+ ).model_dump_json(),
77
104
  )
78
105
  self._raise_error(response)
79
- return RunCreateResponse.model_validate(response.json())
106
+ response = RunCreateResponse.model_validate(response.json())
107
+ self._set_session(response)
108
+ return response
80
109
 
81
- async def run_stream(self, *, agent: AgentName, input: Message) -> AsyncIterator[RunEvent]:
110
+ async def run_stream(self, *, agent: AgentName, inputs: list[Message]) -> AsyncIterator[RunEvent]:
82
111
  async with aconnect_sse(
83
112
  self._client,
84
113
  "POST",
85
114
  "/runs",
86
- json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.STREAM).model_dump(),
115
+ content=RunCreateRequest(
116
+ agent_name=agent,
117
+ inputs=inputs,
118
+ mode=RunMode.STREAM,
119
+ session_id=self.session_id,
120
+ ).model_dump_json(),
87
121
  ) as event_source:
88
122
  async for event in self._validate_stream(event_source):
123
+ if isinstance(event, CreatedEvent):
124
+ self._set_session(event.run)
89
125
  yield event
90
126
 
91
127
  async def run_status(self, *, run_id: RunId) -> Run:
@@ -137,3 +173,6 @@ class Client:
137
173
  response.raise_for_status()
138
174
  except httpx.HTTPError:
139
175
  raise ACPError(Error.model_validate(response.json()))
176
+
177
+ def _set_session(self, run: Run) -> None:
178
+ self.session_id = run.session_id
@@ -58,8 +58,8 @@ class Message(RootModel):
58
58
 
59
59
 
60
60
  AgentName = str
61
- SessionId = str
62
- RunId = str
61
+ SessionId = uuid.UUID
62
+ RunId = uuid.UUID
63
63
 
64
64
 
65
65
  class RunMode(str, Enum):
@@ -91,13 +91,18 @@ class AwaitResume(BaseModel):
91
91
  pass
92
92
 
93
93
 
94
+ class Artifact(BaseModel):
95
+ pass
96
+
97
+
94
98
  class Run(BaseModel):
95
- run_id: RunId = str(uuid.uuid4())
99
+ run_id: RunId = Field(default_factory=uuid.uuid4)
96
100
  agent_name: AgentName
97
101
  session_id: SessionId | None = None
98
102
  status: RunStatus = RunStatus.CREATED
99
103
  await_: Await | None = Field(None, alias="await")
100
- output: Message | None = None
104
+ outputs: list[Message] = []
105
+ artifacts: list[Artifact] = []
101
106
  error: Error | None = None
102
107
 
103
108
  model_config = ConfigDict(populate_by_name=True)
@@ -117,6 +122,11 @@ class MessageEvent(BaseModel):
117
122
  message: Message
118
123
 
119
124
 
125
+ class ArtifactEvent(BaseModel):
126
+ type: Literal["artifact"] = "artifact"
127
+ artifact: Artifact
128
+
129
+
120
130
  class AwaitEvent(BaseModel):
121
131
  type: Literal["await"] = "await"
122
132
  await_: Await | None = Field(alias="await")
@@ -1,4 +1,4 @@
1
- from pydantic import BaseModel, Field
1
+ from pydantic import BaseModel, ConfigDict, Field
2
2
 
3
3
  from acp_sdk.models.models import Agent, AgentName, AwaitResume, Message, Run, RunMode, SessionId
4
4
 
@@ -14,7 +14,7 @@ class AgentReadResponse(Agent):
14
14
  class RunCreateRequest(BaseModel):
15
15
  agent_name: AgentName
16
16
  session_id: SessionId | None = None
17
- input: Message
17
+ inputs: list[Message]
18
18
  mode: RunMode = RunMode.SYNC
19
19
 
20
20
 
@@ -26,6 +26,8 @@ class RunResumeRequest(BaseModel):
26
26
  await_: AwaitResume = Field(alias="await")
27
27
  mode: RunMode
28
28
 
29
+ model_config = ConfigDict(populate_by_name=True)
30
+
29
31
 
30
32
  class RunResumeResponse(Run):
31
33
  pass
@@ -31,19 +31,14 @@ class Agent(abc.ABC):
31
31
 
32
32
  @abc.abstractmethod
33
33
  def run(
34
- self, input: Message, context: Context
34
+ self, inputs: list[Message], context: Context
35
35
  ) -> (
36
36
  AsyncGenerator[RunYield, RunYieldResume] | Generator[RunYield, RunYieldResume] | Coroutine[RunYield] | RunYield
37
37
  ):
38
38
  pass
39
39
 
40
- async def session(self, session_id: SessionId | None) -> SessionId | None:
41
- if session_id:
42
- raise NotImplementedError()
43
- return None
44
-
45
40
  async def execute(
46
- self, input: Message, session_id: SessionId | None, executor: ThreadPoolExecutor
41
+ self, inputs: list[Message], session_id: SessionId | None, executor: ThreadPoolExecutor
47
42
  ) -> AsyncGenerator[RunYield, RunYieldResume]:
48
43
  yield_queue: janus.Queue[RunYield] = janus.Queue()
49
44
  yield_resume_queue: janus.Queue[RunYieldResume] = janus.Queue()
@@ -53,13 +48,13 @@ class Agent(abc.ABC):
53
48
  )
54
49
 
55
50
  if inspect.isasyncgenfunction(self.run):
56
- run = asyncio.create_task(self._run_async_gen(input, context))
51
+ run = asyncio.create_task(self._run_async_gen(inputs, context))
57
52
  elif inspect.iscoroutinefunction(self.run):
58
- run = asyncio.create_task(self._run_coro(input, context))
53
+ run = asyncio.create_task(self._run_coro(inputs, context))
59
54
  elif inspect.isgeneratorfunction(self.run):
60
- run = asyncio.get_running_loop().run_in_executor(executor, self._run_gen, input, context)
55
+ run = asyncio.get_running_loop().run_in_executor(executor, self._run_gen, inputs, context)
61
56
  else:
62
- run = asyncio.get_running_loop().run_in_executor(executor, self._run_func, input, context)
57
+ run = asyncio.get_running_loop().run_in_executor(executor, self._run_func, inputs, context)
63
58
 
64
59
  try:
65
60
  while True:
@@ -1,9 +1,10 @@
1
- import asyncio
2
1
  from collections.abc import AsyncGenerator
3
2
  from concurrent.futures import ThreadPoolExecutor
4
3
  from contextlib import asynccontextmanager
4
+ from enum import Enum
5
5
 
6
6
  from fastapi import FastAPI, HTTPException, status
7
+ from fastapi.encoders import jsonable_encoder
7
8
  from fastapi.responses import JSONResponse, StreamingResponse
8
9
  from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
9
10
 
@@ -23,7 +24,7 @@ from acp_sdk.models import (
23
24
  RunReadResponse,
24
25
  RunResumeRequest,
25
26
  RunResumeResponse,
26
- RunStatus,
27
+ SessionId,
27
28
  )
28
29
  from acp_sdk.models.errors import ACPError
29
30
  from acp_sdk.server.agent import Agent
@@ -36,16 +37,21 @@ from acp_sdk.server.errors import (
36
37
  http_exception_handler,
37
38
  validation_exception_handler,
38
39
  )
40
+ from acp_sdk.server.session import Session
39
41
  from acp_sdk.server.utils import stream_sse
40
42
 
41
43
 
44
+ class Headers(str, Enum):
45
+ RUN_ID = "Run-ID"
46
+
47
+
42
48
  def create_app(*agents: Agent) -> FastAPI:
43
49
  executor: ThreadPoolExecutor
44
50
 
45
51
  @asynccontextmanager
46
52
  async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
47
53
  nonlocal executor
48
- with ThreadPoolExecutor(max_workers=5) as exec:
54
+ with ThreadPoolExecutor() as exec:
49
55
  executor = exec
50
56
  yield
51
57
 
@@ -55,6 +61,7 @@ def create_app(*agents: Agent) -> FastAPI:
55
61
 
56
62
  agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
57
63
  runs: dict[RunId, RunBundle] = {}
64
+ sessions: dict[SessionId, Session] = {}
58
65
 
59
66
  app.exception_handler(ACPError)(acp_error_handler)
60
67
  app.exception_handler(StarletteHTTPException)(http_exception_handler)
@@ -90,31 +97,41 @@ def create_app(*agents: Agent) -> FastAPI:
90
97
  @app.post("/runs")
91
98
  async def create_run(request: RunCreateRequest) -> RunCreateResponse:
92
99
  agent = find_agent(request.agent_name)
100
+
101
+ session = sessions.get(request.session_id, Session()) if request.session_id else Session()
102
+ nonlocal executor
93
103
  bundle = RunBundle(
94
104
  agent=agent,
95
- run=Run(
96
- agent_name=agent.name,
97
- session_id=request.session_id,
98
- ),
105
+ run=Run(agent_name=agent.name, session_id=session.id),
106
+ inputs=request.inputs,
107
+ history=list(session.history()),
108
+ executor=executor,
99
109
  )
110
+ session.append(bundle)
100
111
 
101
- nonlocal executor
102
- bundle.task = asyncio.create_task(bundle.execute(request.input, executor=executor))
103
112
  runs[bundle.run.run_id] = bundle
113
+ sessions[session.id] = session
114
+
115
+ headers = {Headers.RUN_ID: str(bundle.run.run_id)}
104
116
 
105
117
  match request.mode:
106
118
  case RunMode.STREAM:
107
119
  return StreamingResponse(
108
120
  stream_sse(bundle),
121
+ headers=headers,
109
122
  media_type="text/event-stream",
110
123
  )
111
124
  case RunMode.SYNC:
112
125
  await bundle.join()
113
- return bundle.run
126
+ return JSONResponse(
127
+ headers=headers,
128
+ content=jsonable_encoder(bundle.run),
129
+ )
114
130
  case RunMode.ASYNC:
115
131
  return JSONResponse(
116
132
  status_code=status.HTTP_202_ACCEPTED,
117
- content=bundle.run.model_dump(),
133
+ headers=headers,
134
+ content=jsonable_encoder(bundle.run),
118
135
  )
119
136
  case _:
120
137
  raise NotImplementedError()
@@ -127,8 +144,7 @@ def create_app(*agents: Agent) -> FastAPI:
127
144
  @app.post("/runs/{run_id}")
128
145
  async def resume_run(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
129
146
  bundle = find_run_bundle(run_id)
130
- bundle.stream_queue = asyncio.Queue() # TODO improve
131
- await bundle.await_queue.put(request.await_)
147
+ await bundle.resume(request.await_)
132
148
  match request.mode:
133
149
  case RunMode.STREAM:
134
150
  return StreamingResponse(
@@ -141,7 +157,7 @@ def create_app(*agents: Agent) -> FastAPI:
141
157
  case RunMode.ASYNC:
142
158
  return JSONResponse(
143
159
  status_code=status.HTTP_202_ACCEPTED,
144
- content=bundle.run.model_dump(),
160
+ content=jsonable_encoder(bundle.run),
145
161
  )
146
162
  case _:
147
163
  raise NotImplementedError()
@@ -151,11 +167,10 @@ def create_app(*agents: Agent) -> FastAPI:
151
167
  bundle = find_run_bundle(run_id)
152
168
  if bundle.run.status.is_terminal:
153
169
  raise HTTPException(
154
- status_code=403,
155
- detail=f"Run with terminal status {bundle.run.status} can't be cancelled",
170
+ status_code=status.HTTP_403_FORBIDDEN,
171
+ detail=f"Run in terminal status {bundle.run.status} can't be cancelled",
156
172
  )
157
- bundle.task.cancel()
158
- bundle.run.status = RunStatus.CANCELLING
159
- return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=bundle.run.model_dump())
173
+ await bundle.cancel()
174
+ return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder(bundle.run))
160
175
 
161
176
  return app
@@ -3,11 +3,12 @@ import logging
3
3
  from collections.abc import AsyncGenerator
4
4
  from concurrent.futures import ThreadPoolExecutor
5
5
 
6
- from opentelemetry import trace
7
6
  from pydantic import ValidationError
8
7
 
9
8
  from acp_sdk.models import (
10
9
  AnyModel,
10
+ Artifact,
11
+ ArtifactEvent,
11
12
  Await,
12
13
  AwaitEvent,
13
14
  AwaitResume,
@@ -27,20 +28,25 @@ from acp_sdk.models import (
27
28
  from acp_sdk.models.errors import ErrorCode
28
29
  from acp_sdk.server.agent import Agent
29
30
  from acp_sdk.server.logging import logger
31
+ from acp_sdk.server.telemetry import get_tracer
30
32
 
31
33
 
32
34
  class RunBundle:
33
- def __init__(self, *, agent: Agent, run: Run, task: asyncio.Task | None = None) -> None:
35
+ def __init__(
36
+ self, *, agent: Agent, run: Run, inputs: list[Message], history: list[Message], executor: ThreadPoolExecutor
37
+ ) -> None:
34
38
  self.agent = agent
35
39
  self.run = run
36
- self.task = task
40
+ self.inputs = inputs
41
+ self.history = history
37
42
 
38
43
  self.stream_queue: asyncio.Queue[RunEvent] = asyncio.Queue()
39
- self.composed_message = Message()
40
44
 
41
45
  self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
42
46
  self.await_or_terminate_event = asyncio.Event()
43
47
 
48
+ self.task = asyncio.create_task(self._execute(inputs, executor=executor))
49
+
44
50
  async def stream(self) -> AsyncGenerator[RunEvent]:
45
51
  while True:
46
52
  event = await self.stream_queue.get()
@@ -64,20 +70,27 @@ class RunBundle:
64
70
  async def resume(self, resume: AwaitResume) -> None:
65
71
  self.stream_queue = asyncio.Queue()
66
72
  await self.await_queue.put(resume)
73
+ self.run.status = RunStatus.IN_PROGRESS
74
+ self.run.await_ = None
75
+
76
+ async def cancel(self) -> None:
77
+ self.task.cancel()
78
+ self.run.status = RunStatus.CANCELLING
79
+ self.run.await_ = None
67
80
 
68
81
  async def join(self) -> None:
69
82
  await self.await_or_terminate_event.wait()
70
83
 
71
- async def execute(self, input: Message, *, executor: ThreadPoolExecutor) -> None:
72
- with trace.get_tracer(__name__).start_as_current_span("execute"):
73
- run_logger = logging.LoggerAdapter(logger, {"run_id": self.run.run_id})
84
+ async def _execute(self, inputs: list[Message], *, executor: ThreadPoolExecutor) -> None:
85
+ with get_tracer().start_as_current_span("run"):
86
+ run_logger = logging.LoggerAdapter(logger, {"run_id": str(self.run.run_id)})
74
87
 
75
- await self.emit(CreatedEvent(run=self.run))
76
88
  try:
77
- self.run.session_id = await self.agent.session(self.run.session_id)
78
- run_logger.info("Session loaded")
89
+ await self.emit(CreatedEvent(run=self.run))
79
90
 
80
- generator = self.agent.execute(input=input, session_id=self.run.session_id, executor=executor)
91
+ generator = self.agent.execute(
92
+ inputs=self.history + inputs, session_id=self.run.session_id, executor=executor
93
+ )
81
94
  run_logger.info("Run started")
82
95
 
83
96
  self.run.status = RunStatus.IN_PROGRESS
@@ -87,8 +100,11 @@ class RunBundle:
87
100
  while True:
88
101
  next = await generator.asend(await_resume)
89
102
  if isinstance(next, Message):
90
- self.composed_message += next
103
+ self.run.outputs.append(next)
91
104
  await self.emit(MessageEvent(message=next))
105
+ elif isinstance(next, Artifact):
106
+ self.run.artifacts.append(next)
107
+ await self.emit(ArtifactEvent(artifact=next))
92
108
  elif isinstance(next, Await):
93
109
  self.run.await_ = next
94
110
  self.run.status = RunStatus.AWAITING
@@ -103,7 +119,6 @@ class RunBundle:
103
119
  )
104
120
  run_logger.info("Run awaited")
105
121
  await_resume = await self.await_()
106
- self.run.status = RunStatus.IN_PROGRESS
107
122
  await self.emit(InProgressEvent(run=self.run))
108
123
  run_logger.info("Run resumed")
109
124
  else:
@@ -113,7 +128,6 @@ class RunBundle:
113
128
  except ValidationError:
114
129
  raise TypeError("Invalid yield")
115
130
  except StopAsyncIteration:
116
- self.run.output = self.composed_message
117
131
  self.run.status = RunStatus.COMPLETED
118
132
  await self.emit(CompletedEvent(run=self.run))
119
133
  run_logger.info("Run completed")
@@ -126,6 +140,7 @@ class RunBundle:
126
140
  self.run.status = RunStatus.FAILED
127
141
  await self.emit(FailedEvent(run=self.run))
128
142
  run_logger.exception("Run failed")
143
+ raise
129
144
  finally:
130
145
  self.await_or_terminate_event.set()
131
146
  await self.stream_queue.put(None)