agentstack-sdk 0.5.2rc2__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 (76) hide show
  1. agentstack_sdk/__init__.py +6 -0
  2. agentstack_sdk/a2a/__init__.py +2 -0
  3. agentstack_sdk/a2a/extensions/__init__.py +8 -0
  4. agentstack_sdk/a2a/extensions/auth/__init__.py +5 -0
  5. agentstack_sdk/a2a/extensions/auth/oauth/__init__.py +4 -0
  6. agentstack_sdk/a2a/extensions/auth/oauth/oauth.py +151 -0
  7. agentstack_sdk/a2a/extensions/auth/oauth/storage/__init__.py +5 -0
  8. agentstack_sdk/a2a/extensions/auth/oauth/storage/base.py +11 -0
  9. agentstack_sdk/a2a/extensions/auth/oauth/storage/memory.py +38 -0
  10. agentstack_sdk/a2a/extensions/auth/secrets/__init__.py +4 -0
  11. agentstack_sdk/a2a/extensions/auth/secrets/secrets.py +77 -0
  12. agentstack_sdk/a2a/extensions/base.py +205 -0
  13. agentstack_sdk/a2a/extensions/common/__init__.py +4 -0
  14. agentstack_sdk/a2a/extensions/common/form.py +149 -0
  15. agentstack_sdk/a2a/extensions/exceptions.py +11 -0
  16. agentstack_sdk/a2a/extensions/interactions/__init__.py +4 -0
  17. agentstack_sdk/a2a/extensions/interactions/approval.py +125 -0
  18. agentstack_sdk/a2a/extensions/services/__init__.py +8 -0
  19. agentstack_sdk/a2a/extensions/services/embedding.py +106 -0
  20. agentstack_sdk/a2a/extensions/services/form.py +54 -0
  21. agentstack_sdk/a2a/extensions/services/llm.py +100 -0
  22. agentstack_sdk/a2a/extensions/services/mcp.py +193 -0
  23. agentstack_sdk/a2a/extensions/services/platform.py +141 -0
  24. agentstack_sdk/a2a/extensions/tools/__init__.py +5 -0
  25. agentstack_sdk/a2a/extensions/tools/call.py +114 -0
  26. agentstack_sdk/a2a/extensions/tools/exceptions.py +6 -0
  27. agentstack_sdk/a2a/extensions/ui/__init__.py +10 -0
  28. agentstack_sdk/a2a/extensions/ui/agent_detail.py +54 -0
  29. agentstack_sdk/a2a/extensions/ui/canvas.py +71 -0
  30. agentstack_sdk/a2a/extensions/ui/citation.py +78 -0
  31. agentstack_sdk/a2a/extensions/ui/error.py +223 -0
  32. agentstack_sdk/a2a/extensions/ui/form_request.py +52 -0
  33. agentstack_sdk/a2a/extensions/ui/settings.py +73 -0
  34. agentstack_sdk/a2a/extensions/ui/trajectory.py +70 -0
  35. agentstack_sdk/a2a/types.py +104 -0
  36. agentstack_sdk/platform/__init__.py +12 -0
  37. agentstack_sdk/platform/client.py +123 -0
  38. agentstack_sdk/platform/common.py +37 -0
  39. agentstack_sdk/platform/configuration.py +47 -0
  40. agentstack_sdk/platform/context.py +291 -0
  41. agentstack_sdk/platform/file.py +295 -0
  42. agentstack_sdk/platform/model_provider.py +131 -0
  43. agentstack_sdk/platform/provider.py +219 -0
  44. agentstack_sdk/platform/provider_build.py +190 -0
  45. agentstack_sdk/platform/types.py +45 -0
  46. agentstack_sdk/platform/user.py +70 -0
  47. agentstack_sdk/platform/user_feedback.py +42 -0
  48. agentstack_sdk/platform/variables.py +44 -0
  49. agentstack_sdk/platform/vector_store.py +217 -0
  50. agentstack_sdk/py.typed +0 -0
  51. agentstack_sdk/server/__init__.py +4 -0
  52. agentstack_sdk/server/agent.py +594 -0
  53. agentstack_sdk/server/app.py +87 -0
  54. agentstack_sdk/server/constants.py +9 -0
  55. agentstack_sdk/server/context.py +68 -0
  56. agentstack_sdk/server/dependencies.py +117 -0
  57. agentstack_sdk/server/exceptions.py +3 -0
  58. agentstack_sdk/server/middleware/__init__.py +3 -0
  59. agentstack_sdk/server/middleware/platform_auth_backend.py +131 -0
  60. agentstack_sdk/server/server.py +376 -0
  61. agentstack_sdk/server/store/__init__.py +3 -0
  62. agentstack_sdk/server/store/context_store.py +35 -0
  63. agentstack_sdk/server/store/memory_context_store.py +59 -0
  64. agentstack_sdk/server/store/platform_context_store.py +58 -0
  65. agentstack_sdk/server/telemetry.py +53 -0
  66. agentstack_sdk/server/utils.py +26 -0
  67. agentstack_sdk/types.py +15 -0
  68. agentstack_sdk/util/__init__.py +4 -0
  69. agentstack_sdk/util/file.py +260 -0
  70. agentstack_sdk/util/httpx.py +18 -0
  71. agentstack_sdk/util/logging.py +63 -0
  72. agentstack_sdk/util/resource_context.py +44 -0
  73. agentstack_sdk/util/utils.py +47 -0
  74. agentstack_sdk-0.5.2rc2.dist-info/METADATA +120 -0
  75. agentstack_sdk-0.5.2rc2.dist-info/RECORD +76 -0
  76. agentstack_sdk-0.5.2rc2.dist-info/WHEEL +4 -0
@@ -0,0 +1,376 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import asyncio
5
+ import functools
6
+ import os
7
+ import re
8
+ import urllib.parse
9
+ from collections.abc import AsyncGenerator, Awaitable, Callable
10
+ from configparser import RawConfigParser
11
+ from contextlib import asynccontextmanager, nullcontext, suppress
12
+ from datetime import timedelta
13
+ from ssl import CERT_NONE
14
+ from typing import IO, Any, Literal
15
+
16
+ import uvicorn
17
+ import uvicorn.config as uvicorn_config
18
+ from a2a.server.agent_execution import RequestContextBuilder
19
+ from a2a.server.events import QueueManager
20
+ from a2a.server.tasks import PushNotificationConfigStore, PushNotificationSender, TaskStore
21
+ from a2a.types import AgentExtension
22
+ from fastapi import FastAPI
23
+ from fastapi.applications import AppType
24
+ from fastapi.responses import PlainTextResponse
25
+ from httpx import HTTPError, HTTPStatusError
26
+ from pydantic import AnyUrl
27
+ from starlette.authentication import AuthenticationBackend, AuthenticationError
28
+ from starlette.middleware.authentication import AuthenticationMiddleware
29
+ from starlette.requests import HTTPConnection
30
+ from starlette.types import Lifespan
31
+ from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_attempt, wait_exponential
32
+
33
+ from agentstack_sdk.platform import get_platform_client
34
+ from agentstack_sdk.platform.client import PlatformClient
35
+ from agentstack_sdk.platform.provider import Provider
36
+ from agentstack_sdk.server.agent import Agent, AgentFactory
37
+ from agentstack_sdk.server.agent import agent as agent_decorator
38
+ from agentstack_sdk.server.store.context_store import ContextStore
39
+ from agentstack_sdk.server.store.memory_context_store import InMemoryContextStore
40
+ from agentstack_sdk.server.telemetry import configure_telemetry as configure_telemetry_func
41
+ from agentstack_sdk.server.utils import cancel_task
42
+ from agentstack_sdk.util.logging import configure_logger as configure_logger_func
43
+ from agentstack_sdk.util.logging import logger
44
+
45
+
46
+ class Server:
47
+ def __init__(self) -> None:
48
+ self._agent_factory: AgentFactory | None = None
49
+ self._agent: Agent | None = None
50
+ self.server: uvicorn.Server | None = None
51
+ self._context_store: ContextStore | None = None
52
+ self._self_registration_client: PlatformClient | None = None
53
+ self._self_registration_id: str | None = None
54
+ self._provider_id: str | None = None
55
+ self._all_configured_variables: set[str] = set()
56
+
57
+ @functools.wraps(agent_decorator)
58
+ def agent(*args, **kwargs) -> Callable:
59
+ self, other_args = args[0], args[1:] # Must hide self due to pyright issues
60
+ if self._agent_factory:
61
+ raise ValueError("Server can have only one agent.")
62
+
63
+ def decorator(fn: Callable) -> Callable:
64
+ self._agent_factory = agent_decorator(*other_args, **kwargs)(fn) # pyright: ignore [reportArgumentType]
65
+ return fn
66
+
67
+ return decorator
68
+
69
+ async def serve(
70
+ self,
71
+ *,
72
+ configure_logger: bool = True,
73
+ configure_telemetry: bool = False,
74
+ self_registration: bool = True,
75
+ self_registration_id: str | None = None,
76
+ task_store: TaskStore | None = None,
77
+ context_store: ContextStore | None = None,
78
+ queue_manager: QueueManager | None = None,
79
+ task_timeout: timedelta = timedelta(minutes=10),
80
+ push_config_store: PushNotificationConfigStore | None = None,
81
+ push_sender: PushNotificationSender | None = None,
82
+ request_context_builder: RequestContextBuilder | None = None,
83
+ host: str = "127.0.0.1",
84
+ port: int = 10000,
85
+ url: str | None = None,
86
+ uds: str | None = None,
87
+ fd: int | None = None,
88
+ loop: Literal["none", "auto", "asyncio", "uvloop"] = "auto",
89
+ http: type[asyncio.Protocol] | uvicorn_config.HTTPProtocolType = "auto",
90
+ ws: type[asyncio.Protocol] | uvicorn_config.WSProtocolType = "auto",
91
+ ws_max_size: int = 16 * 1024 * 1024,
92
+ ws_max_queue: int = 32,
93
+ ws_ping_interval: float | None = 20.0,
94
+ ws_ping_timeout: float | None = 20.0,
95
+ ws_per_message_deflate: bool = True,
96
+ lifespan: uvicorn_config.LifespanType = "auto",
97
+ lifespan_fn: Lifespan[AppType] | None = None,
98
+ env_file: str | os.PathLike[str] | None = None,
99
+ log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = uvicorn_config.LOGGING_CONFIG,
100
+ log_level: str | int | None = None,
101
+ access_log: bool = True,
102
+ use_colors: bool | None = None,
103
+ interface: uvicorn_config.InterfaceType = "auto",
104
+ reload: bool = False,
105
+ reload_dirs: list[str] | str | None = None,
106
+ reload_delay: float = 0.25,
107
+ reload_includes: list[str] | str | None = None,
108
+ reload_excludes: list[str] | str | None = None,
109
+ workers: int | None = None,
110
+ proxy_headers: bool = True,
111
+ server_header: bool = True,
112
+ date_header: bool = True,
113
+ forwarded_allow_ips: list[str] | str | None = None,
114
+ root_path: str = "",
115
+ limit_concurrency: int | None = None,
116
+ limit_max_requests: int | None = None,
117
+ backlog: int = 2048,
118
+ timeout_keep_alive: int = 5,
119
+ timeout_notify: int = 30,
120
+ timeout_worker_healthcheck: int = 5,
121
+ timeout_graceful_shutdown: int | None = None,
122
+ callback_notify: Callable[..., Awaitable[None]] | None = None,
123
+ ssl_keyfile: str | os.PathLike[str] | None = None,
124
+ ssl_certfile: str | os.PathLike[str] | None = None,
125
+ ssl_keyfile_password: str | None = None,
126
+ ssl_version: int = uvicorn_config.SSL_PROTOCOL_VERSION,
127
+ ssl_cert_reqs: int = CERT_NONE,
128
+ ssl_ca_certs: str | None = None,
129
+ ssl_ciphers: str = "TLSv1",
130
+ headers: list[tuple[str, str]] | None = None,
131
+ factory: bool = False,
132
+ h11_max_incomplete_event_size: int | None = None,
133
+ self_registration_client_factory: Callable[[], PlatformClient] | None = None,
134
+ auth_backend: AuthenticationBackend | None = None,
135
+ ) -> None:
136
+ if self.server:
137
+ raise RuntimeError("The server is already running")
138
+ if not self._agent_factory:
139
+ raise ValueError("Agent is not registered")
140
+
141
+ context_store = context_store or InMemoryContextStore()
142
+ self._agent = self._agent_factory(context_store.modify_dependencies)
143
+ card_url = url and url.strip()
144
+ self._agent.card.url = card_url.rstrip("/") if card_url else f"http://{host}:{port}"
145
+
146
+ self._self_registration_client = (
147
+ self_registration_client_factory() if self_registration_client_factory else None
148
+ )
149
+ self._self_registration_id = urllib.parse.quote(self_registration_id or self._agent.card.name)
150
+
151
+ if headers is None:
152
+ headers = [("server", "a2a")]
153
+ elif not any(k.lower() == "server" for k, _ in headers):
154
+ headers.append(("server", "a2a"))
155
+
156
+ import uvicorn
157
+
158
+ from agentstack_sdk.server.app import create_app
159
+
160
+ @asynccontextmanager
161
+ async def _lifespan_fn(app: FastAPI) -> AsyncGenerator[None, None]:
162
+ async with self._self_registration_client or nullcontext():
163
+ register_task = asyncio.create_task(self._register_agent()) if self_registration else None
164
+ reload_task = asyncio.create_task(self._reload_variables_periodically()) if self_registration else None
165
+
166
+ try:
167
+ async with lifespan_fn(app) if lifespan_fn else nullcontext(): # pyright: ignore [reportArgumentType]
168
+ yield
169
+ finally:
170
+ if register_task:
171
+ with suppress(Exception):
172
+ await cancel_task(register_task)
173
+ if reload_task:
174
+ with suppress(Exception):
175
+ await cancel_task(reload_task)
176
+
177
+ card_url = AnyUrl(self._agent.card.url)
178
+ if card_url.host == "invalid":
179
+ self._agent.card.url = f"http://{host}:{port}"
180
+
181
+ if self_registration:
182
+ from agentstack_sdk.a2a.extensions.services.platform import (
183
+ _PlatformSelfRegistrationExtensionParams,
184
+ _PlatformSelfRegistrationExtensionSpec,
185
+ )
186
+
187
+ self._agent.card.capabilities.extensions = [
188
+ *(self._agent.card.capabilities.extensions or []),
189
+ *_PlatformSelfRegistrationExtensionSpec(
190
+ _PlatformSelfRegistrationExtensionParams(self_registration_id=self._self_registration_id)
191
+ ).to_agent_card_extensions(),
192
+ ]
193
+
194
+ app = create_app(
195
+ self._agent,
196
+ lifespan=_lifespan_fn,
197
+ task_store=task_store,
198
+ context_store=context_store,
199
+ queue_manager=queue_manager,
200
+ push_config_store=push_config_store,
201
+ push_sender=push_sender,
202
+ task_timeout=task_timeout,
203
+ request_context_builder=request_context_builder,
204
+ )
205
+
206
+ if auth_backend:
207
+
208
+ def on_error(connection: HTTPConnection, error: AuthenticationError) -> PlainTextResponse:
209
+ return PlainTextResponse("Unauthorized", status_code=401)
210
+
211
+ app.add_middleware(AuthenticationMiddleware, backend=auth_backend, on_error=on_error)
212
+
213
+ if configure_logger:
214
+ configure_logger_func(log_level)
215
+
216
+ if configure_telemetry:
217
+ configure_telemetry_func(app)
218
+
219
+ config = uvicorn.Config(
220
+ app,
221
+ host,
222
+ port,
223
+ uds,
224
+ fd,
225
+ loop,
226
+ http,
227
+ ws,
228
+ ws_max_size,
229
+ ws_max_queue,
230
+ ws_ping_interval,
231
+ ws_ping_timeout,
232
+ ws_per_message_deflate,
233
+ lifespan,
234
+ env_file,
235
+ log_config if not configure_logger else None,
236
+ log_level,
237
+ access_log,
238
+ use_colors,
239
+ interface,
240
+ reload,
241
+ reload_dirs,
242
+ reload_delay,
243
+ reload_includes,
244
+ reload_excludes,
245
+ workers,
246
+ proxy_headers,
247
+ server_header,
248
+ date_header,
249
+ forwarded_allow_ips,
250
+ root_path,
251
+ limit_concurrency,
252
+ limit_max_requests,
253
+ backlog,
254
+ timeout_keep_alive,
255
+ timeout_notify,
256
+ timeout_graceful_shutdown,
257
+ timeout_worker_healthcheck,
258
+ callback_notify,
259
+ ssl_keyfile,
260
+ ssl_certfile,
261
+ ssl_keyfile_password,
262
+ ssl_version,
263
+ ssl_cert_reqs,
264
+ ssl_ca_certs,
265
+ ssl_ciphers,
266
+ headers,
267
+ factory,
268
+ h11_max_incomplete_event_size,
269
+ )
270
+ self.server = uvicorn.Server(config)
271
+ await self.server.serve()
272
+
273
+ @functools.wraps(serve)
274
+ def run(*args, **kwargs) -> None:
275
+ self = args[0] # Must hide self due to pyright issues
276
+ asyncio.run(self.serve(**kwargs))
277
+
278
+ @property
279
+ def should_exit(self) -> bool:
280
+ return self.server.should_exit if self.server else False
281
+
282
+ @should_exit.setter
283
+ def should_exit(self, value: bool) -> None:
284
+ if self.server:
285
+ self.server.should_exit = value
286
+
287
+ @property
288
+ def _platform_url(self) -> str:
289
+ return os.getenv("PLATFORM_URL", "http://127.0.0.1:8333")
290
+
291
+ @property
292
+ def _production_mode(self) -> bool:
293
+ return os.getenv("PRODUCTION_MODE", "").lower() in ["true", "1"]
294
+
295
+ async def _reload_variables_periodically(self):
296
+ while True:
297
+ await asyncio.sleep(5)
298
+ await self._load_variables()
299
+
300
+ async def _load_variables(self, first_run: bool = False) -> None:
301
+ from agentstack_sdk.a2a.extensions import AgentDetail, AgentDetailExtensionSpec
302
+
303
+ assert self.server and self._agent
304
+ if not self._provider_id:
305
+ return
306
+
307
+ variables = await Provider.list_variables(self._provider_id, client=self._self_registration_client)
308
+ old_variables = self._all_configured_variables.copy()
309
+
310
+ for variable in list(self._all_configured_variables - variables.keys()): # reset removed variables
311
+ os.environ.pop(variable, None)
312
+ self._all_configured_variables.remove(variable)
313
+
314
+ os.environ.update(variables)
315
+ self._all_configured_variables.update(variables.keys())
316
+
317
+ if dirty := old_variables != self._all_configured_variables:
318
+ logger.info(f"Environment variables reloaded dynamically: {self._all_configured_variables}")
319
+
320
+ if first_run or dirty:
321
+ for extension in self._agent.card.capabilities.extensions or []:
322
+ match extension:
323
+ case AgentExtension(uri=AgentDetailExtensionSpec.URI, params=params):
324
+ variables = AgentDetail.model_validate(params).variables or []
325
+ if missing_keys := [env for env in variables if env.required and os.getenv(env.name) is None]:
326
+ logger.warning(
327
+ f"Missing required env variables: {missing_keys}, "
328
+ f"add them using `agentstack env add <agent> key=value`"
329
+ )
330
+
331
+ async def _register_agent(self) -> None:
332
+ """If not in PRODUCTION mode, register agent to the agentstack platform and provide missing env variables"""
333
+ assert self.server and self._agent
334
+ if self._production_mode:
335
+ logger.debug("Agent is not automatically registered in the production mode.")
336
+ return
337
+
338
+ host = re.sub(r"localhost|127\.0\.0\.1", "host.docker.internal", self.server.config.host)
339
+ provider_location = f"http://{host}:{self.server.config.port}#{self._self_registration_id}"
340
+ logger.info("Registering agent to the agentstack platform")
341
+ try:
342
+ async for attempt in AsyncRetrying(
343
+ stop=stop_after_attempt(10),
344
+ wait=wait_exponential(max=10),
345
+ retry=retry_if_exception_type(HTTPError),
346
+ reraise=True,
347
+ ):
348
+ with attempt:
349
+ try:
350
+ provider = await Provider.get_by_location(
351
+ location=provider_location, client=self._self_registration_client
352
+ )
353
+ await provider.patch(agent_card=self._agent.card, client=self._self_registration_client)
354
+ except HTTPStatusError as error:
355
+ if error.response.status_code != 404:
356
+ raise
357
+ provider = await Provider.create(
358
+ location=provider_location,
359
+ client=self._self_registration_client,
360
+ agent_card=self._agent.card,
361
+ )
362
+ self._provider_id = provider.id
363
+ logger.debug("Agent registered to the agentstack server.")
364
+ await self._load_variables()
365
+ logger.debug("Environment variables loaded dynamically.")
366
+ logger.info("Agent registered successfully")
367
+ except HTTPStatusError as e:
368
+ with suppress(Exception):
369
+ if error_message := e.response.json().get("detail"):
370
+ logger.info(f"Agent can not be registered to agentstack server: {error_message}")
371
+ return
372
+ logger.info(f"Agent can not be registered to agentstack server: {e}")
373
+ except HTTPError as e:
374
+ logger.info(f"Can not reach server, check if running on {get_platform_client().base_url} : {e}")
375
+ except Exception as e:
376
+ logger.info(f"Agent can not be registered to agentstack server: {e}")
@@ -0,0 +1,3 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
@@ -0,0 +1,35 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import abc
7
+ from collections.abc import AsyncIterator
8
+ from typing import TYPE_CHECKING, Protocol
9
+ from uuid import UUID
10
+
11
+ from a2a.types import Artifact, Message
12
+
13
+ from agentstack_sdk.platform.context import ContextHistoryItem
14
+
15
+ if TYPE_CHECKING:
16
+ from agentstack_sdk.server.dependencies import Dependency, Depends
17
+
18
+
19
+ class ContextStoreInstance(Protocol):
20
+ async def load_history(
21
+ self, load_history_items: bool = False
22
+ ) -> AsyncIterator[ContextHistoryItem | Message | Artifact]:
23
+ yield ... # type: ignore
24
+
25
+ async def store(self, data: Message | Artifact) -> None: ...
26
+
27
+ async def delete_history_from_id(self, from_id: UUID) -> None: ...
28
+
29
+
30
+ class ContextStore(abc.ABC):
31
+ def modify_dependencies(self, dependencies: dict[str, Depends]) -> None:
32
+ return
33
+
34
+ @abc.abstractmethod
35
+ async def create(self, context_id: str, initialized_dependencies: list[Dependency]) -> ContextStoreInstance: ...
@@ -0,0 +1,59 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from collections.abc import AsyncIterator
5
+ from datetime import timedelta
6
+ from uuid import UUID
7
+
8
+ from a2a.types import Artifact, Message
9
+ from cachetools import TTLCache
10
+
11
+ from agentstack_sdk.platform.context import ContextHistoryItem
12
+ from agentstack_sdk.server.dependencies import Dependency
13
+ from agentstack_sdk.server.store.context_store import ContextStore, ContextStoreInstance
14
+
15
+
16
+ class MemoryContextStoreInstance(ContextStoreInstance):
17
+ def __init__(self, context_id: str):
18
+ self.context_id = context_id
19
+ self._history: list[ContextHistoryItem] = []
20
+
21
+ async def load_history(
22
+ self, load_history_items: bool = False
23
+ ) -> AsyncIterator[ContextHistoryItem | Message | Artifact]:
24
+ for item in self._history.copy():
25
+ if load_history_items:
26
+ yield item.model_copy(deep=True)
27
+ else:
28
+ yield item.data.model_copy(deep=True)
29
+
30
+ async def store(self, data: Message | Artifact) -> None:
31
+ self._history.append(ContextHistoryItem(data=data.model_copy(deep=True), context_id=self.context_id))
32
+
33
+ async def delete_history_from_id(self, from_id: UUID) -> None:
34
+ # Does not allow to delete from an artifact onwards
35
+ index = next(
36
+ (i for i, item in enumerate(self._history) if item.id == from_id),
37
+ None,
38
+ )
39
+ if index is not None:
40
+ self._history = self._history[:index]
41
+
42
+
43
+ class InMemoryContextStore(ContextStore):
44
+ def __init__(self, max_contexts: int = 1000, context_ttl: timedelta = timedelta(hours=1)):
45
+ """
46
+ Initialize in-memory context store with TTL cache.
47
+
48
+ Args:
49
+ max_contexts: Maximum number of contexts to keep in memory
50
+ ttl_seconds: Time-to-live for context instances in seconds (default: 1 hour)
51
+ """
52
+ self._instances: TTLCache[str, MemoryContextStoreInstance] = TTLCache(
53
+ maxsize=max_contexts, ttl=context_ttl.total_seconds()
54
+ )
55
+
56
+ async def create(self, context_id: str, initialized_dependencies: list[Dependency]) -> ContextStoreInstance:
57
+ if context_id not in self._instances:
58
+ self._instances[context_id] = MemoryContextStoreInstance(context_id)
59
+ return self._instances[context_id]
@@ -0,0 +1,58 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from collections.abc import AsyncIterator
5
+ from uuid import UUID
6
+
7
+ from a2a.types import Artifact, Message
8
+
9
+ from agentstack_sdk.a2a.extensions.services.platform import (
10
+ PlatformApiExtensionServer,
11
+ PlatformApiExtensionSpec,
12
+ )
13
+ from agentstack_sdk.platform.context import Context, ContextHistoryItem
14
+ from agentstack_sdk.server.constants import _IMPLICIT_DEPENDENCY_PREFIX
15
+ from agentstack_sdk.server.dependencies import Dependency, Depends
16
+ from agentstack_sdk.server.store.context_store import ContextStore, ContextStoreInstance
17
+
18
+
19
+ class PlatformContextStore(ContextStore):
20
+ def modify_dependencies(self, dependencies: dict[str, Depends]) -> None:
21
+ for dependency in dependencies.values():
22
+ if dependency.extension is None:
23
+ continue
24
+ if dependency.extension.spec.URI == PlatformApiExtensionSpec.URI:
25
+ dependency.extension.spec.required = True
26
+ break
27
+ else:
28
+ dependencies[f"{_IMPLICIT_DEPENDENCY_PREFIX}_{PlatformApiExtensionSpec.URI}"] = Depends(
29
+ PlatformApiExtensionServer(PlatformApiExtensionSpec())
30
+ )
31
+
32
+ async def create(self, context_id: str, initialized_dependencies: list[Dependency]) -> ContextStoreInstance:
33
+ [platform_ext] = [d for d in initialized_dependencies if isinstance(d, PlatformApiExtensionServer)]
34
+ return PlatformContextStoreInstance(context_id=context_id, platform_extension=platform_ext)
35
+
36
+
37
+ class PlatformContextStoreInstance(ContextStoreInstance):
38
+ def __init__(self, context_id: str, platform_extension: PlatformApiExtensionServer):
39
+ self._context_id = context_id
40
+ self._platform_extension = platform_extension
41
+
42
+ async def load_history(
43
+ self, load_history_items: bool = False
44
+ ) -> AsyncIterator[ContextHistoryItem | Message | Artifact]:
45
+ async with self._platform_extension.use_client():
46
+ async for history_item in Context.list_all_history(self._context_id):
47
+ if load_history_items:
48
+ yield history_item
49
+ else:
50
+ yield history_item.data
51
+
52
+ async def store(self, data: Message | Artifact) -> None:
53
+ async with self._platform_extension.use_client():
54
+ await Context.add_history_item(self._context_id, data=data)
55
+
56
+ async def delete_history_from_id(self, from_id: UUID) -> None:
57
+ async with self._platform_extension.use_client():
58
+ await Context.delete_history_from_id(self._context_id, from_id=from_id)
@@ -0,0 +1,53 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+
6
+ from fastapi import FastAPI
7
+ from opentelemetry import metrics, trace
8
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
9
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
10
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
11
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
12
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
13
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
14
+ from opentelemetry.sdk.metrics import MeterProvider
15
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
16
+ from opentelemetry.sdk.resources import (
17
+ SERVICE_NAME,
18
+ SERVICE_VERSION,
19
+ Resource,
20
+ )
21
+ from opentelemetry.sdk.trace import TracerProvider
22
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
23
+
24
+ from agentstack_sdk import __version__
25
+
26
+ root_logger = logging.getLogger()
27
+
28
+
29
+ def configure_telemetry(app: FastAPI) -> None:
30
+ """Utility that configures opentelemetry with OTLP exporter and FastAPI instrumentation"""
31
+
32
+ FastAPIInstrumentor.instrument_app(app)
33
+
34
+ resource = Resource(attributes={SERVICE_NAME: "agentstack-sdk-a2a-server", SERVICE_VERSION: __version__})
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))
@@ -0,0 +1,26 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import asyncio
5
+ from asyncio import CancelledError
6
+ from contextlib import suppress
7
+
8
+ from a2a.server.events import QueueManager
9
+
10
+
11
+ async def cancel_task(task: asyncio.Task):
12
+ task.cancel()
13
+ with suppress(CancelledError):
14
+ await task
15
+
16
+
17
+ async def close_queue(queue_manager: QueueManager, queue_name: str, immediate: bool = False):
18
+ """Closes a queue without blocking the QueueManager
19
+
20
+ By default, QueueManager.close() will block all QueueManager operations (creating new queues, etc)
21
+ until all queue events are processed. This can have unexpected side effects, we avoid this by closing queue
22
+ independently and then removing it from queue_manager
23
+ """
24
+ if queue := await queue_manager.get(queue_name):
25
+ await queue.close(immediate=immediate)
26
+ await queue_manager.close(queue_name)
@@ -0,0 +1,15 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from typing import TYPE_CHECKING, TypeAlias
5
+
6
+ if TYPE_CHECKING:
7
+ JsonValue: TypeAlias = list["JsonValue"] | dict[str, "JsonValue"] | str | bool | int | float | None
8
+ JsonDict: TypeAlias = dict[str, JsonValue]
9
+ else:
10
+ from typing import Union
11
+
12
+ from typing_extensions import TypeAliasType
13
+
14
+ JsonValue = TypeAliasType("JsonValue", "Union[dict[str, JsonValue], list[JsonValue], str, int, float, bool, None]") # noqa: UP007
15
+ JsonDict = TypeAliasType("JsonDict", "dict[str, JsonValue]")
@@ -0,0 +1,4 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .resource_context import *