acp-sdk 0.0.5__py3-none-any.whl → 0.1.0__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 (79) hide show
  1. acp_sdk/__init__.py +2 -0
  2. acp_sdk/client/__init__.py +1 -0
  3. acp_sdk/client/client.py +178 -0
  4. acp_sdk/models/__init__.py +3 -0
  5. acp_sdk/models/errors.py +23 -0
  6. acp_sdk/models/models.py +192 -0
  7. acp_sdk/models/schemas.py +41 -0
  8. acp_sdk/server/__init__.py +7 -0
  9. acp_sdk/server/agent.py +178 -0
  10. acp_sdk/server/app.py +176 -0
  11. acp_sdk/server/bundle.py +146 -0
  12. acp_sdk/server/context.py +33 -0
  13. acp_sdk/server/errors.py +54 -0
  14. acp_sdk/server/logging.py +16 -0
  15. acp_sdk/server/server.py +166 -0
  16. acp_sdk/server/session.py +21 -0
  17. acp_sdk/server/telemetry.py +57 -0
  18. acp_sdk/server/types.py +6 -0
  19. acp_sdk/server/utils.py +14 -0
  20. acp_sdk/version.py +3 -0
  21. acp_sdk-0.1.0.dist-info/METADATA +113 -0
  22. acp_sdk-0.1.0.dist-info/RECORD +24 -0
  23. acp/__init__.py +0 -138
  24. acp/cli/__init__.py +0 -6
  25. acp/cli/claude.py +0 -139
  26. acp/cli/cli.py +0 -471
  27. acp/client/__init__.py +0 -0
  28. acp/client/__main__.py +0 -79
  29. acp/client/session.py +0 -372
  30. acp/client/sse.py +0 -142
  31. acp/client/stdio.py +0 -153
  32. acp/server/__init__.py +0 -3
  33. acp/server/__main__.py +0 -50
  34. acp/server/highlevel/__init__.py +0 -9
  35. acp/server/highlevel/agents/__init__.py +0 -5
  36. acp/server/highlevel/agents/agent_manager.py +0 -110
  37. acp/server/highlevel/agents/base.py +0 -20
  38. acp/server/highlevel/agents/templates.py +0 -21
  39. acp/server/highlevel/context.py +0 -185
  40. acp/server/highlevel/exceptions.py +0 -25
  41. acp/server/highlevel/prompts/__init__.py +0 -4
  42. acp/server/highlevel/prompts/base.py +0 -167
  43. acp/server/highlevel/prompts/manager.py +0 -50
  44. acp/server/highlevel/prompts/prompt_manager.py +0 -33
  45. acp/server/highlevel/resources/__init__.py +0 -23
  46. acp/server/highlevel/resources/base.py +0 -48
  47. acp/server/highlevel/resources/resource_manager.py +0 -94
  48. acp/server/highlevel/resources/templates.py +0 -80
  49. acp/server/highlevel/resources/types.py +0 -185
  50. acp/server/highlevel/server.py +0 -705
  51. acp/server/highlevel/tools/__init__.py +0 -4
  52. acp/server/highlevel/tools/base.py +0 -83
  53. acp/server/highlevel/tools/tool_manager.py +0 -53
  54. acp/server/highlevel/utilities/__init__.py +0 -1
  55. acp/server/highlevel/utilities/func_metadata.py +0 -210
  56. acp/server/highlevel/utilities/logging.py +0 -43
  57. acp/server/highlevel/utilities/types.py +0 -54
  58. acp/server/lowlevel/__init__.py +0 -3
  59. acp/server/lowlevel/helper_types.py +0 -9
  60. acp/server/lowlevel/server.py +0 -643
  61. acp/server/models.py +0 -17
  62. acp/server/session.py +0 -315
  63. acp/server/sse.py +0 -175
  64. acp/server/stdio.py +0 -83
  65. acp/server/websocket.py +0 -61
  66. acp/shared/__init__.py +0 -0
  67. acp/shared/context.py +0 -14
  68. acp/shared/exceptions.py +0 -14
  69. acp/shared/memory.py +0 -87
  70. acp/shared/progress.py +0 -40
  71. acp/shared/session.py +0 -413
  72. acp/shared/version.py +0 -3
  73. acp/types.py +0 -1258
  74. acp_sdk-0.0.5.dist-info/METADATA +0 -46
  75. acp_sdk-0.0.5.dist-info/RECORD +0 -57
  76. acp_sdk-0.0.5.dist-info/entry_points.txt +0 -2
  77. acp_sdk-0.0.5.dist-info/licenses/LICENSE +0 -22
  78. {acp → acp_sdk}/py.typed +0 -0
  79. {acp_sdk-0.0.5.dist-info → acp_sdk-0.1.0.dist-info}/WHEEL +0 -0
acp_sdk/server/app.py ADDED
@@ -0,0 +1,176 @@
1
+ from collections.abc import AsyncGenerator
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ from contextlib import asynccontextmanager
4
+ from enum import Enum
5
+
6
+ from fastapi import FastAPI, HTTPException, status
7
+ from fastapi.encoders import jsonable_encoder
8
+ from fastapi.responses import JSONResponse, StreamingResponse
9
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
10
+
11
+ from acp_sdk.models import (
12
+ Agent as AgentModel,
13
+ )
14
+ from acp_sdk.models import (
15
+ AgentName,
16
+ AgentReadResponse,
17
+ AgentsListResponse,
18
+ Run,
19
+ RunCancelResponse,
20
+ RunCreateRequest,
21
+ RunCreateResponse,
22
+ RunId,
23
+ RunMode,
24
+ RunReadResponse,
25
+ RunResumeRequest,
26
+ RunResumeResponse,
27
+ SessionId,
28
+ )
29
+ from acp_sdk.models.errors import ACPError
30
+ from acp_sdk.server.agent import Agent
31
+ from acp_sdk.server.bundle import RunBundle
32
+ from acp_sdk.server.errors import (
33
+ RequestValidationError,
34
+ StarletteHTTPException,
35
+ acp_error_handler,
36
+ catch_all_exception_handler,
37
+ http_exception_handler,
38
+ validation_exception_handler,
39
+ )
40
+ from acp_sdk.server.session import Session
41
+ from acp_sdk.server.utils import stream_sse
42
+
43
+
44
+ class Headers(str, Enum):
45
+ RUN_ID = "Run-ID"
46
+
47
+
48
+ def create_app(*agents: Agent) -> FastAPI:
49
+ executor: ThreadPoolExecutor
50
+
51
+ @asynccontextmanager
52
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
53
+ nonlocal executor
54
+ with ThreadPoolExecutor() as exec:
55
+ executor = exec
56
+ yield
57
+
58
+ app = FastAPI(lifespan=lifespan)
59
+
60
+ FastAPIInstrumentor.instrument_app(app)
61
+
62
+ agents: dict[AgentName, Agent] = {agent.name: agent for agent in agents}
63
+ runs: dict[RunId, RunBundle] = {}
64
+ sessions: dict[SessionId, Session] = {}
65
+
66
+ app.exception_handler(ACPError)(acp_error_handler)
67
+ app.exception_handler(StarletteHTTPException)(http_exception_handler)
68
+ app.exception_handler(RequestValidationError)(validation_exception_handler)
69
+ app.exception_handler(Exception)(catch_all_exception_handler)
70
+
71
+ def find_run_bundle(run_id: RunId) -> RunBundle:
72
+ bundle = runs.get(run_id)
73
+ if not bundle:
74
+ raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
75
+ return bundle
76
+
77
+ def find_agent(agent_name: AgentName) -> Agent:
78
+ agent = agents.get(agent_name, None)
79
+ if not agent:
80
+ raise HTTPException(status_code=404, detail=f"Agent {agent_name} not found")
81
+ return agent
82
+
83
+ @app.get("/agents")
84
+ async def list_agents() -> AgentsListResponse:
85
+ return AgentsListResponse(
86
+ agents=[
87
+ AgentModel(name=agent.name, description=agent.description, metadata=agent.metadata)
88
+ for agent in agents.values()
89
+ ]
90
+ )
91
+
92
+ @app.get("/agents/{name}")
93
+ async def read_agent(name: AgentName) -> AgentReadResponse:
94
+ agent = find_agent(name)
95
+ return AgentModel(name=agent.name, description=agent.description, metadata=agent.metadata)
96
+
97
+ @app.post("/runs")
98
+ async def create_run(request: RunCreateRequest) -> RunCreateResponse:
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
103
+ bundle = RunBundle(
104
+ agent=agent,
105
+ run=Run(agent_name=agent.name, session_id=session.id),
106
+ inputs=request.inputs,
107
+ history=list(session.history()),
108
+ executor=executor,
109
+ )
110
+ session.append(bundle)
111
+
112
+ runs[bundle.run.run_id] = bundle
113
+ sessions[session.id] = session
114
+
115
+ headers = {Headers.RUN_ID: str(bundle.run.run_id)}
116
+
117
+ match request.mode:
118
+ case RunMode.STREAM:
119
+ return StreamingResponse(
120
+ stream_sse(bundle),
121
+ headers=headers,
122
+ media_type="text/event-stream",
123
+ )
124
+ case RunMode.SYNC:
125
+ await bundle.join()
126
+ return JSONResponse(
127
+ headers=headers,
128
+ content=jsonable_encoder(bundle.run),
129
+ )
130
+ case RunMode.ASYNC:
131
+ return JSONResponse(
132
+ status_code=status.HTTP_202_ACCEPTED,
133
+ headers=headers,
134
+ content=jsonable_encoder(bundle.run),
135
+ )
136
+ case _:
137
+ raise NotImplementedError()
138
+
139
+ @app.get("/runs/{run_id}")
140
+ async def read_run(run_id: RunId) -> RunReadResponse:
141
+ bundle = find_run_bundle(run_id)
142
+ return bundle.run
143
+
144
+ @app.post("/runs/{run_id}")
145
+ async def resume_run(run_id: RunId, request: RunResumeRequest) -> RunResumeResponse:
146
+ bundle = find_run_bundle(run_id)
147
+ await bundle.resume(request.await_)
148
+ match request.mode:
149
+ case RunMode.STREAM:
150
+ return StreamingResponse(
151
+ stream_sse(bundle),
152
+ media_type="text/event-stream",
153
+ )
154
+ case RunMode.SYNC:
155
+ await bundle.join()
156
+ return bundle.run
157
+ case RunMode.ASYNC:
158
+ return JSONResponse(
159
+ status_code=status.HTTP_202_ACCEPTED,
160
+ content=jsonable_encoder(bundle.run),
161
+ )
162
+ case _:
163
+ raise NotImplementedError()
164
+
165
+ @app.post("/runs/{run_id}/cancel")
166
+ async def cancel_run(run_id: RunId) -> RunCancelResponse:
167
+ bundle = find_run_bundle(run_id)
168
+ if bundle.run.status.is_terminal:
169
+ raise HTTPException(
170
+ status_code=status.HTTP_403_FORBIDDEN,
171
+ detail=f"Run in terminal status {bundle.run.status} can't be cancelled",
172
+ )
173
+ await bundle.cancel()
174
+ return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder(bundle.run))
175
+
176
+ return app
@@ -0,0 +1,146 @@
1
+ import asyncio
2
+ import logging
3
+ from collections.abc import AsyncGenerator
4
+ from concurrent.futures import ThreadPoolExecutor
5
+
6
+ from pydantic import ValidationError
7
+
8
+ from acp_sdk.models import (
9
+ AnyModel,
10
+ Artifact,
11
+ ArtifactEvent,
12
+ Await,
13
+ AwaitEvent,
14
+ AwaitResume,
15
+ CancelledEvent,
16
+ CompletedEvent,
17
+ CreatedEvent,
18
+ Error,
19
+ FailedEvent,
20
+ GenericEvent,
21
+ InProgressEvent,
22
+ Message,
23
+ MessageEvent,
24
+ Run,
25
+ RunEvent,
26
+ RunStatus,
27
+ )
28
+ from acp_sdk.models.errors import ErrorCode
29
+ from acp_sdk.server.agent import Agent
30
+ from acp_sdk.server.logging import logger
31
+ from acp_sdk.server.telemetry import get_tracer
32
+
33
+
34
+ class RunBundle:
35
+ def __init__(
36
+ self, *, agent: Agent, run: Run, inputs: list[Message], history: list[Message], executor: ThreadPoolExecutor
37
+ ) -> None:
38
+ self.agent = agent
39
+ self.run = run
40
+ self.inputs = inputs
41
+ self.history = history
42
+
43
+ self.stream_queue: asyncio.Queue[RunEvent] = asyncio.Queue()
44
+
45
+ self.await_queue: asyncio.Queue[AwaitResume] = asyncio.Queue(maxsize=1)
46
+ self.await_or_terminate_event = asyncio.Event()
47
+
48
+ self.task = asyncio.create_task(self._execute(inputs, executor=executor))
49
+
50
+ async def stream(self) -> AsyncGenerator[RunEvent]:
51
+ while True:
52
+ event = await self.stream_queue.get()
53
+ if event is None:
54
+ break
55
+ yield event
56
+ self.stream_queue.task_done()
57
+
58
+ async def emit(self, event: RunEvent) -> None:
59
+ await self.stream_queue.put(event)
60
+
61
+ async def await_(self) -> AwaitResume:
62
+ await self.stream_queue.put(None)
63
+ self.await_queue.empty()
64
+ self.await_or_terminate_event.set()
65
+ self.await_or_terminate_event.clear()
66
+ resume = await self.await_queue.get()
67
+ self.await_queue.task_done()
68
+ return resume
69
+
70
+ async def resume(self, resume: AwaitResume) -> None:
71
+ self.stream_queue = asyncio.Queue()
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
80
+
81
+ async def join(self) -> None:
82
+ await self.await_or_terminate_event.wait()
83
+
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)})
87
+
88
+ try:
89
+ await self.emit(CreatedEvent(run=self.run))
90
+
91
+ generator = self.agent.execute(
92
+ inputs=self.history + inputs, session_id=self.run.session_id, executor=executor
93
+ )
94
+ run_logger.info("Run started")
95
+
96
+ self.run.status = RunStatus.IN_PROGRESS
97
+ await self.emit(InProgressEvent(run=self.run))
98
+
99
+ await_resume = None
100
+ while True:
101
+ next = await generator.asend(await_resume)
102
+ if isinstance(next, Message):
103
+ self.run.outputs.append(next)
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))
108
+ elif isinstance(next, Await):
109
+ self.run.await_ = next
110
+ self.run.status = RunStatus.AWAITING
111
+ await self.emit(
112
+ AwaitEvent.model_validate(
113
+ {
114
+ "run_id": self.run.run_id,
115
+ "type": "await",
116
+ "await": next,
117
+ }
118
+ )
119
+ )
120
+ run_logger.info("Run awaited")
121
+ await_resume = await self.await_()
122
+ await self.emit(InProgressEvent(run=self.run))
123
+ run_logger.info("Run resumed")
124
+ else:
125
+ try:
126
+ generic = AnyModel.model_validate(next)
127
+ await self.emit(GenericEvent(generic=generic))
128
+ except ValidationError:
129
+ raise TypeError("Invalid yield")
130
+ except StopAsyncIteration:
131
+ self.run.status = RunStatus.COMPLETED
132
+ await self.emit(CompletedEvent(run=self.run))
133
+ run_logger.info("Run completed")
134
+ except asyncio.CancelledError:
135
+ self.run.status = RunStatus.CANCELLED
136
+ await self.emit(CancelledEvent(run=self.run))
137
+ run_logger.info("Run cancelled")
138
+ except Exception as e:
139
+ self.run.error = Error(code=ErrorCode.SERVER_ERROR, message=str(e))
140
+ self.run.status = RunStatus.FAILED
141
+ await self.emit(FailedEvent(run=self.run))
142
+ run_logger.exception("Run failed")
143
+ raise
144
+ finally:
145
+ self.await_or_terminate_event.set()
146
+ await self.stream_queue.put(None)
@@ -0,0 +1,33 @@
1
+ from concurrent.futures import ThreadPoolExecutor
2
+
3
+ import janus
4
+
5
+ from acp_sdk.models import SessionId
6
+ from acp_sdk.server.types import RunYield, RunYieldResume
7
+
8
+
9
+ class Context:
10
+ def __init__(
11
+ self,
12
+ *,
13
+ session_id: SessionId | None = None,
14
+ executor: ThreadPoolExecutor,
15
+ yield_queue: janus.Queue[RunYield],
16
+ yield_resume_queue: janus.Queue[RunYieldResume],
17
+ ) -> None:
18
+ self.session_id = session_id
19
+ self.executor = executor
20
+ self._yield_queue = yield_queue
21
+ self._yield_resume_queue = yield_resume_queue
22
+
23
+ def yield_sync(self, value: RunYield) -> RunYieldResume:
24
+ self._yield_queue.sync_q.put(value)
25
+ return self._yield_resume_queue.sync_q.get()
26
+
27
+ async def yield_async(self, value: RunYield) -> RunYieldResume:
28
+ await self._yield_queue.async_q.put(value)
29
+ return await self._yield_resume_queue.async_q.get()
30
+
31
+ def shutdown(self) -> None:
32
+ self._yield_queue.shutdown()
33
+ self._yield_resume_queue.shutdown()
@@ -0,0 +1,54 @@
1
+ from fastapi import Request, status
2
+ from fastapi.exceptions import RequestValidationError
3
+ from fastapi.responses import JSONResponse
4
+ from starlette.exceptions import HTTPException as StarletteHTTPException
5
+
6
+ from acp_sdk.models import Error, ErrorCode
7
+ from acp_sdk.models.errors import ACPError
8
+ from acp_sdk.server.logging import logger
9
+
10
+
11
+ def error_code_to_status_code(error_code: ErrorCode) -> int:
12
+ match error_code:
13
+ case ErrorCode.NOT_FOUND:
14
+ return status.HTTP_404_NOT_FOUND
15
+ case ErrorCode.INVALID_INPUT:
16
+ return status.HTTP_422_UNPROCESSABLE_ENTITY
17
+ case _:
18
+ return status.HTTP_500_INTERNAL_SERVER_ERROR
19
+
20
+
21
+ def status_code_to_error_code(status_code: int) -> ErrorCode:
22
+ match status_code:
23
+ case status.HTTP_400_BAD_REQUEST:
24
+ return ErrorCode.INVALID_INPUT
25
+ case status.HTTP_404_NOT_FOUND:
26
+ return ErrorCode.NOT_FOUND
27
+ case status.HTTP_422_UNPROCESSABLE_ENTITY:
28
+ return ErrorCode.INVALID_INPUT
29
+ case _:
30
+ return ErrorCode.SERVER_ERROR
31
+
32
+
33
+ async def acp_error_handler(request: Request, exc: ACPError, *, status_code: int | None = None) -> JSONResponse:
34
+ error = exc.error
35
+ return JSONResponse(status_code=status_code or error_code_to_status_code(error.code), content=error.model_dump())
36
+
37
+
38
+ async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
39
+ return await acp_error_handler(
40
+ request,
41
+ ACPError(Error(code=status_code_to_error_code(exc.status_code), message=exc.detail)),
42
+ status_code=exc.status_code,
43
+ )
44
+
45
+
46
+ async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
47
+ return await acp_error_handler(request, ACPError(Error(code=ErrorCode.INVALID_INPUT, message=str(exc))))
48
+
49
+
50
+ async def catch_all_exception_handler(request: Request, exc: Exception) -> JSONResponse:
51
+ logger.error(exc)
52
+ return await acp_error_handler(
53
+ request, ACPError(Error(code=ErrorCode.SERVER_ERROR, message="An unexpected error occurred"))
54
+ )
@@ -0,0 +1,16 @@
1
+ import logging
2
+
3
+ from uvicorn.logging import DefaultFormatter
4
+
5
+ logger = logging.getLogger("acp")
6
+
7
+
8
+ def configure_logger() -> None:
9
+ """Utility that configures the root logger"""
10
+ root_logger = logging.getLogger()
11
+
12
+ handler = logging.StreamHandler()
13
+ handler.setFormatter(DefaultFormatter(fmt="%(levelprefix)s %(message)s"))
14
+
15
+ root_logger.addHandler(handler)
16
+ root_logger.setLevel(logging.INFO)
@@ -0,0 +1,166 @@
1
+ import asyncio
2
+ import os
3
+ from collections.abc import Awaitable
4
+ from typing import Any, Callable
5
+
6
+ import uvicorn
7
+ import uvicorn.config
8
+
9
+ from acp_sdk.models import Metadata
10
+ from acp_sdk.server.agent import Agent
11
+ from acp_sdk.server.agent import agent as agent_decorator
12
+ from acp_sdk.server.app import create_app
13
+ from acp_sdk.server.logging import configure_logger as configure_logger_func
14
+ from acp_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
15
+
16
+
17
+ class Server:
18
+ def __init__(self) -> None:
19
+ self._agents: list[Agent] = []
20
+ self._server: uvicorn.Server | None = None
21
+
22
+ def agent(
23
+ self,
24
+ name: str | None = None,
25
+ description: str | None = None,
26
+ *,
27
+ metadata: Metadata | None = None,
28
+ ) -> Callable:
29
+ """Decorator to register an agent."""
30
+
31
+ def decorator(fn: Callable) -> Callable:
32
+ agent = agent_decorator(name=name, description=description, metadata=metadata)(fn)
33
+ self.register(agent)
34
+ return fn
35
+
36
+ return decorator
37
+
38
+ def register(self, *agents: Agent) -> None:
39
+ self._agents.extend(agents)
40
+
41
+ def run(
42
+ self,
43
+ configure_logger: bool = True,
44
+ configure_telemetry: bool = False,
45
+ host: str = "127.0.0.1",
46
+ port: int = 8000,
47
+ uds: str | None = None,
48
+ fd: int | None = None,
49
+ loop: uvicorn.config.LoopSetupType = "auto",
50
+ http: type[asyncio.Protocol] | uvicorn.config.HTTPProtocolType = "auto",
51
+ ws: type[asyncio.Protocol] | uvicorn.config.WSProtocolType = "auto",
52
+ ws_max_size: int = 16 * 1024 * 1024,
53
+ ws_max_queue: int = 32,
54
+ ws_ping_interval: float | None = 20.0,
55
+ ws_ping_timeout: float | None = 20.0,
56
+ ws_per_message_deflate: bool = True,
57
+ lifespan: uvicorn.config.LifespanType = "auto",
58
+ env_file: str | os.PathLike[str] | None = None,
59
+ log_config: dict[str, Any]
60
+ | str
61
+ | uvicorn.config.RawConfigParser
62
+ | uvicorn.config.IO[Any]
63
+ | None = uvicorn.config.LOGGING_CONFIG,
64
+ log_level: str | int | None = None,
65
+ access_log: bool = True,
66
+ use_colors: bool | None = None,
67
+ interface: uvicorn.config.InterfaceType = "auto",
68
+ reload: bool = False,
69
+ reload_dirs: list[str] | str | None = None,
70
+ reload_delay: float = 0.25,
71
+ reload_includes: list[str] | str | None = None,
72
+ reload_excludes: list[str] | str | None = None,
73
+ workers: int | None = None,
74
+ proxy_headers: bool = True,
75
+ server_header: bool = True,
76
+ date_header: bool = True,
77
+ forwarded_allow_ips: list[str] | str | None = None,
78
+ root_path: str = "",
79
+ limit_concurrency: int | None = None,
80
+ limit_max_requests: int | None = None,
81
+ backlog: int = 2048,
82
+ timeout_keep_alive: int = 5,
83
+ timeout_notify: int = 30,
84
+ timeout_graceful_shutdown: int | None = None,
85
+ callback_notify: Callable[..., Awaitable[None]] | None = None,
86
+ ssl_keyfile: str | os.PathLike[str] | None = None,
87
+ ssl_certfile: str | os.PathLike[str] | None = None,
88
+ ssl_keyfile_password: str | None = None,
89
+ ssl_version: int = uvicorn.config.SSL_PROTOCOL_VERSION,
90
+ ssl_cert_reqs: int = uvicorn.config.ssl.CERT_NONE,
91
+ ssl_ca_certs: str | None = None,
92
+ ssl_ciphers: str = "TLSv1",
93
+ headers: list[tuple[str, str]] | None = None,
94
+ factory: bool = False,
95
+ h11_max_incomplete_event_size: int | None = None,
96
+ ) -> None:
97
+ if self._server:
98
+ raise RuntimeError("The server is already running")
99
+
100
+ import uvicorn
101
+
102
+ if configure_logger:
103
+ configure_logger_func()
104
+ if configure_telemetry:
105
+ configure_telemetry_func()
106
+
107
+ config = uvicorn.Config(
108
+ create_app(*self._agents),
109
+ host,
110
+ port,
111
+ uds,
112
+ fd,
113
+ loop,
114
+ http,
115
+ ws,
116
+ ws_max_size,
117
+ ws_max_queue,
118
+ ws_ping_interval,
119
+ ws_ping_timeout,
120
+ ws_per_message_deflate,
121
+ lifespan,
122
+ env_file,
123
+ log_config,
124
+ log_level,
125
+ access_log,
126
+ use_colors,
127
+ interface,
128
+ reload,
129
+ reload_dirs,
130
+ reload_delay,
131
+ reload_includes,
132
+ reload_excludes,
133
+ workers,
134
+ proxy_headers,
135
+ server_header,
136
+ date_header,
137
+ forwarded_allow_ips,
138
+ root_path,
139
+ limit_concurrency,
140
+ limit_max_requests,
141
+ backlog,
142
+ timeout_keep_alive,
143
+ timeout_notify,
144
+ timeout_graceful_shutdown,
145
+ callback_notify,
146
+ ssl_keyfile,
147
+ ssl_certfile,
148
+ ssl_keyfile_password,
149
+ ssl_version,
150
+ ssl_cert_reqs,
151
+ ssl_ca_certs,
152
+ ssl_ciphers,
153
+ headers,
154
+ factory,
155
+ h11_max_incomplete_event_size,
156
+ )
157
+ self._server = uvicorn.Server(config)
158
+ self._server.run()
159
+
160
+ @property
161
+ def should_exit(self) -> bool:
162
+ return self._server.should_exit if self._server else False
163
+
164
+ @should_exit.setter
165
+ def should_exit(self, value: bool) -> None:
166
+ 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
@@ -0,0 +1,57 @@
1
+ import logging
2
+
3
+ from opentelemetry import metrics, trace
4
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
5
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
6
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
7
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
8
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
9
+ from opentelemetry.sdk.metrics import MeterProvider
10
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
11
+ from opentelemetry.sdk.resources import (
12
+ SERVICE_NAME,
13
+ SERVICE_NAMESPACE,
14
+ SERVICE_VERSION,
15
+ Resource,
16
+ )
17
+ from opentelemetry.sdk.trace import TracerProvider
18
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
19
+
20
+ from acp_sdk.version import __version__
21
+
22
+ root_logger = logging.getLogger()
23
+
24
+
25
+ def configure_telemetry() -> None:
26
+ """Utility that configures opentelemetry with OTLP exporter"""
27
+
28
+ resource = Resource(
29
+ attributes={
30
+ SERVICE_NAME: "acp-server",
31
+ SERVICE_NAMESPACE: "acp",
32
+ SERVICE_VERSION: __version__,
33
+ }
34
+ )
35
+
36
+ # Traces
37
+ provider = TracerProvider(resource=resource)
38
+ processor = BatchSpanProcessor(OTLPSpanExporter())
39
+ provider.add_span_processor(processor)
40
+ trace.set_tracer_provider(provider)
41
+
42
+ # Metrics
43
+ meter_provider = MeterProvider(
44
+ resource=resource,
45
+ metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())],
46
+ )
47
+ metrics.set_meter_provider(meter_provider)
48
+
49
+ # Logs
50
+ logger_provider = LoggerProvider(resource=resource)
51
+ processor = BatchLogRecordProcessor(OTLPLogExporter())
52
+ logger_provider.add_log_record_processor(processor)
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__)
@@ -0,0 +1,6 @@
1
+ from typing import Any
2
+
3
+ from acp_sdk.models import Await, AwaitResume, Message
4
+
5
+ RunYield = Message | Await | dict[str | Any]
6
+ RunYieldResume = AwaitResume | None
@@ -0,0 +1,14 @@
1
+ from collections.abc import AsyncGenerator
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from acp_sdk.server.bundle import RunBundle
6
+
7
+
8
+ def encode_sse(model: BaseModel) -> str:
9
+ return f"data: {model.model_dump_json()}\n\n"
10
+
11
+
12
+ async def stream_sse(bundle: RunBundle) -> AsyncGenerator[str]:
13
+ async for event in bundle.stream():
14
+ yield encode_sse(event)