agentenv-framework-protocol 0.1.269__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.
- agentenv_framework_protocol-0.1.269.dist-info/METADATA +599 -0
- agentenv_framework_protocol-0.1.269.dist-info/RECORD +20 -0
- agentenv_framework_protocol-0.1.269.dist-info/WHEEL +4 -0
- agentenv_framework_protocol-0.1.269.dist-info/licenses/LICENSE +202 -0
- agentenv_framework_protocol-0.1.269.dist-info/licenses/NOTICE +4 -0
- agentenv_framework_protocol-0.1.269.dist-info/licenses/THIRD_PARTY_NOTICES.md +1701 -0
- agentenv_protocol/__init__.py +121 -0
- agentenv_protocol/a2a_agent/__init__.py +204 -0
- agentenv_protocol/a2a_agent/_triggers.py +489 -0
- agentenv_protocol/a2a_agent/extensions.py +1151 -0
- agentenv_protocol/a2a_agent/framework.py +1283 -0
- agentenv_protocol/a2a_agent/registry.py +449 -0
- agentenv_protocol/a2a_agent/tasks/__init__.py +39 -0
- agentenv_protocol/a2a_agent/tasks/v1.py +408 -0
- agentenv_protocol/agent_env_environment.py +653 -0
- agentenv_protocol/client.py +185 -0
- agentenv_protocol/manifest.py +203 -0
- agentenv_protocol/preflight.py +81 -0
- agentenv_protocol/transfers.py +554 -0
- agentenv_protocol/types.py +165 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""Version 1 of the stable agent task boundary.
|
|
2
|
+
|
|
3
|
+
The framework constructs :class:`TaskRequest` instances and agent authors return
|
|
4
|
+
an immutable :class:`TaskResult`. V1 is additive-only: new request fields must
|
|
5
|
+
have defaults and new result capabilities are exposed through optional builder
|
|
6
|
+
methods rather than required constructor arguments.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import AsyncIterator, Awaitable, Iterable, Mapping
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar, Union
|
|
16
|
+
|
|
17
|
+
from a2a.types import TaskStatusUpdateEvent
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_WriteOnlyT = TypeVar("_WriteOnlyT")
|
|
22
|
+
WriteOnly: TypeAlias = Annotated[
|
|
23
|
+
_WriteOnlyT,
|
|
24
|
+
Field(json_schema_extra={"writeOnly": True}),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _copy_json_value(value: Any) -> Any:
|
|
29
|
+
"""Detach JSON-shaped values without changing their serializable types."""
|
|
30
|
+
if isinstance(value, BaseModel):
|
|
31
|
+
return value.model_copy(deep=True)
|
|
32
|
+
if isinstance(value, Mapping):
|
|
33
|
+
return {str(key): _copy_json_value(item) for key, item in value.items()}
|
|
34
|
+
if isinstance(value, (list, tuple)):
|
|
35
|
+
return [_copy_json_value(item) for item in value]
|
|
36
|
+
if isinstance(value, (set, frozenset)):
|
|
37
|
+
return [_copy_json_value(item) for item in value]
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _thaw(value: Any) -> Any:
|
|
42
|
+
"""Convert immutable boundary values into JSON-ready containers."""
|
|
43
|
+
if isinstance(value, Mapping):
|
|
44
|
+
return {key: _thaw(item) for key, item in value.items()}
|
|
45
|
+
if isinstance(value, tuple):
|
|
46
|
+
return [_thaw(item) for item in value]
|
|
47
|
+
if isinstance(value, frozenset):
|
|
48
|
+
return [_thaw(item) for item in value]
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
53
|
+
class TextPart:
|
|
54
|
+
text: str
|
|
55
|
+
kind: Literal["text"] = "text"
|
|
56
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
object.__setattr__(self, "metadata", _copy_json_value(self.metadata))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
63
|
+
class FilePart:
|
|
64
|
+
name: str | None = None
|
|
65
|
+
mime_type: str | None = None
|
|
66
|
+
bytes: str | None = None
|
|
67
|
+
uri: str | None = None
|
|
68
|
+
kind: Literal["file"] = "file"
|
|
69
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
def __post_init__(self) -> None:
|
|
72
|
+
if (self.bytes is None) == (self.uri is None):
|
|
73
|
+
raise ValueError("a file part requires exactly one of bytes or uri")
|
|
74
|
+
object.__setattr__(self, "metadata", _copy_json_value(self.metadata))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
78
|
+
class DataPart:
|
|
79
|
+
data: Mapping[str, Any]
|
|
80
|
+
kind: Literal["data"] = "data"
|
|
81
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
object.__setattr__(self, "data", _copy_json_value(self.data))
|
|
85
|
+
object.__setattr__(self, "metadata", _copy_json_value(self.metadata))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
TaskPart = Union[TextPart, FilePart, DataPart]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
92
|
+
class TaskProgress:
|
|
93
|
+
"""A non-terminal status update yielded by a streaming ``run`` method."""
|
|
94
|
+
|
|
95
|
+
parts: tuple[TaskPart, ...] = ()
|
|
96
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
97
|
+
|
|
98
|
+
def __post_init__(self) -> None:
|
|
99
|
+
object.__setattr__(self, "parts", tuple(self.parts))
|
|
100
|
+
object.__setattr__(self, "metadata", _copy_json_value(self.metadata))
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def text(
|
|
104
|
+
cls, text: str, *, metadata: Mapping[str, Any] | None = None
|
|
105
|
+
) -> TaskProgress:
|
|
106
|
+
return cls(parts=(TextPart(text=text),), metadata=metadata or {})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AgentConfig(BaseModel):
|
|
110
|
+
"""Base configuration supplied by AgentEnv to every configured agent.
|
|
111
|
+
|
|
112
|
+
Subclasses add runtime-specific fields. These SDK-owned fields are part of
|
|
113
|
+
the control-plane contract and therefore do not need to be repeated by each
|
|
114
|
+
agent author.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
118
|
+
|
|
119
|
+
name: str | None = None
|
|
120
|
+
description: str | None = None
|
|
121
|
+
role: str | None = None
|
|
122
|
+
timeout_seconds: int = 600
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
ConfigT = TypeVar("ConfigT", bound=AgentConfig)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
129
|
+
class TaskRequest(Generic[ConfigT]):
|
|
130
|
+
"""Frozen request record with detached, JSON-native nested values."""
|
|
131
|
+
|
|
132
|
+
task_id: str
|
|
133
|
+
context_id: str
|
|
134
|
+
parts: tuple[TaskPart, ...]
|
|
135
|
+
config: ConfigT
|
|
136
|
+
mcp_servers: Mapping[str, Any] = field(default_factory=dict)
|
|
137
|
+
skills: tuple[Mapping[str, Any], ...] = ()
|
|
138
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
139
|
+
session_ref: str | None = None
|
|
140
|
+
workspace: Path | None = None
|
|
141
|
+
|
|
142
|
+
def __post_init__(self) -> None:
|
|
143
|
+
object.__setattr__(self, "parts", tuple(self.parts))
|
|
144
|
+
if not isinstance(self.config, AgentConfig):
|
|
145
|
+
raise TypeError("TaskRequest.config must be an AgentConfig instance")
|
|
146
|
+
# Detach mutable values from framework-owned state while preserving the
|
|
147
|
+
# JSON-native types declared by the config model and request contract.
|
|
148
|
+
object.__setattr__(self, "config", self.config.model_copy(deep=True))
|
|
149
|
+
object.__setattr__(self, "mcp_servers", _copy_json_value(self.mcp_servers))
|
|
150
|
+
object.__setattr__(
|
|
151
|
+
self, "skills", tuple(_copy_json_value(skill) for skill in self.skills)
|
|
152
|
+
)
|
|
153
|
+
object.__setattr__(self, "metadata", _copy_json_value(self.metadata))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TaskOutcome(str, Enum):
|
|
157
|
+
SUCCEEDED = "succeeded"
|
|
158
|
+
FAILED = "failed"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
162
|
+
class TaskError:
|
|
163
|
+
code: str
|
|
164
|
+
message: str
|
|
165
|
+
error_type: Literal["agent_error", "infra_error"] = "agent_error"
|
|
166
|
+
|
|
167
|
+
def __post_init__(self) -> None:
|
|
168
|
+
if self.error_type not in {"agent_error", "infra_error"}:
|
|
169
|
+
raise ValueError("error_type must be 'agent_error' or 'infra_error'")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
173
|
+
class NativeTrajectory:
|
|
174
|
+
format: str
|
|
175
|
+
payload: Any
|
|
176
|
+
version: int = 1
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
if self.version < 1:
|
|
180
|
+
raise ValueError("trajectory envelope version must be positive")
|
|
181
|
+
object.__setattr__(self, "payload", _copy_json_value(self.payload))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
185
|
+
class Usage:
|
|
186
|
+
"""Common execution metrics plus runtime-specific, JSON-shaped details."""
|
|
187
|
+
|
|
188
|
+
tool_call_count: int | None = None
|
|
189
|
+
input_tokens: int | None = None
|
|
190
|
+
output_tokens: int | None = None
|
|
191
|
+
total_tokens: int | None = None
|
|
192
|
+
cost_usd: float | None = None
|
|
193
|
+
provider_details: Mapping[str, Any] = field(default_factory=dict)
|
|
194
|
+
|
|
195
|
+
def __post_init__(self) -> None:
|
|
196
|
+
for name in (
|
|
197
|
+
"tool_call_count",
|
|
198
|
+
"input_tokens",
|
|
199
|
+
"output_tokens",
|
|
200
|
+
"total_tokens",
|
|
201
|
+
):
|
|
202
|
+
value = getattr(self, name)
|
|
203
|
+
if value is not None and (
|
|
204
|
+
isinstance(value, bool) or not isinstance(value, int) or value < 0
|
|
205
|
+
):
|
|
206
|
+
raise ValueError(f"{name} cannot be negative")
|
|
207
|
+
if self.cost_usd is not None and (
|
|
208
|
+
isinstance(self.cost_usd, bool)
|
|
209
|
+
or not isinstance(self.cost_usd, (int, float))
|
|
210
|
+
or self.cost_usd < 0
|
|
211
|
+
):
|
|
212
|
+
raise ValueError("cost_usd cannot be negative")
|
|
213
|
+
if not isinstance(self.provider_details, Mapping):
|
|
214
|
+
raise TypeError("provider_details must be a mapping")
|
|
215
|
+
object.__setattr__(
|
|
216
|
+
self, "provider_details", _copy_json_value(self.provider_details)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def is_empty(self) -> bool:
|
|
221
|
+
return (
|
|
222
|
+
self.tool_call_count is None
|
|
223
|
+
and self.input_tokens is None
|
|
224
|
+
and self.output_tokens is None
|
|
225
|
+
and self.total_tokens is None
|
|
226
|
+
and self.cost_usd is None
|
|
227
|
+
and not self.provider_details
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def to_dict(self) -> dict[str, Any]:
|
|
231
|
+
values = {
|
|
232
|
+
"tool_call_count": self.tool_call_count,
|
|
233
|
+
"input_tokens": self.input_tokens,
|
|
234
|
+
"output_tokens": self.output_tokens,
|
|
235
|
+
"total_tokens": self.total_tokens,
|
|
236
|
+
"cost_usd": self.cost_usd,
|
|
237
|
+
}
|
|
238
|
+
result = {name: value for name, value in values.items() if value is not None}
|
|
239
|
+
if self.provider_details:
|
|
240
|
+
result["provider_details"] = _thaw(self.provider_details)
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
245
|
+
class TaskResult:
|
|
246
|
+
"""Validated, immutable result returned by an agent's ``run`` method."""
|
|
247
|
+
|
|
248
|
+
outcome: TaskOutcome
|
|
249
|
+
parts: tuple[TaskPart, ...] = ()
|
|
250
|
+
error: TaskError | None = None
|
|
251
|
+
session_ref: str | None = None
|
|
252
|
+
usage: Usage = field(default_factory=Usage)
|
|
253
|
+
native_trajectory: NativeTrajectory | None = None
|
|
254
|
+
|
|
255
|
+
def __post_init__(self) -> None:
|
|
256
|
+
object.__setattr__(self, "parts", tuple(self.parts))
|
|
257
|
+
object.__setattr__(self, "usage", _copy_json_value(self.usage))
|
|
258
|
+
if self.outcome is TaskOutcome.SUCCEEDED and self.error is not None:
|
|
259
|
+
raise ValueError("a successful result cannot contain an error")
|
|
260
|
+
if self.outcome is TaskOutcome.SUCCEEDED and not self.parts:
|
|
261
|
+
raise ValueError("a successful result requires at least one part")
|
|
262
|
+
if self.outcome is TaskOutcome.FAILED and self.error is None:
|
|
263
|
+
raise ValueError("a failed result requires an error")
|
|
264
|
+
if not isinstance(self.usage, Usage):
|
|
265
|
+
raise TypeError("TaskResult.usage must be a Usage instance")
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def builder(cls) -> "TaskResultBuilder":
|
|
269
|
+
return TaskResultBuilder()
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def text(cls, text: str, **kwargs: Any) -> "TaskResult":
|
|
273
|
+
return cls.success(parts=(TextPart(text=text),), **kwargs)
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def success(
|
|
277
|
+
cls,
|
|
278
|
+
*,
|
|
279
|
+
parts: Iterable[TaskPart] = (),
|
|
280
|
+
session_ref: str | None = None,
|
|
281
|
+
usage: Usage | None = None,
|
|
282
|
+
native_trajectory: NativeTrajectory | None = None,
|
|
283
|
+
) -> "TaskResult":
|
|
284
|
+
builder = (
|
|
285
|
+
cls.builder()
|
|
286
|
+
.succeeded()
|
|
287
|
+
.parts(parts)
|
|
288
|
+
.session_ref(session_ref)
|
|
289
|
+
.usage(usage or Usage())
|
|
290
|
+
)
|
|
291
|
+
if native_trajectory is not None:
|
|
292
|
+
builder.native_trajectory(
|
|
293
|
+
format=native_trajectory.format,
|
|
294
|
+
payload=native_trajectory.payload,
|
|
295
|
+
version=native_trajectory.version,
|
|
296
|
+
)
|
|
297
|
+
return builder.build()
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def failure(
|
|
301
|
+
cls,
|
|
302
|
+
code: str,
|
|
303
|
+
message: str,
|
|
304
|
+
*,
|
|
305
|
+
error_type: Literal["agent_error", "infra_error"] = "agent_error",
|
|
306
|
+
parts: Iterable[TaskPart] | None = None,
|
|
307
|
+
) -> "TaskResult":
|
|
308
|
+
return (
|
|
309
|
+
cls.builder()
|
|
310
|
+
.failed(code, message, error_type=error_type)
|
|
311
|
+
.parts(parts if parts is not None else (TextPart(text=message),))
|
|
312
|
+
.build()
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TaskResultBuilder:
|
|
317
|
+
"""Mutable assembly object whose ``build`` method returns a valid result."""
|
|
318
|
+
|
|
319
|
+
def __init__(self) -> None:
|
|
320
|
+
self._outcome: TaskOutcome | None = None
|
|
321
|
+
self._parts: list[TaskPart] = []
|
|
322
|
+
self._error: TaskError | None = None
|
|
323
|
+
self._session_ref: str | None = None
|
|
324
|
+
self._usage = Usage()
|
|
325
|
+
self._native_trajectory: NativeTrajectory | None = None
|
|
326
|
+
|
|
327
|
+
def succeeded(self) -> "TaskResultBuilder":
|
|
328
|
+
self._outcome = TaskOutcome.SUCCEEDED
|
|
329
|
+
self._error = None
|
|
330
|
+
return self
|
|
331
|
+
|
|
332
|
+
def failed(
|
|
333
|
+
self,
|
|
334
|
+
code: str,
|
|
335
|
+
message: str,
|
|
336
|
+
*,
|
|
337
|
+
error_type: Literal["agent_error", "infra_error"] = "agent_error",
|
|
338
|
+
) -> "TaskResultBuilder":
|
|
339
|
+
self._outcome = TaskOutcome.FAILED
|
|
340
|
+
self._error = TaskError(code=code, message=message, error_type=error_type)
|
|
341
|
+
return self
|
|
342
|
+
|
|
343
|
+
def parts(self, parts: Iterable[TaskPart]) -> "TaskResultBuilder":
|
|
344
|
+
self._parts = list(parts)
|
|
345
|
+
return self
|
|
346
|
+
|
|
347
|
+
def add_text(
|
|
348
|
+
self, text: str, *, metadata: Mapping[str, Any] | None = None
|
|
349
|
+
) -> "TaskResultBuilder":
|
|
350
|
+
self._parts.append(TextPart(text=text, metadata=metadata or {}))
|
|
351
|
+
return self
|
|
352
|
+
|
|
353
|
+
def add_data(
|
|
354
|
+
self, data: Mapping[str, Any], *, metadata: Mapping[str, Any] | None = None
|
|
355
|
+
) -> "TaskResultBuilder":
|
|
356
|
+
self._parts.append(DataPart(data=data, metadata=metadata or {}))
|
|
357
|
+
return self
|
|
358
|
+
|
|
359
|
+
def add_structured_output(self, value: Any) -> "TaskResultBuilder":
|
|
360
|
+
"""Append the AgentEnv structured-output convention as a data part.
|
|
361
|
+
|
|
362
|
+
Structured output is ordinary terminal response data, not a separate
|
|
363
|
+
result field. AgentEnv consumers read ``data["structured_output"]``.
|
|
364
|
+
"""
|
|
365
|
+
return self.add_data({"structured_output": value})
|
|
366
|
+
|
|
367
|
+
def session_ref(self, value: str | None) -> "TaskResultBuilder":
|
|
368
|
+
self._session_ref = value
|
|
369
|
+
return self
|
|
370
|
+
|
|
371
|
+
def usage(self, value: Usage) -> "TaskResultBuilder":
|
|
372
|
+
if not isinstance(value, Usage):
|
|
373
|
+
raise TypeError("usage must be a Usage instance")
|
|
374
|
+
self._usage = value
|
|
375
|
+
return self
|
|
376
|
+
|
|
377
|
+
def native_trajectory(
|
|
378
|
+
self, *, format: str, payload: Any, version: int = 1
|
|
379
|
+
) -> "TaskResultBuilder":
|
|
380
|
+
self._native_trajectory = NativeTrajectory(
|
|
381
|
+
format=format, payload=payload, version=version
|
|
382
|
+
)
|
|
383
|
+
return self
|
|
384
|
+
|
|
385
|
+
def build(self) -> TaskResult:
|
|
386
|
+
if self._outcome is None:
|
|
387
|
+
raise ValueError(
|
|
388
|
+
"task result outcome is required; call succeeded() or failed()"
|
|
389
|
+
)
|
|
390
|
+
return TaskResult(
|
|
391
|
+
outcome=self._outcome,
|
|
392
|
+
parts=tuple(self._parts),
|
|
393
|
+
error=self._error,
|
|
394
|
+
session_ref=self._session_ref,
|
|
395
|
+
usage=self._usage,
|
|
396
|
+
native_trajectory=self._native_trajectory,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
TaskStreamItem = Union[
|
|
401
|
+
TaskProgress,
|
|
402
|
+
TaskStatusUpdateEvent,
|
|
403
|
+
TaskResult,
|
|
404
|
+
]
|
|
405
|
+
AgentRunResult = Union[
|
|
406
|
+
Awaitable[TaskResult],
|
|
407
|
+
AsyncIterator[TaskStreamItem],
|
|
408
|
+
]
|