agentstack-sdk 0.4.2rc9__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/PKG-INFO +1 -1
  2. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/pyproject.toml +2 -3
  3. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/__init__.py +1 -0
  4. agentstack_sdk-0.4.3/src/agentstack_sdk/a2a/extensions/tools/__init__.py +5 -0
  5. agentstack_sdk-0.4.3/src/agentstack_sdk/a2a/extensions/tools/call.py +114 -0
  6. agentstack_sdk-0.4.3/src/agentstack_sdk/a2a/extensions/tools/exceptions.py +6 -0
  7. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/ui/__init__.py +1 -0
  8. agentstack_sdk-0.4.3/src/agentstack_sdk/a2a/extensions/ui/error.py +213 -0
  9. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/types.py +7 -1
  10. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/vector_store.py +14 -2
  11. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/agent.py +131 -113
  12. agentstack_sdk-0.4.3/src/agentstack_sdk/server/constants.py +12 -0
  13. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/context.py +5 -0
  14. agentstack_sdk-0.4.2rc9/src/agentstack_sdk/server/constants.py +0 -8
  15. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/README.md +0 -0
  16. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/__init__.py +0 -0
  17. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/__init__.py +0 -0
  18. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/auth/__init__.py +0 -0
  19. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/auth/oauth/__init__.py +0 -0
  20. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/auth/oauth/oauth.py +0 -0
  21. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/auth/oauth/storage/__init__.py +0 -0
  22. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/auth/oauth/storage/base.py +0 -0
  23. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/auth/oauth/storage/memory.py +0 -0
  24. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/auth/secrets/__init__.py +0 -0
  25. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/auth/secrets/secrets.py +0 -0
  26. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/base.py +0 -0
  27. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/common/__init__.py +0 -0
  28. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/common/form.py +0 -0
  29. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/exceptions.py +0 -0
  30. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/services/__init__.py +0 -0
  31. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/services/embedding.py +0 -0
  32. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/services/form.py +0 -0
  33. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/services/llm.py +0 -0
  34. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/services/mcp.py +0 -0
  35. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/services/platform.py +0 -0
  36. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/ui/agent_detail.py +0 -0
  37. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/ui/canvas.py +0 -0
  38. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/ui/citation.py +0 -0
  39. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/ui/form_request.py +0 -0
  40. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/ui/settings.py +0 -0
  41. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/a2a/extensions/ui/trajectory.py +0 -0
  42. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/__init__.py +0 -0
  43. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/client.py +0 -0
  44. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/common.py +0 -0
  45. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/configuration.py +0 -0
  46. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/context.py +0 -0
  47. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/file.py +0 -0
  48. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/model_provider.py +0 -0
  49. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/provider.py +0 -0
  50. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/provider_build.py +0 -0
  51. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/types.py +0 -0
  52. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/user.py +0 -0
  53. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/platform/variables.py +0 -0
  54. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/py.typed +0 -0
  55. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/__init__.py +0 -0
  56. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/app.py +0 -0
  57. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/dependencies.py +0 -0
  58. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/server.py +0 -0
  59. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/store/__init__.py +0 -0
  60. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/store/context_store.py +0 -0
  61. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/store/memory_context_store.py +0 -0
  62. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/store/platform_context_store.py +0 -0
  63. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/telemetry.py +0 -0
  64. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/server/utils.py +0 -0
  65. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/util/__init__.py +0 -0
  66. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/util/file.py +0 -0
  67. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/util/httpx.py +0 -0
  68. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/util/logging.py +0 -0
  69. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/util/resource_context.py +0 -0
  70. {agentstack_sdk-0.4.2rc9 → agentstack_sdk-0.4.3}/src/agentstack_sdk/util/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: agentstack-sdk
3
- Version: 0.4.2rc9
3
+ Version: 0.4.3
4
4
  Summary: Agent Stack SDK
5
5
  Author: IBM Corp.
6
6
  Requires-Dist: a2a-sdk==0.3.9
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentstack-sdk"
3
- version = "0.4.2-rc9"
3
+ version = "0.4.3"
4
4
  description = "Agent Stack SDK"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "IBM Corp." }]
@@ -26,8 +26,7 @@ dependencies = [
26
26
 
27
27
  [dependency-groups]
28
28
  dev = [
29
- "beeai-framework[duckduckgo,wikipedia]>=0.1.68",
30
- "ddgs>=9.9.1",
29
+ "beeai-framework[duckduckgo,wikipedia]>=0.1.70",
31
30
  "pyright>=1.1.403",
32
31
  "pytest>=8.4.1",
33
32
  "pytest-asyncio>=1.1.0",
@@ -3,4 +3,5 @@
3
3
 
4
4
  from .auth import *
5
5
  from .services import *
6
+ from .tools import *
6
7
  from .ui import *
@@ -0,0 +1,5 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .call import *
5
+ from .exceptions import *
@@ -0,0 +1,114 @@
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 uuid
7
+ from types import NoneType
8
+ from typing import TYPE_CHECKING, Any, Literal
9
+
10
+ import a2a.types
11
+ from mcp import Tool
12
+ from mcp.types import Implementation
13
+ from pydantic import BaseModel, Field
14
+
15
+ from agentstack_sdk.a2a.extensions.base import BaseExtensionClient, BaseExtensionServer, BaseExtensionSpec
16
+ from agentstack_sdk.a2a.extensions.tools.exceptions import ToolCallRejectionError
17
+ from agentstack_sdk.a2a.types import AgentMessage, InputRequired
18
+
19
+ if TYPE_CHECKING:
20
+ from agentstack_sdk.server.context import RunContext
21
+
22
+
23
+ class ToolCallServer(BaseModel):
24
+ name: str = Field(description="The programmatic name of the server.")
25
+ title: str | None = Field(description="A human-readable title for the server.")
26
+ version: str = Field(description="The version of the server.")
27
+
28
+
29
+ class ToolCallRequest(BaseModel):
30
+ name: str = Field(description="The programmatic name of the tool.")
31
+ title: str | None = Field(None, description="A human-readable title for the tool.")
32
+ description: str | None = Field(None, description="A human-readable description of the tool.")
33
+
34
+ input: dict[str, Any] | None = Field(description="The input for the tool.")
35
+
36
+ server: ToolCallServer | None = Field(None, description="The server executing the tool.")
37
+
38
+ @staticmethod
39
+ def from_mcp_tool(
40
+ tool: Tool, input: dict[str, Any] | None, server: Implementation | None = None
41
+ ) -> ToolCallRequest:
42
+ return ToolCallRequest(
43
+ name=tool.name,
44
+ title=tool.annotations.title if tool.annotations else None,
45
+ description=tool.description,
46
+ input=input,
47
+ server=ToolCallServer(name=server.name, title=server.title, version=server.version) if server else None,
48
+ )
49
+
50
+
51
+ class ToolCallResponse(BaseModel):
52
+ action: Literal["accept", "reject"]
53
+
54
+
55
+ class ToolCallExtensionParams(BaseModel):
56
+ pass
57
+
58
+
59
+ class ToolCallExtensionSpec(BaseExtensionSpec[ToolCallExtensionParams]):
60
+ URI: str = "https://a2a-extensions.agentstack.beeai.dev/tools/call/v1"
61
+
62
+
63
+ class ToolCallExtensionMetadata(BaseModel):
64
+ pass
65
+
66
+
67
+ class ToolCallExtensionServer(BaseExtensionServer[ToolCallExtensionSpec, ToolCallExtensionMetadata]):
68
+ def create_request_message(self, *, request: ToolCallRequest):
69
+ return AgentMessage(
70
+ text="Tool call approval requested", metadata={self.spec.URI: request.model_dump(mode="json")}
71
+ )
72
+
73
+ def parse_response(self, *, message: a2a.types.Message):
74
+ if not message or not message.metadata or not (data := message.metadata.get(self.spec.URI)):
75
+ raise RuntimeError("Invalid mcp response")
76
+ return ToolCallResponse.model_validate(data)
77
+
78
+ async def request_tool_call_approval(
79
+ self,
80
+ request: ToolCallRequest,
81
+ *,
82
+ context: RunContext,
83
+ ) -> ToolCallResponse:
84
+ message = self.create_request_message(request=request)
85
+ message = await context.yield_async(InputRequired(message=message))
86
+ if message:
87
+ result = self.parse_response(message=message)
88
+ match result.action:
89
+ case "accept":
90
+ return result
91
+ case "reject":
92
+ raise ToolCallRejectionError("User has rejected the tool call")
93
+
94
+ else:
95
+ raise RuntimeError("Yield did not return a message")
96
+
97
+
98
+ class ToolCallExtensionClient(BaseExtensionClient[ToolCallExtensionSpec, NoneType]):
99
+ def create_response_message(self, *, response: ToolCallResponse, task_id: str | None):
100
+ return a2a.types.Message(
101
+ message_id=str(uuid.uuid4()),
102
+ role=a2a.types.Role.user,
103
+ parts=[],
104
+ task_id=task_id,
105
+ metadata={self.spec.URI: response.model_dump(mode="json")},
106
+ )
107
+
108
+ def parse_request(self, *, message: a2a.types.Message):
109
+ if not message or not message.metadata or not (data := message.metadata.get(self.spec.URI)):
110
+ raise ValueError("Invalid tool call request")
111
+ return ToolCallRequest.model_validate(data)
112
+
113
+ def metadata(self) -> dict[str, Any]:
114
+ return {self.spec.URI: ToolCallExtensionMetadata().model_dump(mode="json")}
@@ -0,0 +1,6 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ class ToolCallRejectionError(RuntimeError):
6
+ pass
@@ -3,6 +3,7 @@
3
3
 
4
4
  from .agent_detail import *
5
5
  from .citation import *
6
+ from .error import *
6
7
  from .form_request import *
7
8
  from .settings import *
8
9
  from .trajectory import *
@@ -0,0 +1,213 @@
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 contextvars
7
+ import json
8
+ import logging
9
+ import traceback
10
+ from collections.abc import AsyncIterator
11
+ from contextlib import asynccontextmanager
12
+ from types import NoneType
13
+ from typing import Any
14
+
15
+ import pydantic
16
+
17
+ from agentstack_sdk.a2a.extensions.base import (
18
+ BaseExtensionClient,
19
+ BaseExtensionServer,
20
+ BaseExtensionSpec,
21
+ )
22
+ from agentstack_sdk.a2a.types import AgentMessage, JsonDict, Metadata
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class Error(pydantic.BaseModel):
28
+ """
29
+ Represents error information for displaying exceptions in the UI.
30
+
31
+ This extension helps display errors in a user-friendly way with:
32
+ - A clear error title (exception type)
33
+ - A descriptive error message
34
+
35
+ Visually, this may appear as an error card in the UI.
36
+
37
+ Properties:
38
+ - title: Title of the error (typically the exception class name).
39
+ - message: The error message describing what went wrong.
40
+ """
41
+
42
+ title: str
43
+ message: str
44
+
45
+
46
+ class ErrorGroup(pydantic.BaseModel):
47
+ """
48
+ Represents a group of errors.
49
+
50
+ Properties:
51
+ - message: A message describing the group of errors.
52
+ - errors: A list of error objects.
53
+ """
54
+
55
+ message: str
56
+ errors: list[Error]
57
+
58
+
59
+ class ErrorMetadata(pydantic.BaseModel):
60
+ """
61
+ Metadata containing an error (or group of errors) and an optional stack trace.
62
+
63
+ Properties:
64
+ - error: The error object or group of errors.
65
+ - stack_trace: Optional formatted stack trace for debugging.
66
+ - context: Optional context dictionary.
67
+ """
68
+
69
+ error: Error | ErrorGroup
70
+ stack_trace: str | None = None
71
+ context: JsonDict | None = None
72
+
73
+
74
+ class ErrorExtensionParams(pydantic.BaseModel):
75
+ """
76
+ Configuration parameters for the error extension.
77
+
78
+ Properties:
79
+ - include_stacktrace: Whether to include stack traces in error messages (default: False).
80
+ """
81
+
82
+ include_stacktrace: bool = False
83
+
84
+
85
+ class ErrorExtensionSpec(BaseExtensionSpec[ErrorExtensionParams]):
86
+ URI: str = "https://a2a-extensions.agentstack.beeai.dev/ui/error/v1"
87
+
88
+
89
+ def _format_stacktrace(exc: BaseException, include_cause: bool = True) -> str:
90
+ """Format exception with full traceback including nested causes."""
91
+ return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__, chain=include_cause))
92
+
93
+
94
+ def _extract_error(exc: BaseException) -> Error | ErrorGroup:
95
+ """
96
+ Extract error information from an exception, handling:
97
+ - BaseExceptionGroup (returns ErrorGroup)
98
+ - FrameworkError from beeai_framework (uses .explain() method)
99
+ """
100
+ # Handle BaseExceptionGroup by recursively extracting errors from each exception
101
+ if isinstance(exc, BaseExceptionGroup):
102
+ errors: list[Error] = []
103
+ for sub_exc in exc.exceptions:
104
+ extracted = _extract_error(sub_exc)
105
+ if isinstance(extracted, ErrorGroup):
106
+ errors.extend(extracted.errors)
107
+ else:
108
+ errors.append(extracted)
109
+ return ErrorGroup(message=str(exc), errors=errors)
110
+
111
+ # Try to handle FrameworkError if beeai_framework is available
112
+ try:
113
+ from beeai_framework.errors import FrameworkError
114
+
115
+ if isinstance(exc, FrameworkError):
116
+ # FrameworkError has special .explain() method
117
+ return Error(title=exc.name(), message=exc.explain())
118
+ except ImportError:
119
+ # beeai_framework not installed, continue with standard handling
120
+ pass
121
+
122
+ return Error(title=type(exc).__name__, message=str(exc))
123
+
124
+
125
+ class ErrorExtensionServer(BaseExtensionServer[ErrorExtensionSpec, NoneType]):
126
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
127
+ super().__init__(*args, **kwargs)
128
+ # Server-scoped ContextVar for request-scoped error context
129
+ self._error_context_var: contextvars.ContextVar[JsonDict] = contextvars.ContextVar("error_context")
130
+
131
+ @asynccontextmanager
132
+ async def lifespan(self) -> AsyncIterator[None]:
133
+ """Set up request-scoped error context using ContextVar."""
134
+ # Set an empty dict for this request's context
135
+ token = self._error_context_var.set({})
136
+
137
+ try:
138
+ yield
139
+ finally:
140
+ self._error_context_var.reset(token)
141
+
142
+ @property
143
+ def context(self) -> JsonDict:
144
+ """Get the current request's error context."""
145
+ try:
146
+ return self._error_context_var.get()
147
+ except LookupError:
148
+ # Fallback for when lifespan hasn't been entered yet
149
+ logger.warning(
150
+ "Attempted to use error context when the error extension is not initialized. Make sure to add the ErrorExtensionServer to the agent dependencies."
151
+ )
152
+ return {}
153
+
154
+ def error_metadata(self, error: BaseException) -> Metadata[str, Any]:
155
+ """
156
+ Create metadata for an error.
157
+
158
+ Args:
159
+ error: The exception to convert to metadata
160
+
161
+ Returns:
162
+ Metadata dictionary with error information
163
+ """
164
+ error_data = _extract_error(error)
165
+ stack_trace = _format_stacktrace(error) if self.spec.params.include_stacktrace else None
166
+ return Metadata(
167
+ {
168
+ self.spec.URI: ErrorMetadata(
169
+ error=error_data,
170
+ stack_trace=stack_trace,
171
+ context=self.context or None,
172
+ ).model_dump(mode="json")
173
+ }
174
+ )
175
+
176
+ def message(
177
+ self,
178
+ error: BaseException,
179
+ ) -> AgentMessage:
180
+ """
181
+ Create an AgentMessage with error metadata and serialized text representation.
182
+
183
+ Args:
184
+ error: The exception to include in the message
185
+
186
+ Returns:
187
+ AgentMessage with error metadata and markdown-formatted text
188
+ """
189
+ metadata = self.error_metadata(error)
190
+ error_metadata = ErrorMetadata.model_validate(metadata[self.spec.URI])
191
+
192
+ # Serialize to markdown for display
193
+ text_lines: list[str] = []
194
+ if isinstance(error_metadata.error, ErrorGroup):
195
+ text_lines.append(f"## {error_metadata.error.message}\n")
196
+ for err in error_metadata.error.errors:
197
+ text_lines.append(f"### {err.title}\n{err.message}")
198
+ else:
199
+ text_lines.append(f"## {error_metadata.error.title}\n{error_metadata.error.message}")
200
+
201
+ # Add context if present
202
+ if error_metadata.context:
203
+ text_lines.append(f"## Context\n```json\n{json.dumps(error_metadata.context, indent=2)}\n```")
204
+
205
+ if error_metadata.stack_trace:
206
+ text_lines.append(f"## Stack Trace\n```\n{error_metadata.stack_trace}\n```")
207
+
208
+ text = "\n\n".join(text_lines)
209
+
210
+ return AgentMessage(text=text, metadata=metadata)
211
+
212
+
213
+ class ErrorExtensionClient(BaseExtensionClient[ErrorExtensionSpec, ErrorMetadata]): ...
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  import typing
4
4
  import uuid
5
- from typing import Generic, Literal, TypeAlias
5
+ from typing import Generic, Literal, TypeAlias, TypeAliasType, Union
6
6
 
7
7
  from a2a.types import (
8
8
  Artifact,
@@ -104,3 +104,9 @@ class InputRequired(TaskStatus):
104
104
 
105
105
  class AuthRequired(InputRequired):
106
106
  state: Literal[TaskState.auth_required] = TaskState.auth_required # pyright: ignore [reportIncompatibleVariableOverride]
107
+
108
+
109
+ JsonDict = TypeAliasType(
110
+ "JsonDict",
111
+ "Union[dict[str, JsonDict], list[JsonDict], str, int, float, bool, None]", # pyright: ignore[reportDeprecated] # noqa: UP007
112
+ )
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
 
6
6
  import typing
7
7
  import uuid
8
- from typing import Literal
8
+ from typing import Literal, Self
9
9
 
10
10
  import pydantic
11
11
 
@@ -29,12 +29,24 @@ class VectorStoreDocument(pydantic.BaseModel):
29
29
  class VectorStoreItem(pydantic.BaseModel):
30
30
  id: str = pydantic.Field(default_factory=lambda: uuid.uuid4().hex)
31
31
  document_id: str
32
- document_type: typing.Literal["platform_file", "external"] = "platform_file"
32
+ document_type: typing.Literal["platform_file", "external"] | None = "platform_file"
33
33
  model_id: str | typing.Literal["platform"] = "platform"
34
34
  text: str
35
35
  embedding: list[float]
36
36
  metadata: Metadata | None = None
37
37
 
38
+ @pydantic.model_validator(mode="after")
39
+ def validate_document_id(self) -> Self:
40
+ """Validate that document_id is a valid UUID when document_type is platform_file."""
41
+ if self.document_type == "platform_file":
42
+ try:
43
+ _ = uuid.UUID(self.document_id)
44
+ except ValueError as ex:
45
+ raise ValueError(
46
+ f"document_id must be a valid UUID when document_type is platform_file, got: {self.document_id}"
47
+ ) from ex
48
+ return self
49
+
38
50
 
39
51
  class VectorStoreSearchResult(pydantic.BaseModel):
40
52
  item: VectorStoreItem
@@ -3,7 +3,6 @@
3
3
 
4
4
  import asyncio
5
5
  import inspect
6
- import json
7
6
  from asyncio import CancelledError
8
7
  from collections.abc import AsyncGenerator, AsyncIterator, Callable, Generator
9
8
  from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
@@ -37,14 +36,14 @@ from a2a.types import (
37
36
  )
38
37
 
39
38
  from agentstack_sdk.a2a.extensions.ui.agent_detail import AgentDetail, AgentDetailExtensionSpec
40
- from agentstack_sdk.a2a.types import ArtifactChunk, Metadata, RunYield, RunYieldResume
41
- from agentstack_sdk.server.constants import _IMPLICIT_DEPENDENCY_PREFIX
39
+ from agentstack_sdk.a2a.extensions.ui.error import ErrorExtensionParams, ErrorExtensionServer, ErrorExtensionSpec
40
+ from agentstack_sdk.a2a.types import AgentMessage, ArtifactChunk, Metadata, RunYield, RunYieldResume
41
+ from agentstack_sdk.server.constants import _IMPLICIT_DEPENDENCY_PREFIX, DEFAULT_ERROR_EXTENSION
42
42
  from agentstack_sdk.server.context import RunContext
43
43
  from agentstack_sdk.server.dependencies import extract_dependencies
44
44
  from agentstack_sdk.server.store.context_store import ContextStore
45
45
  from agentstack_sdk.server.utils import cancel_task, close_queue
46
46
  from agentstack_sdk.util.logging import logger
47
- from agentstack_sdk.util.utils import extract_messages
48
47
 
49
48
  AgentFunction: TypeAlias = Callable[[], AsyncGenerator[RunYield, RunYieldResume]]
50
49
  AgentFunctionFactory: TypeAlias = Callable[
@@ -124,9 +123,14 @@ def agent(
124
123
  resolved_name = name or fn.__name__
125
124
  resolved_description = description or fn.__doc__ or ""
126
125
 
126
+ # Check if user has provided an ErrorExtensionServer, if not add default
127
+ has_error_extension = any(isinstance(ext, ErrorExtensionServer) for ext in sdk_extensions)
128
+ error_extension_spec = ErrorExtensionSpec(ErrorExtensionParams()) if not has_error_extension else None
129
+
127
130
  capabilities.extensions = [
128
131
  *(capabilities.extensions or []),
129
132
  *(AgentDetailExtensionSpec(detail).to_agent_card_extensions()),
133
+ *(error_extension_spec.to_agent_card_extensions() if error_extension_spec else []),
130
134
  *(e_card for ext in sdk_extensions for e_card in ext.spec.to_agent_card_extensions()),
131
135
  ]
132
136
 
@@ -232,6 +236,11 @@ def agent(
232
236
  depends(message, context, dependency_args)
233
237
  )
234
238
 
239
+ context._error_extension = next(
240
+ (ext for ext in dependency_args.values() if isinstance(ext, ErrorExtensionServer)),
241
+ DEFAULT_ERROR_EXTENSION,
242
+ )
243
+
235
244
  context._store = await context_store.create(
236
245
  context_id=request_context.context_id,
237
246
  initialized_dependencies=list(dependency_args.values()),
@@ -339,124 +348,133 @@ class Executor(AgentExecutor):
339
348
  deep=True, update={"context_id": task_updater.context_id, "task_id": task_updater.task_id}
340
349
  )
341
350
 
351
+ run_context: RunContext | None = None
342
352
  try:
343
- async with self._agent_executor_span(task_updater, context, context_store) as (execute_fn, _run_context):
344
- agent_generator_fn = execute_fn()
345
-
346
- await task_updater.start_work()
347
- value: RunYieldResume = None
348
- opened_artifacts: set[str] = set()
349
- while True:
350
- # update invocation time
351
- self._running_tasks[task_updater.task_id]["last_invocation"] = datetime.now()
352
-
353
- yielded_value = await agent_generator_fn.asend(value)
354
-
355
- match yielded_value:
356
- case str(text):
357
- await task_updater.update_status(
358
- TaskState.working,
359
- message=task_updater.new_agent_message(parts=[Part(root=TextPart(text=text))]),
360
- )
361
- case Part(root=part) | (TextPart() | FilePart() | DataPart() as part):
362
- await task_updater.update_status(
363
- TaskState.working,
364
- message=task_updater.new_agent_message(parts=[Part(root=part)]),
365
- )
366
- case FileWithBytes() | FileWithUri() as file:
367
- await task_updater.update_status(
368
- TaskState.working,
369
- message=task_updater.new_agent_message(parts=[Part(root=FilePart(file=file))]),
370
- )
371
- case Message() as message:
372
- await task_updater.update_status(TaskState.working, message=with_context(message))
373
- case ArtifactChunk(
374
- parts=parts,
375
- artifact_id=artifact_id,
376
- name=name,
377
- metadata=metadata,
378
- last_chunk=last_chunk,
379
- ):
380
- await task_updater.add_artifact(
381
- parts=cast(list[Part], parts),
382
- artifact_id=artifact_id,
383
- name=name,
384
- metadata=metadata,
385
- append=artifact_id in opened_artifacts,
386
- last_chunk=last_chunk,
387
- )
388
- opened_artifacts.add(artifact_id)
389
- case Artifact(parts=parts, artifact_id=artifact_id, name=name, metadata=metadata):
390
- await task_updater.add_artifact(
353
+ async with self._agent_executor_span(task_updater, context, context_store) as (execute_fn, run_context):
354
+ try:
355
+ agent_generator_fn = execute_fn()
356
+
357
+ await task_updater.start_work()
358
+ value: RunYieldResume = None
359
+ opened_artifacts: set[str] = set()
360
+ while True:
361
+ # update invocation time
362
+ self._running_tasks[task_updater.task_id]["last_invocation"] = datetime.now()
363
+
364
+ yielded_value = await agent_generator_fn.asend(value)
365
+
366
+ match yielded_value:
367
+ case str(text):
368
+ await task_updater.update_status(
369
+ TaskState.working,
370
+ message=task_updater.new_agent_message(parts=[Part(root=TextPart(text=text))]),
371
+ )
372
+ case Part(root=part) | (TextPart() | FilePart() | DataPart() as part):
373
+ await task_updater.update_status(
374
+ TaskState.working,
375
+ message=task_updater.new_agent_message(parts=[Part(root=part)]),
376
+ )
377
+ case FileWithBytes() | FileWithUri() as file:
378
+ await task_updater.update_status(
379
+ TaskState.working,
380
+ message=task_updater.new_agent_message(parts=[Part(root=FilePart(file=file))]),
381
+ )
382
+ case Message() as message:
383
+ await task_updater.update_status(TaskState.working, message=with_context(message))
384
+ case ArtifactChunk(
391
385
  parts=parts,
392
386
  artifact_id=artifact_id,
393
387
  name=name,
394
388
  metadata=metadata,
395
- last_chunk=True,
396
- append=False,
397
- )
398
- case TaskStatus(state=TaskState.input_required, message=message, timestamp=timestamp):
399
- await task_updater.requires_input(message=with_context(message), final=True)
400
- value = cast(RunYieldResume, await resume_queue.dequeue_event())
401
- resume_queue.task_done()
402
- continue
403
- case TaskStatus(state=TaskState.auth_required, message=message, timestamp=timestamp):
404
- await task_updater.requires_auth(message=with_context(message), final=True)
405
- value = cast(RunYieldResume, await resume_queue.dequeue_event())
406
- resume_queue.task_done()
407
- continue
408
- case TaskStatus(state=state, message=message, timestamp=timestamp):
409
- await task_updater.update_status(
410
- state=state, message=with_context(message), timestamp=timestamp
411
- )
412
- case TaskStatusUpdateEvent(
413
- status=TaskStatus(state=state, message=message, timestamp=timestamp),
414
- final=final,
415
- metadata=metadata,
416
- ):
417
- await task_updater.update_status(
418
- state=state,
419
- message=with_context(message),
420
- timestamp=timestamp,
389
+ last_chunk=last_chunk,
390
+ ):
391
+ await task_updater.add_artifact(
392
+ parts=cast(list[Part], parts),
393
+ artifact_id=artifact_id,
394
+ name=name,
395
+ metadata=metadata,
396
+ append=artifact_id in opened_artifacts,
397
+ last_chunk=last_chunk,
398
+ )
399
+ opened_artifacts.add(artifact_id)
400
+ case Artifact(parts=parts, artifact_id=artifact_id, name=name, metadata=metadata):
401
+ await task_updater.add_artifact(
402
+ parts=parts,
403
+ artifact_id=artifact_id,
404
+ name=name,
405
+ metadata=metadata,
406
+ last_chunk=True,
407
+ append=False,
408
+ )
409
+ case TaskStatus(state=TaskState.input_required, message=message, timestamp=timestamp):
410
+ await task_updater.requires_input(message=with_context(message), final=True)
411
+ value = cast(RunYieldResume, await resume_queue.dequeue_event())
412
+ resume_queue.task_done()
413
+ continue
414
+ case TaskStatus(state=TaskState.auth_required, message=message, timestamp=timestamp):
415
+ await task_updater.requires_auth(message=with_context(message), final=True)
416
+ value = cast(RunYieldResume, await resume_queue.dequeue_event())
417
+ resume_queue.task_done()
418
+ continue
419
+ case TaskStatus(state=state, message=message, timestamp=timestamp):
420
+ await task_updater.update_status(
421
+ state=state, message=with_context(message), timestamp=timestamp
422
+ )
423
+ case TaskStatusUpdateEvent(
424
+ status=TaskStatus(state=state, message=message, timestamp=timestamp),
421
425
  final=final,
422
426
  metadata=metadata,
423
- )
424
- case TaskArtifactUpdateEvent(
425
- artifact=Artifact(artifact_id=artifact_id, name=name, metadata=metadata, parts=parts),
426
- append=append,
427
- last_chunk=last_chunk,
428
- ):
429
- await task_updater.add_artifact(
430
- parts=parts,
431
- artifact_id=artifact_id,
432
- name=name,
433
- metadata=metadata,
427
+ ):
428
+ await task_updater.update_status(
429
+ state=state,
430
+ message=with_context(message),
431
+ timestamp=timestamp,
432
+ final=final,
433
+ metadata=metadata,
434
+ )
435
+ case TaskArtifactUpdateEvent(
436
+ artifact=Artifact(artifact_id=artifact_id, name=name, metadata=metadata, parts=parts),
434
437
  append=append,
435
438
  last_chunk=last_chunk,
436
- )
437
- case Metadata() as metadata:
438
- await task_updater.update_status(
439
- state=TaskState.working,
440
- message=task_updater.new_agent_message(parts=[], metadata=metadata),
441
- )
442
- case dict() as data:
443
- await task_updater.update_status(
444
- state=TaskState.working,
445
- message=task_updater.new_agent_message(parts=[Part(root=DataPart(data=data))]),
446
- )
447
- case Exception() as ex:
448
- raise ex
449
- case _:
450
- raise ValueError(f"Invalid value yielded from agent: {type(yielded_value)}")
451
- value = None
452
- except StopAsyncIteration:
453
- await task_updater.complete()
454
- except CancelledError:
455
- await task_updater.cancel()
456
- except Exception as ex:
457
- logger.error("Error when executing agent", exc_info=ex)
458
- msg = json.dumps(extract_messages(ex), indent=2)
459
- await task_updater.failed(task_updater.new_agent_message(parts=[Part(root=TextPart(text=msg))]))
439
+ ):
440
+ await task_updater.add_artifact(
441
+ parts=parts,
442
+ artifact_id=artifact_id,
443
+ name=name,
444
+ metadata=metadata,
445
+ append=append,
446
+ last_chunk=last_chunk,
447
+ )
448
+ case Metadata() as metadata:
449
+ await task_updater.update_status(
450
+ state=TaskState.working,
451
+ message=task_updater.new_agent_message(parts=[], metadata=metadata),
452
+ )
453
+ case dict() as data:
454
+ await task_updater.update_status(
455
+ state=TaskState.working,
456
+ message=task_updater.new_agent_message(parts=[Part(root=DataPart(data=data))]),
457
+ )
458
+ case Exception() as ex:
459
+ raise ex
460
+ case _:
461
+ raise ValueError(f"Invalid value yielded from agent: {type(yielded_value)}")
462
+ value = None
463
+ except StopAsyncIteration:
464
+ await task_updater.complete()
465
+ except CancelledError:
466
+ await task_updater.cancel()
467
+ except Exception as ex:
468
+ logger.error("Error when executing agent", exc_info=ex)
469
+ try:
470
+ error_extension = run_context._error_extension if run_context else None
471
+ error_extension = error_extension if error_extension is not None else DEFAULT_ERROR_EXTENSION
472
+ error_msg = error_extension.message(ex)
473
+ except Exception as error_exc:
474
+ error_msg = AgentMessage(
475
+ text=(f"Failed to create error message: {error_exc!s}\noriginal exc: {ex!s}")
476
+ )
477
+ await task_updater.failed(error_msg)
460
478
  finally: # cleanup
461
479
  await cancel_task(cancellation_task)
462
480
  is_cancelling = bool(current_task.cancelling())
@@ -0,0 +1,12 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from typing import Final
5
+
6
+ from agentstack_sdk.a2a.extensions.ui.error import ErrorExtensionParams, ErrorExtensionServer, ErrorExtensionSpec
7
+
8
+ _IMPLICIT_DEPENDENCY_PREFIX: Final = "___server_dep"
9
+
10
+ DEFAULT_ERROR_EXTENSION: Final = ErrorExtensionServer(ErrorExtensionSpec(ErrorExtensionParams()))
11
+
12
+ __all__ = []
@@ -3,6 +3,7 @@
3
3
 
4
4
 
5
5
  from collections.abc import AsyncIterator
6
+ from typing import TYPE_CHECKING
6
7
 
7
8
  import janus
8
9
  from a2a.server.context import ServerCallContext
@@ -13,6 +14,9 @@ from pydantic import BaseModel, PrivateAttr
13
14
  from agentstack_sdk.a2a.types import RunYield, RunYieldResume
14
15
  from agentstack_sdk.server.store.context_store import ContextStoreInstance
15
16
 
17
+ if TYPE_CHECKING:
18
+ from agentstack_sdk.a2a.extensions.ui.error import ErrorExtensionServer
19
+
16
20
 
17
21
  class RunContext(BaseModel, arbitrary_types_allowed=True):
18
22
  configuration: MessageSendConfiguration | None = None
@@ -26,6 +30,7 @@ class RunContext(BaseModel, arbitrary_types_allowed=True):
26
30
  _store: ContextStoreInstance | None = PrivateAttr(None)
27
31
  _yield_queue: janus.Queue[RunYield] = PrivateAttr(default_factory=janus.Queue)
28
32
  _yield_resume_queue: janus.Queue[RunYieldResume] = PrivateAttr(default_factory=janus.Queue)
33
+ _error_extension: "ErrorExtensionServer | None" = PrivateAttr(None)
29
34
 
30
35
  async def store(self, data: Message | Artifact):
31
36
  if not self._store:
@@ -1,8 +0,0 @@
1
- # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- from typing import Final
5
-
6
- _IMPLICIT_DEPENDENCY_PREFIX: Final = "___server_dep"
7
-
8
- __all__ = []