acp-sdk 0.1.0rc6__py3-none-any.whl → 0.1.0rc8__py3-none-any.whl

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.
acp_sdk/__init__.py CHANGED
@@ -1 +1,2 @@
1
1
  from acp_sdk.models import * # noqa: F403
2
+ from acp_sdk.version import __version__ as __version__
acp_sdk/client/client.py CHANGED
@@ -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
acp_sdk/models/models.py CHANGED
@@ -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")
acp_sdk/models/schemas.py CHANGED
@@ -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
acp_sdk/server/agent.py CHANGED
@@ -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:
acp_sdk/server/app.py CHANGED
@@ -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
acp_sdk/server/bundle.py CHANGED
@@ -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)
acp_sdk/server/server.py CHANGED
@@ -1,8 +1,13 @@
1
+ import asyncio
1
2
  import inspect
2
- from collections.abc import AsyncGenerator, Coroutine, Generator
3
+ import os
4
+ from collections.abc import AsyncGenerator, Awaitable, Coroutine, Generator
3
5
  from typing import Any, Callable
4
6
 
5
- from acp_sdk.models import Message
7
+ import uvicorn
8
+ import uvicorn.config
9
+
10
+ from acp_sdk.models import Message, Metadata
6
11
  from acp_sdk.server.agent import Agent
7
12
  from acp_sdk.server.app import create_app
8
13
  from acp_sdk.server.context import Context
@@ -13,9 +18,16 @@ from acp_sdk.server.types import RunYield, RunYieldResume
13
18
 
14
19
  class Server:
15
20
  def __init__(self) -> None:
16
- self.agents: list[Agent] = []
21
+ self._agents: list[Agent] = []
22
+ self._server: uvicorn.Server | None = None
17
23
 
18
- def agent(self, name: str | None = None, description: str | None = None) -> Callable:
24
+ def agent(
25
+ self,
26
+ name: str | None = None,
27
+ description: str | None = None,
28
+ *,
29
+ metadata: Metadata | None = None,
30
+ ) -> Callable:
19
31
  """Decorator to register an agent."""
20
32
 
21
33
  def decorator(fn: Callable) -> Callable:
@@ -43,6 +55,10 @@ class Server:
43
55
  def description(self) -> str:
44
56
  return description or fn.__doc__ or ""
45
57
 
58
+ @property
59
+ def metadata(self) -> Metadata:
60
+ return metadata or Metadata()
61
+
46
62
  async def run(self, input: Message, context: Context) -> AsyncGenerator[RunYield, RunYieldResume]:
47
63
  try:
48
64
  gen: AsyncGenerator[RunYield, RunYieldResume] = (
@@ -66,6 +82,10 @@ class Server:
66
82
  def description(self) -> str:
67
83
  return description or fn.__doc__ or ""
68
84
 
85
+ @property
86
+ def metadata(self) -> Metadata:
87
+ return metadata or Metadata()
88
+
69
89
  async def run(self, input: Message, context: Context) -> Coroutine[RunYield]:
70
90
  return await (fn(input, context) if has_context_param else fn(input))
71
91
 
@@ -81,6 +101,10 @@ class Server:
81
101
  def description(self) -> str:
82
102
  return description or fn.__doc__ or ""
83
103
 
104
+ @property
105
+ def metadata(self) -> Metadata:
106
+ return metadata or Metadata()
107
+
84
108
  def run(self, input: Message, context: Context) -> Generator[RunYield, RunYieldResume]:
85
109
  yield from (fn(input, context) if has_context_param else fn(input))
86
110
 
@@ -96,6 +120,10 @@ class Server:
96
120
  def description(self) -> str:
97
121
  return description or fn.__doc__ or ""
98
122
 
123
+ @property
124
+ def metadata(self) -> Metadata:
125
+ return metadata or Metadata()
126
+
99
127
  def run(self, input: Message, context: Context) -> RunYield:
100
128
  return fn(input, context) if has_context_param else fn(input)
101
129
 
@@ -107,11 +135,67 @@ class Server:
107
135
  return decorator
108
136
 
109
137
  def register(self, *agents: Agent) -> None:
110
- self.agents.extend(agents)
138
+ self._agents.extend(agents)
111
139
 
112
- def __call__(
113
- self, configure_logger: bool = True, configure_telemetry: bool = False, **kwargs: dict[str, Any]
140
+ def run(
141
+ self,
142
+ configure_logger: bool = True,
143
+ configure_telemetry: bool = False,
144
+ host: str = "127.0.0.1",
145
+ port: int = 8000,
146
+ uds: str | None = None,
147
+ fd: int | None = None,
148
+ loop: uvicorn.config.LoopSetupType = "auto",
149
+ http: type[asyncio.Protocol] | uvicorn.config.HTTPProtocolType = "auto",
150
+ ws: type[asyncio.Protocol] | uvicorn.config.WSProtocolType = "auto",
151
+ ws_max_size: int = 16 * 1024 * 1024,
152
+ ws_max_queue: int = 32,
153
+ ws_ping_interval: float | None = 20.0,
154
+ ws_ping_timeout: float | None = 20.0,
155
+ ws_per_message_deflate: bool = True,
156
+ lifespan: uvicorn.config.LifespanType = "auto",
157
+ env_file: str | os.PathLike[str] | None = None,
158
+ log_config: dict[str, Any]
159
+ | str
160
+ | uvicorn.config.RawConfigParser
161
+ | uvicorn.config.IO[Any]
162
+ | None = uvicorn.config.LOGGING_CONFIG,
163
+ log_level: str | int | None = None,
164
+ access_log: bool = True,
165
+ use_colors: bool | None = None,
166
+ interface: uvicorn.config.InterfaceType = "auto",
167
+ reload: bool = False,
168
+ reload_dirs: list[str] | str | None = None,
169
+ reload_delay: float = 0.25,
170
+ reload_includes: list[str] | str | None = None,
171
+ reload_excludes: list[str] | str | None = None,
172
+ workers: int | None = None,
173
+ proxy_headers: bool = True,
174
+ server_header: bool = True,
175
+ date_header: bool = True,
176
+ forwarded_allow_ips: list[str] | str | None = None,
177
+ root_path: str = "",
178
+ limit_concurrency: int | None = None,
179
+ limit_max_requests: int | None = None,
180
+ backlog: int = 2048,
181
+ timeout_keep_alive: int = 5,
182
+ timeout_notify: int = 30,
183
+ timeout_graceful_shutdown: int | None = None,
184
+ callback_notify: Callable[..., Awaitable[None]] | None = None,
185
+ ssl_keyfile: str | os.PathLike[str] | None = None,
186
+ ssl_certfile: str | os.PathLike[str] | None = None,
187
+ ssl_keyfile_password: str | None = None,
188
+ ssl_version: int = uvicorn.config.SSL_PROTOCOL_VERSION,
189
+ ssl_cert_reqs: int = uvicorn.config.ssl.CERT_NONE,
190
+ ssl_ca_certs: str | None = None,
191
+ ssl_ciphers: str = "TLSv1",
192
+ headers: list[tuple[str, str]] | None = None,
193
+ factory: bool = False,
194
+ h11_max_incomplete_event_size: int | None = None,
114
195
  ) -> None:
196
+ if self._server:
197
+ raise RuntimeError("The server is already running")
198
+
115
199
  import uvicorn
116
200
 
117
201
  if configure_logger:
@@ -119,4 +203,63 @@ class Server:
119
203
  if configure_telemetry:
120
204
  configure_telemetry_func()
121
205
 
122
- uvicorn.run(create_app(*self.agents), **kwargs)
206
+ config = uvicorn.Config(
207
+ create_app(*self._agents),
208
+ host,
209
+ port,
210
+ uds,
211
+ fd,
212
+ loop,
213
+ http,
214
+ ws,
215
+ ws_max_size,
216
+ ws_max_queue,
217
+ ws_ping_interval,
218
+ ws_ping_timeout,
219
+ ws_per_message_deflate,
220
+ lifespan,
221
+ env_file,
222
+ log_config,
223
+ log_level,
224
+ access_log,
225
+ use_colors,
226
+ interface,
227
+ reload,
228
+ reload_dirs,
229
+ reload_delay,
230
+ reload_includes,
231
+ reload_excludes,
232
+ workers,
233
+ proxy_headers,
234
+ server_header,
235
+ date_header,
236
+ forwarded_allow_ips,
237
+ root_path,
238
+ limit_concurrency,
239
+ limit_max_requests,
240
+ backlog,
241
+ timeout_keep_alive,
242
+ timeout_notify,
243
+ timeout_graceful_shutdown,
244
+ callback_notify,
245
+ ssl_keyfile,
246
+ ssl_certfile,
247
+ ssl_keyfile_password,
248
+ ssl_version,
249
+ ssl_cert_reqs,
250
+ ssl_ca_certs,
251
+ ssl_ciphers,
252
+ headers,
253
+ factory,
254
+ h11_max_incomplete_event_size,
255
+ )
256
+ self._server = uvicorn.Server(config)
257
+ self._server.run()
258
+
259
+ @property
260
+ def should_exit(self) -> bool:
261
+ return self._server.should_exit if self._server else False
262
+
263
+ @should_exit.setter
264
+ def should_exit(self, value: bool) -> None:
265
+ self._server.should_exit = value
@@ -0,0 +1,21 @@
1
+ import uuid
2
+ from collections.abc import Iterator
3
+
4
+ from acp_sdk.models import Message, SessionId
5
+ from acp_sdk.models.models import RunStatus
6
+ from acp_sdk.server.bundle import RunBundle
7
+
8
+
9
+ class Session:
10
+ def __init__(self) -> None:
11
+ self.id: SessionId = uuid.uuid4()
12
+ self.bundles: list[RunBundle] = []
13
+
14
+ def append(self, bundle: RunBundle) -> None:
15
+ self.bundles.append(bundle)
16
+
17
+ def history(self) -> Iterator[Message]:
18
+ for bundle in self.bundles:
19
+ if bundle.run.status == RunStatus.COMPLETED:
20
+ yield from bundle.inputs
21
+ yield from bundle.run.outputs
@@ -1,5 +1,4 @@
1
1
  import logging
2
- from importlib.metadata import version
3
2
 
4
3
  from opentelemetry import metrics, trace
5
4
  from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
@@ -18,6 +17,8 @@ from opentelemetry.sdk.resources import (
18
17
  from opentelemetry.sdk.trace import TracerProvider
19
18
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
20
19
 
20
+ from acp_sdk.version import __version__
21
+
21
22
  root_logger = logging.getLogger()
22
23
 
23
24
 
@@ -28,7 +29,7 @@ def configure_telemetry() -> None:
28
29
  attributes={
29
30
  SERVICE_NAME: "acp-server",
30
31
  SERVICE_NAMESPACE: "acp",
31
- SERVICE_VERSION: version("acp-sdk"),
32
+ SERVICE_VERSION: __version__,
32
33
  }
33
34
  )
34
35
 
@@ -50,3 +51,7 @@ def configure_telemetry() -> None:
50
51
  processor = BatchLogRecordProcessor(OTLPLogExporter())
51
52
  logger_provider.add_log_record_processor(processor)
52
53
  root_logger.addHandler(LoggingHandler(logger_provider=logger_provider))
54
+
55
+
56
+ def get_tracer() -> trace.Tracer:
57
+ return trace.get_tracer("acp-sdk", __version__)
acp_sdk/version.py ADDED
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("acp-sdk")
@@ -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).
@@ -0,0 +1,24 @@
1
+ acp_sdk/__init__.py,sha256=tXdAUM9zcmdSKCAkVrOCrGcXcuVS-yuvQUoQwTe9pek,98
2
+ acp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ acp_sdk/version.py,sha256=Niy83rgvigB4hL_rR-O4ySvI7dj6xnqkyOe_JTymi9s,73
4
+ acp_sdk/client/__init__.py,sha256=Bca1DORrswxzZsrR2aUFpATuNG2xNSmYvF1Z2WJaVbc,51
5
+ acp_sdk/client/client.py,sha256=phsFdVMdVmTcfmzUQUgqzzvq4zNvmEUgW2eL9cFJY2g,6140
6
+ acp_sdk/models/__init__.py,sha256=numSDBDT1QHx7n_Y3Deb5VOvKWcUBxbOEaMwQBSRHxc,151
7
+ acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
8
+ acp_sdk/models/models.py,sha256=LrOZIWwn_6SenSwzz2JeK7Q555vzqjBKFElCifxQgnk,4107
9
+ acp_sdk/models/schemas.py,sha256=LNs3oN1pQ4GD0ceV3-k6X6R39eu33nsAfnW6uEgOP0c,735
10
+ acp_sdk/server/__init__.py,sha256=CowMcwN_WSsnO_-ZoqWQKtNVfa21MW3X3trZ9haJjaA,329
11
+ acp_sdk/server/agent.py,sha256=wMwyB3Ouz361DX-RDPFD61nymoUOTvBIJt3_JU9KmOw,3297
12
+ acp_sdk/server/app.py,sha256=Ys5EN4MzmrwrpBGvycEP5dKEIYkDZmeBMMV1Aq58AU0,5897
13
+ acp_sdk/server/bundle.py,sha256=VUOqzBhwsADkoyPWqXSZ2KG59S3q4LMDfpFVAssnGSM,5386
14
+ acp_sdk/server/context.py,sha256=MgnLV6qcDIhc_0BjW7r4Jj1tHts4ZuwpdTGIBnz2Mgo,1036
15
+ acp_sdk/server/errors.py,sha256=fWlgVsQ5hs_AXwzc-wvy6QgoDWEMRUBlSrfJfhHHMyE,2085
16
+ acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
17
+ acp_sdk/server/server.py,sha256=6iCJZrMfIDtqEmxqFREpPbyAiuxVxbQD73QHiFJ6KpA,9307
18
+ acp_sdk/server/session.py,sha256=0cDr924HC5x2bBNbK9NSKVHAt5A_mi5dK8P4jP_ugq0,629
19
+ acp_sdk/server/telemetry.py,sha256=WIEHK8syOTG9SyWi3Y-cos7CsCF5-IHGiyL9bCaUN0E,1921
20
+ acp_sdk/server/types.py,sha256=2yJPkfUzjVIhHmc0SegGTMqDROe2uFgycb-7CATvYVw,161
21
+ acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
22
+ acp_sdk-0.1.0rc8.dist-info/METADATA,sha256=hrIiPuTocgUWerNeGvolLPMY1DgqFqpTshMswZr7sQw,2041
23
+ acp_sdk-0.1.0rc8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
+ acp_sdk-0.1.0rc8.dist-info/RECORD,,
@@ -1,22 +0,0 @@
1
- acp_sdk/__init__.py,sha256=XlcAbmvJfvJK5tmOFdxXP19aDwxxdG_jygsTCvXtjTk,43
2
- acp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- acp_sdk/client/__init__.py,sha256=Bca1DORrswxzZsrR2aUFpATuNG2xNSmYvF1Z2WJaVbc,51
4
- acp_sdk/client/client.py,sha256=W6iXxH65UUODnvt3tnvgdqT_pP1meZUF2HwJpWCM0BM,5029
5
- acp_sdk/models/__init__.py,sha256=numSDBDT1QHx7n_Y3Deb5VOvKWcUBxbOEaMwQBSRHxc,151
6
- acp_sdk/models/errors.py,sha256=rEyaMVvQuBi7fwWe_d0PGGySYsD3FZTluQ-SkC0yhAs,444
7
- acp_sdk/models/models.py,sha256=ACZPQp-wm43tdlx83hrX7QmdtBRWVXvU5bsotUOidHM,3908
8
- acp_sdk/models/schemas.py,sha256=a0T29vfy93uVIce3ikbd3GSTdKBokMmfXWNVRJD3Eb8,662
9
- acp_sdk/server/__init__.py,sha256=CowMcwN_WSsnO_-ZoqWQKtNVfa21MW3X3trZ9haJjaA,329
10
- acp_sdk/server/agent.py,sha256=SZfJK634d4s8fRh0u1Tc56l58XCRHAh3hq5Hcabh6tI,3442
11
- acp_sdk/server/app.py,sha256=7alacNo9y_f0-BjZI0dm_rzSweW-n5YiuY0hrVtEIR8,5420
12
- acp_sdk/server/bundle.py,sha256=ZVkIOwWfYgi-VyGYSMKr9WrG_WzYbej-LD-YPUWQdeg,4982
13
- acp_sdk/server/context.py,sha256=MgnLV6qcDIhc_0BjW7r4Jj1tHts4ZuwpdTGIBnz2Mgo,1036
14
- acp_sdk/server/errors.py,sha256=fWlgVsQ5hs_AXwzc-wvy6QgoDWEMRUBlSrfJfhHHMyE,2085
15
- acp_sdk/server/logging.py,sha256=Oc8yZigCsuDnHHPsarRzu0RX3NKaLEgpELM2yovGKDI,411
16
- acp_sdk/server/server.py,sha256=H-oh-cRpo9NR7_Mdt7ECRXrGnk8mfAAb7pp94XLGoAs,4637
17
- acp_sdk/server/telemetry.py,sha256=EwmtUrWMYid7XHiX76V1J6CigJPa2NrzEPOX0fBoY3o,1838
18
- acp_sdk/server/types.py,sha256=2yJPkfUzjVIhHmc0SegGTMqDROe2uFgycb-7CATvYVw,161
19
- acp_sdk/server/utils.py,sha256=EfrF9VCyVk3AM_ao-BIB9EzGbfTrh4V2Bz-VFr6f6Sg,351
20
- acp_sdk-0.1.0rc6.dist-info/METADATA,sha256=t5zWJsU-C67HQNzCFyo561s7epz4KQvsFjlqUIkF9xw,2147
21
- acp_sdk-0.1.0rc6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- acp_sdk-0.1.0rc6.dist-info/RECORD,,