acp-sdk 0.0.6__py3-none-any.whl → 1.0.0rc2__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.
Files changed (69) hide show
  1. acp_sdk/client/__init__.py +1 -0
  2. acp_sdk/client/client.py +123 -0
  3. acp_sdk/models.py +215 -0
  4. acp_sdk/server/__init__.py +2 -0
  5. acp_sdk/server/agent.py +30 -0
  6. acp_sdk/server/bundle.py +131 -0
  7. acp_sdk/server/context.py +6 -0
  8. acp_sdk/server/server.py +133 -0
  9. acp_sdk/server/telemetry.py +46 -0
  10. acp_sdk/server/utils.py +14 -0
  11. acp_sdk-1.0.0rc2.dist-info/METADATA +53 -0
  12. acp_sdk-1.0.0rc2.dist-info/RECORD +15 -0
  13. acp/__init__.py +0 -138
  14. acp/cli/__init__.py +0 -6
  15. acp/cli/claude.py +0 -139
  16. acp/cli/cli.py +0 -471
  17. acp/client/__main__.py +0 -79
  18. acp/client/session.py +0 -372
  19. acp/client/sse.py +0 -145
  20. acp/client/stdio.py +0 -153
  21. acp/server/__init__.py +0 -3
  22. acp/server/__main__.py +0 -50
  23. acp/server/highlevel/__init__.py +0 -9
  24. acp/server/highlevel/agents/__init__.py +0 -5
  25. acp/server/highlevel/agents/agent_manager.py +0 -110
  26. acp/server/highlevel/agents/base.py +0 -20
  27. acp/server/highlevel/agents/templates.py +0 -21
  28. acp/server/highlevel/context.py +0 -185
  29. acp/server/highlevel/exceptions.py +0 -25
  30. acp/server/highlevel/prompts/__init__.py +0 -4
  31. acp/server/highlevel/prompts/base.py +0 -167
  32. acp/server/highlevel/prompts/manager.py +0 -50
  33. acp/server/highlevel/prompts/prompt_manager.py +0 -33
  34. acp/server/highlevel/resources/__init__.py +0 -23
  35. acp/server/highlevel/resources/base.py +0 -48
  36. acp/server/highlevel/resources/resource_manager.py +0 -94
  37. acp/server/highlevel/resources/templates.py +0 -80
  38. acp/server/highlevel/resources/types.py +0 -185
  39. acp/server/highlevel/server.py +0 -705
  40. acp/server/highlevel/tools/__init__.py +0 -4
  41. acp/server/highlevel/tools/base.py +0 -83
  42. acp/server/highlevel/tools/tool_manager.py +0 -53
  43. acp/server/highlevel/utilities/__init__.py +0 -1
  44. acp/server/highlevel/utilities/func_metadata.py +0 -210
  45. acp/server/highlevel/utilities/logging.py +0 -43
  46. acp/server/highlevel/utilities/types.py +0 -54
  47. acp/server/lowlevel/__init__.py +0 -3
  48. acp/server/lowlevel/helper_types.py +0 -9
  49. acp/server/lowlevel/server.py +0 -643
  50. acp/server/models.py +0 -17
  51. acp/server/session.py +0 -315
  52. acp/server/sse.py +0 -175
  53. acp/server/stdio.py +0 -83
  54. acp/server/websocket.py +0 -61
  55. acp/shared/__init__.py +0 -0
  56. acp/shared/context.py +0 -14
  57. acp/shared/exceptions.py +0 -14
  58. acp/shared/memory.py +0 -87
  59. acp/shared/progress.py +0 -40
  60. acp/shared/session.py +0 -413
  61. acp/shared/version.py +0 -3
  62. acp/types.py +0 -1258
  63. acp_sdk-0.0.6.dist-info/METADATA +0 -46
  64. acp_sdk-0.0.6.dist-info/RECORD +0 -57
  65. acp_sdk-0.0.6.dist-info/entry_points.txt +0 -2
  66. acp_sdk-0.0.6.dist-info/licenses/LICENSE +0 -22
  67. {acp/client → acp_sdk}/__init__.py +0 -0
  68. {acp → acp_sdk}/py.typed +0 -0
  69. {acp_sdk-0.0.6.dist-info → acp_sdk-1.0.0rc2.dist-info}/WHEEL +0 -0
@@ -0,0 +1 @@
1
+ from acp_sdk.client.client import Client as Client
@@ -0,0 +1,123 @@
1
+ from collections.abc import AsyncIterator
2
+ from types import TracebackType
3
+ from typing import Self
4
+
5
+ import httpx
6
+ from httpx_sse import EventSource, aconnect_sse
7
+ from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
8
+ from pydantic import TypeAdapter
9
+
10
+ from acp_sdk.models import (
11
+ Agent,
12
+ AgentName,
13
+ AgentReadResponse,
14
+ AgentsListResponse,
15
+ AwaitResume,
16
+ Message,
17
+ Run,
18
+ RunCancelResponse,
19
+ RunCreateRequest,
20
+ RunCreateResponse,
21
+ RunEvent,
22
+ RunId,
23
+ RunMode,
24
+ RunResumeRequest,
25
+ RunResumeResponse,
26
+ )
27
+
28
+
29
+ class Client:
30
+ def __init__(self, *, base_url: httpx.URL | str = "", client: httpx.AsyncClient | None = None) -> None:
31
+ self.base_url = base_url
32
+
33
+ self._client = self._init_client(client)
34
+
35
+ def _init_client(self, client: httpx.AsyncClient | None = None) -> Self:
36
+ client = client or httpx.AsyncClient(base_url=self.base_url)
37
+ HTTPXClientInstrumentor.instrument_client(client)
38
+ return client
39
+
40
+ async def __aenter__(self) -> Self:
41
+ await self._client.__aenter__()
42
+ return self
43
+
44
+ async def __aexit__(
45
+ self,
46
+ exc_type: type[BaseException] | None = None,
47
+ exc_value: BaseException | None = None,
48
+ traceback: TracebackType | None = None,
49
+ ) -> None:
50
+ await self._client.__aexit__(exc_type, exc_value, traceback)
51
+
52
+ async def agents(self) -> AsyncIterator[Agent]:
53
+ response = await self._client.get("/agents")
54
+ for agent in AgentsListResponse.model_validate(response.json()).agents:
55
+ yield agent
56
+
57
+ async def agent(self, *, name: AgentName) -> Agent:
58
+ response = await self._client.get(f"/agents/{name}")
59
+ return AgentReadResponse.model_validate(response.json())
60
+
61
+ async def run_sync(self, *, agent: AgentName, input: Message) -> Run:
62
+ response = await self._client.post(
63
+ "/runs",
64
+ json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.SYNC).model_dump(),
65
+ )
66
+ return RunCreateResponse.model_validate(response.json())
67
+
68
+ async def run_async(self, *, agent: AgentName, input: Message) -> Run:
69
+ response = await self._client.post(
70
+ "/runs",
71
+ json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.ASYNC).model_dump(),
72
+ )
73
+ return RunCreateResponse.model_validate(response.json())
74
+
75
+ async def run_stream(self, *, agent: AgentName, input: Message) -> AsyncIterator[RunEvent]:
76
+ async with aconnect_sse(
77
+ self._client,
78
+ "POST",
79
+ "/runs",
80
+ json=RunCreateRequest(agent_name=agent, input=input, mode=RunMode.STREAM).model_dump(),
81
+ ) as event_source:
82
+ async for event in self._validate_stream(event_source):
83
+ yield event
84
+
85
+ async def run_status(self, *, run_id: RunId) -> Run:
86
+ response = await self._client.get(f"/runs/{run_id}")
87
+ return Run.model_validate(response.json())
88
+
89
+ async def run_cancel(self, *, run_id: RunId) -> Run:
90
+ response = await self._client.post(f"/runs/{run_id}/cancel")
91
+ return RunCancelResponse.model_validate(response.json())
92
+
93
+ async def run_resume_sync(self, *, run_id: RunId, await_: AwaitResume) -> Run:
94
+ response = await self._client.post(
95
+ f"/runs/{run_id}",
96
+ json=RunResumeRequest(await_=await_, mode=RunMode.SYNC).model_dump(),
97
+ )
98
+ return RunResumeResponse.model_validate(response.json())
99
+
100
+ async def run_resume_async(self, *, run_id: RunId, await_: AwaitResume) -> Run:
101
+ response = await self._client.post(
102
+ f"/runs/{run_id}",
103
+ json=RunResumeRequest(await_=await_, mode=RunMode.ASYNC).model_dump(),
104
+ )
105
+ return RunResumeResponse.model_validate(response.json())
106
+
107
+ async def run_resume_stream(self, *, run_id: RunId, await_: AwaitResume) -> AsyncIterator[RunEvent]:
108
+ async with aconnect_sse(
109
+ self._client,
110
+ "POST",
111
+ f"/runs/{run_id}",
112
+ json=RunResumeRequest(await_=await_, mode=RunMode.STREAM).model_dump(),
113
+ ) as event_source:
114
+ async for event in self._validate_stream(event_source):
115
+ yield event
116
+
117
+ async def _validate_stream(
118
+ self,
119
+ event_source: EventSource,
120
+ ) -> AsyncIterator[RunEvent]:
121
+ async for event in event_source.aiter_sse():
122
+ event = TypeAdapter(RunEvent).validate_json(event.data)
123
+ yield event
acp_sdk/models.py ADDED
@@ -0,0 +1,215 @@
1
+ import uuid
2
+ from collections.abc import Iterator
3
+ from enum import Enum
4
+ from typing import Any, Literal, Union
5
+
6
+ from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel
7
+
8
+
9
+ class ACPError(BaseModel):
10
+ code: str
11
+ message: str
12
+
13
+
14
+ class AnyModel(BaseModel):
15
+ model_config = ConfigDict(extra="allow")
16
+
17
+
18
+ class MessagePartBase(BaseModel):
19
+ type: Literal["text", "image", "artifact"]
20
+
21
+
22
+ class TextMessagePart(MessagePartBase):
23
+ type: Literal["text"] = "text"
24
+ content: str
25
+
26
+
27
+ class ImageMessagePart(MessagePartBase):
28
+ type: Literal["image"] = "image"
29
+ content_url: AnyUrl
30
+
31
+
32
+ class ArtifactMessagePart(MessagePartBase):
33
+ type: Literal["artifact"] = "artifact"
34
+ name: str
35
+ content_url: AnyUrl
36
+
37
+
38
+ MessagePart = Union[TextMessagePart, ImageMessagePart, ArtifactMessagePart]
39
+
40
+
41
+ class Message(RootModel):
42
+ root: list[MessagePart]
43
+
44
+ def __init__(self, *items: MessagePart) -> None:
45
+ super().__init__(root=list(items))
46
+
47
+ def __iter__(self) -> Iterator[MessagePart]:
48
+ return iter(self.root)
49
+
50
+ def __add__(self, other: "Message") -> "Message":
51
+ if not isinstance(other, Message):
52
+ raise TypeError(f"Cannot concatenate Message with {type(other).__name__}")
53
+ return Message(*(self.root + other.root))
54
+
55
+ def __str__(self) -> str:
56
+ return "".join(str(part) for part in self.root if isinstance(part, TextMessagePart))
57
+
58
+
59
+ AgentName = str
60
+ SessionId = str
61
+ RunId = str
62
+
63
+
64
+ class RunMode(str, Enum):
65
+ SYNC = "sync"
66
+ ASYNC = "async"
67
+ STREAM = "stream"
68
+
69
+
70
+ class RunStatus(str, Enum):
71
+ CREATED = "created"
72
+ IN_PROGRESS = "in-progress"
73
+ AWAITING = "awaiting"
74
+ CANCELLING = "cancelling"
75
+ CANCELLED = "cancelled"
76
+ COMPLETED = "completed"
77
+ FAILED = "failed"
78
+
79
+ @property
80
+ def is_terminal(self) -> bool:
81
+ terminal_states = {RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED}
82
+ return self in terminal_states
83
+
84
+
85
+ class Await(BaseModel):
86
+ type: Literal["placeholder"] = "placeholder"
87
+
88
+
89
+ class AwaitResume(BaseModel):
90
+ pass
91
+
92
+
93
+ class Run(BaseModel):
94
+ run_id: RunId = str(uuid.uuid4())
95
+ agent_name: AgentName
96
+ session_id: SessionId | None = None
97
+ status: RunStatus = RunStatus.CREATED
98
+ await_: Await | None = Field(None, alias="await")
99
+ output: Message | None = None
100
+ error: ACPError | None = None
101
+
102
+ model_config = ConfigDict(populate_by_name=True)
103
+
104
+ def model_dump_json(
105
+ self,
106
+ **kwargs: dict[str, Any],
107
+ ) -> str:
108
+ return super().model_dump_json(
109
+ by_alias=True,
110
+ **kwargs,
111
+ )
112
+
113
+
114
+ class MessageEvent(BaseModel):
115
+ type: Literal["message"] = "message"
116
+ message: Message
117
+
118
+
119
+ class AwaitEvent(BaseModel):
120
+ type: Literal["await"] = "await"
121
+ await_: Await | None = Field(alias="await")
122
+
123
+ model_config = ConfigDict(populate_by_name=True)
124
+
125
+ def model_dump_json(
126
+ self,
127
+ **kwargs: dict[str, Any],
128
+ ) -> str:
129
+ return super().model_dump_json(
130
+ by_alias=True,
131
+ **kwargs,
132
+ )
133
+
134
+
135
+ class GenericEvent(BaseModel):
136
+ type: Literal["generic"] = "generic"
137
+ generic: AnyModel
138
+
139
+
140
+ class CreatedEvent(BaseModel):
141
+ type: Literal["created"] = "created"
142
+ run: Run
143
+
144
+
145
+ class InProgressEvent(BaseModel):
146
+ type: Literal["in-progress"] = "in-progress"
147
+ run: Run
148
+
149
+
150
+ class FailedEvent(BaseModel):
151
+ type: Literal["failed"] = "failed"
152
+ run: Run
153
+
154
+
155
+ class CancelledEvent(BaseModel):
156
+ type: Literal["cancelled"] = "cancelled"
157
+ run: Run
158
+
159
+
160
+ class CompletedEvent(BaseModel):
161
+ type: Literal["completed"] = "completed"
162
+ run: Run
163
+
164
+
165
+ RunEvent = Union[
166
+ CreatedEvent,
167
+ InProgressEvent,
168
+ MessageEvent,
169
+ AwaitEvent,
170
+ GenericEvent,
171
+ CancelledEvent,
172
+ FailedEvent,
173
+ CompletedEvent,
174
+ ]
175
+
176
+
177
+ class RunCreateRequest(BaseModel):
178
+ agent_name: AgentName
179
+ session_id: SessionId | None = None
180
+ input: Message
181
+ mode: RunMode = RunMode.SYNC
182
+
183
+
184
+ class RunCreateResponse(Run):
185
+ pass
186
+
187
+
188
+ class RunResumeRequest(BaseModel):
189
+ await_: AwaitResume = Field(alias="await")
190
+ mode: RunMode
191
+
192
+
193
+ class RunResumeResponse(Run):
194
+ pass
195
+
196
+
197
+ class RunReadResponse(Run):
198
+ pass
199
+
200
+
201
+ class RunCancelResponse(Run):
202
+ pass
203
+
204
+
205
+ class Agent(BaseModel):
206
+ name: str
207
+ description: str | None = None
208
+
209
+
210
+ class AgentsListResponse(BaseModel):
211
+ agents: list[Agent]
212
+
213
+
214
+ class AgentReadResponse(Agent):
215
+ pass
@@ -0,0 +1,2 @@
1
+ from acp_sdk.server.agent import Agent as Agent
2
+ from acp_sdk.server.server import create_app as create_app
@@ -0,0 +1,30 @@
1
+ import abc
2
+ from collections.abc import AsyncGenerator
3
+
4
+ from acp_sdk.models import (
5
+ AgentName,
6
+ Await,
7
+ AwaitResume,
8
+ Message,
9
+ SessionId,
10
+ )
11
+ from acp_sdk.server.context import Context
12
+
13
+
14
+ class Agent(abc.ABC):
15
+ @property
16
+ def name(self) -> AgentName:
17
+ return self.__class__.__name__
18
+
19
+ @property
20
+ def description(self) -> str:
21
+ return ""
22
+
23
+ @abc.abstractmethod
24
+ def run(self, input: Message, *, context: Context) -> AsyncGenerator[Message | Await, AwaitResume]:
25
+ pass
26
+
27
+ async def session(self, session_id: SessionId | None) -> SessionId | None:
28
+ if session_id:
29
+ raise NotImplementedError()
30
+ return None
@@ -0,0 +1,131 @@
1
+ import asyncio
2
+ import logging
3
+ from collections.abc import AsyncGenerator
4
+
5
+ from opentelemetry import trace
6
+ from pydantic import ValidationError
7
+
8
+ from acp_sdk.models import (
9
+ ACPError,
10
+ AnyModel,
11
+ Await,
12
+ AwaitEvent,
13
+ AwaitResume,
14
+ CancelledEvent,
15
+ CompletedEvent,
16
+ CreatedEvent,
17
+ FailedEvent,
18
+ GenericEvent,
19
+ InProgressEvent,
20
+ Message,
21
+ MessageEvent,
22
+ Run,
23
+ RunEvent,
24
+ RunStatus,
25
+ )
26
+ from acp_sdk.server.agent import Agent
27
+ from acp_sdk.server.context import Context
28
+
29
+ logger = logging.getLogger("uvicorn.error")
30
+
31
+
32
+ class RunBundle:
33
+ def __init__(self, *, agent: Agent, run: Run, task: asyncio.Task | None = None) -> None:
34
+ self.agent = agent
35
+ self.run = run
36
+ self.task = task
37
+
38
+ self.stream_queue: asyncio.Queue[RunEvent] = asyncio.Queue()
39
+ self.composed_message = Message()
40
+
41
+ self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
42
+ self.await_or_terminate_event = asyncio.Event()
43
+
44
+ async def stream(self) -> AsyncGenerator[RunEvent]:
45
+ while True:
46
+ event = await self.stream_queue.get()
47
+ if event is None:
48
+ break
49
+ yield event
50
+ self.stream_queue.task_done()
51
+
52
+ async def emit(self, event: RunEvent) -> None:
53
+ await self.stream_queue.put(event)
54
+
55
+ async def await_(self) -> AwaitResume:
56
+ await self.stream_queue.put(None)
57
+ self.await_queue.empty()
58
+ self.await_or_terminate_event.set()
59
+ self.await_or_terminate_event.clear()
60
+ resume = await self.await_queue.get()
61
+ self.await_queue.task_done()
62
+ return resume
63
+
64
+ async def resume(self, resume: AwaitResume) -> None:
65
+ self.stream_queue = asyncio.Queue()
66
+ await self.await_queue.put(resume)
67
+
68
+ async def join(self) -> None:
69
+ await self.await_or_terminate_event.wait()
70
+
71
+ async def execute(self, input: Message) -> None:
72
+ with trace.get_tracer(__name__).start_as_current_span("execute"):
73
+ run_logger = logging.LoggerAdapter(logger, {"run_id": self.run.run_id})
74
+
75
+ await self.emit(CreatedEvent(run=self.run))
76
+ try:
77
+ self.run.session_id = await self.agent.session(self.run.session_id)
78
+ run_logger.info("Session loaded")
79
+
80
+ generator = self.agent.run(input=input, context=Context(session_id=self.run.session_id))
81
+ run_logger.info("Run started")
82
+
83
+ self.run.status = RunStatus.IN_PROGRESS
84
+ await self.emit(InProgressEvent(run=self.run))
85
+
86
+ await_resume = None
87
+ while True:
88
+ next = await generator.asend(await_resume)
89
+ if isinstance(next, Message):
90
+ self.composed_message += next
91
+ await self.emit(MessageEvent(message=next))
92
+ elif isinstance(next, Await):
93
+ self.run.await_ = next
94
+ self.run.status = RunStatus.AWAITING
95
+ await self.emit(
96
+ AwaitEvent.model_validate(
97
+ {
98
+ "run_id": self.run.run_id,
99
+ "type": "await",
100
+ "await": next,
101
+ }
102
+ )
103
+ )
104
+ run_logger.info("Run awaited")
105
+ await_resume = await self.await_()
106
+ self.run.status = RunStatus.IN_PROGRESS
107
+ await self.emit(InProgressEvent(run=self.run))
108
+ run_logger.info("Run resumed")
109
+ else:
110
+ try:
111
+ generic = AnyModel.model_validate(next)
112
+ await self.emit(GenericEvent(generic=generic))
113
+ except ValidationError:
114
+ raise TypeError("Invalid yield")
115
+ except StopAsyncIteration:
116
+ self.run.output = self.composed_message
117
+ self.run.status = RunStatus.COMPLETED
118
+ await self.emit(CompletedEvent(run=self.run))
119
+ run_logger.info("Run completed")
120
+ except asyncio.CancelledError:
121
+ self.run.status = RunStatus.CANCELLED
122
+ await self.emit(CancelledEvent(run=self.run))
123
+ run_logger.info("Run cancelled")
124
+ except Exception as e:
125
+ self.run.error = ACPError(code="unspecified", message=str(e))
126
+ self.run.status = RunStatus.FAILED
127
+ await self.emit(FailedEvent(run=self.run))
128
+ run_logger.exception("Run failed")
129
+ finally:
130
+ self.await_or_terminate_event.set()
131
+ await self.stream_queue.put(None)
@@ -0,0 +1,6 @@
1
+ from acp_sdk.models import SessionId
2
+
3
+
4
+ class Context:
5
+ def __init__(self, *, session_id: SessionId | None = None) -> None:
6
+ self.session_id = session_id
@@ -0,0 +1,133 @@
1
+ import asyncio
2
+
3
+ from fastapi import FastAPI, HTTPException, status
4
+ from fastapi.responses import JSONResponse, StreamingResponse
5
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
6
+
7
+ from acp_sdk.models import (
8
+ Agent as AgentModel,
9
+ )
10
+ from acp_sdk.models import (
11
+ AgentName,
12
+ AgentReadResponse,
13
+ AgentsListResponse,
14
+ Run,
15
+ RunCancelResponse,
16
+ RunCreateRequest,
17
+ RunCreateResponse,
18
+ RunId,
19
+ RunMode,
20
+ RunReadResponse,
21
+ RunResumeRequest,
22
+ RunResumeResponse,
23
+ RunStatus,
24
+ )
25
+ from acp_sdk.server.agent import Agent
26
+ from acp_sdk.server.bundle import RunBundle
27
+ from acp_sdk.server.telemetry import configure_telemetry
28
+ from acp_sdk.server.utils import stream_sse
29
+
30
+
31
+ def create_app(*agents: Agent) -> FastAPI:
32
+ app = FastAPI(title="acp-agents")
33
+
34
+ configure_telemetry()
35
+ FastAPIInstrumentor.instrument_app(app)
36
+
37
+ agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
38
+ runs: dict[RunId, RunBundle] = {}
39
+
40
+ def find_run_bundle(run_id: RunId) -> RunBundle:
41
+ bundle = runs.get(run_id)
42
+ if not bundle:
43
+ raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
44
+ return bundle
45
+
46
+ def find_agent(agent_name: AgentName) -> Agent:
47
+ agent = agents.get(agent_name, None)
48
+ if not agent:
49
+ raise HTTPException(status_code=404, detail=f"Agent {agent_name} not found")
50
+ return agent
51
+
52
+ @app.get("/agents")
53
+ async def list_agents() -> AgentsListResponse:
54
+ return AgentsListResponse(
55
+ agents=[AgentModel(name=agent.name, description=agent.description) for agent in agents.values()]
56
+ )
57
+
58
+ @app.get("/agents/{name}")
59
+ async def read_agent(name: AgentName) -> AgentReadResponse:
60
+ agent = find_agent(name)
61
+ return AgentModel(name=agent.name, description=agent.description)
62
+
63
+ @app.post("/runs")
64
+ async def create_run(request: RunCreateRequest) -> RunCreateResponse:
65
+ agent = find_agent(request.agent_name)
66
+ bundle = RunBundle(
67
+ agent=agent,
68
+ run=Run(
69
+ agent_name=agent.name,
70
+ session_id=request.session_id,
71
+ ),
72
+ )
73
+
74
+ bundle.task = asyncio.create_task(bundle.execute(request.input))
75
+ runs[bundle.run.run_id] = bundle
76
+
77
+ match request.mode:
78
+ case RunMode.STREAM:
79
+ return StreamingResponse(
80
+ stream_sse(bundle),
81
+ media_type="text/event-stream",
82
+ )
83
+ case RunMode.SYNC:
84
+ await bundle.join()
85
+ return bundle.run
86
+ case RunMode.ASYNC:
87
+ return JSONResponse(
88
+ status_code=status.HTTP_202_ACCEPTED,
89
+ content=bundle.run.model_dump(),
90
+ )
91
+ case _:
92
+ raise NotImplementedError()
93
+
94
+ @app.get("/runs/{run_id}")
95
+ async def read_run(run_id: RunId) -> RunReadResponse:
96
+ bundle = find_run_bundle(run_id)
97
+ return bundle.run
98
+
99
+ @app.post("/runs/{run_id}")
100
+ async def resume_run(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
101
+ bundle = find_run_bundle(run_id)
102
+ bundle.stream_queue = asyncio.Queue() # TODO improve
103
+ await bundle.await_queue.put(request.await_)
104
+ match request.mode:
105
+ case RunMode.STREAM:
106
+ return StreamingResponse(
107
+ stream_sse(bundle),
108
+ media_type="text/event-stream",
109
+ )
110
+ case RunMode.SYNC:
111
+ await bundle.join()
112
+ return bundle.run
113
+ case RunMode.ASYNC:
114
+ return JSONResponse(
115
+ status_code=status.HTTP_202_ACCEPTED,
116
+ content=bundle.run.model_dump(),
117
+ )
118
+ case _:
119
+ raise NotImplementedError()
120
+
121
+ @app.post("/runs/{run_id}/cancel")
122
+ async def cancel_run(run_id: RunId) -> RunCancelResponse:
123
+ bundle = find_run_bundle(run_id)
124
+ if bundle.run.status.is_terminal:
125
+ raise HTTPException(
126
+ status_code=403,
127
+ detail=f"Run with terminal status {bundle.run.status} can't be cancelled",
128
+ )
129
+ bundle.task.cancel()
130
+ bundle.run.status = RunStatus.CANCELLING
131
+ return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=bundle.run.model_dump())
132
+
133
+ return app
@@ -0,0 +1,46 @@
1
+ import logging
2
+ from importlib.metadata import version
3
+ from typing import Any
4
+
5
+ from opentelemetry import trace
6
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
7
+ from opentelemetry.sdk.resources import (
8
+ SERVICE_NAME,
9
+ SERVICE_NAMESPACE,
10
+ SERVICE_VERSION,
11
+ Resource,
12
+ )
13
+ from opentelemetry.sdk.trace import TracerProvider
14
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExportResult
15
+
16
+ logger = logging.getLogger("uvicorn.error")
17
+
18
+
19
+ class SilentOTLPSpanExporter(OTLPSpanExporter):
20
+ def export(self, spans: Any) -> SpanExportResult:
21
+ try:
22
+ return super().export(spans)
23
+ except Exception as e:
24
+ logger.warning(f"OpenTelemetry Exporter failed silently: {e}")
25
+ return SpanExportResult.FAILURE
26
+
27
+
28
+ def configure_telemetry() -> None:
29
+ current_provider = trace.get_tracer_provider()
30
+
31
+ # Detect default provider and override
32
+ if isinstance(current_provider, trace.ProxyTracerProvider):
33
+ provider = TracerProvider(
34
+ resource=Resource(
35
+ attributes={
36
+ SERVICE_NAME: "acp-server",
37
+ SERVICE_NAMESPACE: "acp",
38
+ SERVICE_VERSION: version("acp-sdk"),
39
+ }
40
+ )
41
+ )
42
+
43
+ processor = BatchSpanProcessor(SilentOTLPSpanExporter())
44
+ provider.add_span_processor(processor)
45
+
46
+ trace.set_tracer_provider(provider)