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,599 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentenv-framework-protocol
|
|
3
|
+
Version: 0.1.269
|
|
4
|
+
Summary: Open data-plane protocol and server SDK for agent environments.
|
|
5
|
+
Project-URL: Homepage, https://github.com/scaleapi/agentenv-framework
|
|
6
|
+
Project-URL: Repository, https://github.com/scaleapi/agentenv-framework/tree/main/packages/agentenv-protocol
|
|
7
|
+
Project-URL: Issues, https://github.com/scaleapi/agentenv-framework/issues
|
|
8
|
+
Author: Scale AI
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE
|
|
12
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: httpx>=0.27
|
|
15
|
+
Requires-Dist: pydantic>=2.11
|
|
16
|
+
Requires-Dist: starlette>=0.37
|
|
17
|
+
Provides-Extra: agent
|
|
18
|
+
Requires-Dist: a2a-sdk[http-server]<0.4,>=0.3.26; extra == 'agent'
|
|
19
|
+
Requires-Dist: regex>=2023.12.25; extra == 'agent'
|
|
20
|
+
Requires-Dist: uvicorn>=0.30; extra == 'agent'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: a2a-sdk[http-server]<0.4,>=0.3.26; extra == 'dev'
|
|
23
|
+
Requires-Dist: mcp<2,>=1.25; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=9.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: regex>=2023.12.25; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# agentenv-framework-protocol
|
|
30
|
+
|
|
31
|
+
Open data-plane protocol and server SDK for agent environments. Install it with
|
|
32
|
+
`pip install agentenv-framework-protocol`; the import package is `agentenv_protocol`.
|
|
33
|
+
|
|
34
|
+
An environment author writes a class with decorated methods and serves it:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import base64
|
|
38
|
+
import json
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Annotated
|
|
41
|
+
from urllib.parse import urlparse
|
|
42
|
+
from urllib.request import urlopen
|
|
43
|
+
|
|
44
|
+
from pydantic import Field
|
|
45
|
+
from agentenv_protocol import (
|
|
46
|
+
AgentEnvEnvironment, DataPart, FilePart,
|
|
47
|
+
environment_card, reset_data, add_data, get_data, tool,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@environment_card(name="slack")
|
|
52
|
+
class SlackEnv(AgentEnvEnvironment):
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
self.channels, self.messages = {}, []
|
|
56
|
+
|
|
57
|
+
@reset_data
|
|
58
|
+
async def _reset(self):
|
|
59
|
+
self.channels.clear(); self.messages.clear()
|
|
60
|
+
|
|
61
|
+
@add_data
|
|
62
|
+
async def _add(self, parts):
|
|
63
|
+
# Seeds arrive as an inline DataPart (live deploy) OR a FilePart whose
|
|
64
|
+
# file carries inline bytes, a file:// URI (a staged file or an exported
|
|
65
|
+
# bundle) or an https:// URL (a signed URL, when agent-env can't stage the
|
|
66
|
+
# file). Handle all four — dropping the FilePart branch makes bundles load empty.
|
|
67
|
+
for p in parts:
|
|
68
|
+
if isinstance(p, DataPart):
|
|
69
|
+
payload = p.data
|
|
70
|
+
elif isinstance(p, FilePart):
|
|
71
|
+
f = p.file
|
|
72
|
+
if getattr(f, "bytes", None) is not None:
|
|
73
|
+
payload = json.loads(base64.b64decode(f.bytes))
|
|
74
|
+
elif urlparse(f.uri).scheme in ("http", "https"):
|
|
75
|
+
with urlopen(f.uri) as response:
|
|
76
|
+
payload = json.loads(response.read())
|
|
77
|
+
else:
|
|
78
|
+
payload = json.loads(Path(urlparse(f.uri).path).read_bytes())
|
|
79
|
+
else:
|
|
80
|
+
continue # TextPart / unknown — nothing to load
|
|
81
|
+
self.messages.extend(payload.get("messages", []))
|
|
82
|
+
|
|
83
|
+
@get_data
|
|
84
|
+
async def _state(self):
|
|
85
|
+
return [DataPart(data={"channels": list(self.channels.values()), "messages": self.messages})]
|
|
86
|
+
|
|
87
|
+
@tool(name="{environment_name}_send_message")
|
|
88
|
+
def send_message(
|
|
89
|
+
self,
|
|
90
|
+
channel: Annotated[str, Field(description="Channel to post to.")],
|
|
91
|
+
text: Annotated[str, Field(description="Message text.")],
|
|
92
|
+
) -> str:
|
|
93
|
+
"""Send a message to a channel."""
|
|
94
|
+
self.messages.append({"channel": channel, "text": text})
|
|
95
|
+
return "ok"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
SlackEnv().serve()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`@tool` methods are registered as real MCP tools on the FastMCP app at mount and advertised
|
|
103
|
+
under the card's `capabilities.tools` (name, description, signature-derived `inputSchema` —
|
|
104
|
+
`Annotated[..., Field(description=...)]` param descriptions included). `{environment_name}` in a
|
|
105
|
+
tool name is resolved to the card's name at mount, so a shared mixin or base class can declare
|
|
106
|
+
environment-prefixed tools without knowing the name at class-definition time; any other unresolved
|
|
107
|
+
`{...}` token raises. Duplicate tool names raise at construction.
|
|
108
|
+
|
|
109
|
+
`AgentEnvStarletteApplication` mounts the same handler onto a Starlette/FastAPI app instead of
|
|
110
|
+
FastMCP; since those apps have no MCP tool registry, constructing one with `@tool` methods
|
|
111
|
+
raises.
|
|
112
|
+
|
|
113
|
+
## Serving
|
|
114
|
+
|
|
115
|
+
`serve()` builds the FastMCP app via `create_fastmcp_app()`, which encodes the agent-env deploy
|
|
116
|
+
contract once — the name resolution order (`ENVIRONMENT_NAME`, which agent-env sets to the env's
|
|
117
|
+
registered name, then `@environment_card`'s name, then the class name; `SERVICE_NAME` is no
|
|
118
|
+
longer consulted), `MCP_HOST`/`MCP_PORT`
|
|
119
|
+
binding (default 18765), DNS-rebinding protection off (gateways reach servers by compose
|
|
120
|
+
hostname, which mcp's localhost-only default allowlist rejects), and the AgentEnv mount — then
|
|
121
|
+
runs `streamable-http`. Pre-declared card content is more `@environment_card(...)` kwargs — any
|
|
122
|
+
`EnvironmentCard` field (keys are validated at decoration time); an undecorated class defaults
|
|
123
|
+
its card name to the class name. To mutate
|
|
124
|
+
the app before serving (extra imperative tools, custom routes), call `create_app()` first — it
|
|
125
|
+
returns the app un-served.
|
|
126
|
+
|
|
127
|
+
An environment that already owns its FastMCP app keeps full control: construct and configure
|
|
128
|
+
`self.mcp` yourself, then `mount(self.mcp)` — `serve()` runs it as-is (mounting first if you
|
|
129
|
+
haven't) and never alters a caller-built app's settings.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
@environment_card(name="legacy")
|
|
133
|
+
class LegacyEnv(AgentEnvEnvironment):
|
|
134
|
+
|
|
135
|
+
def __init__(self):
|
|
136
|
+
self.mcp = FastMCP("legacy") # yours: settings, guards, extra routes
|
|
137
|
+
self.mount(self.mcp)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
LegacyEnv().serve() # or run your app your own way; mount() alone is enough
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The composition style — no base class, just
|
|
144
|
+
`AgentEnvFastMCPApplication(environment_card=card, handler=handler).add_routes_to_app(app)` —
|
|
145
|
+
remains fully supported; the base class is sugar over it.
|
|
146
|
+
|
|
147
|
+
A FastMCP-backed card declares its MCP endpoint in `additionalInterfaces` when it is mounted:
|
|
148
|
+
`{"url": <path>, "transport": "mcp"}`, where the path is the app's `streamable_http_path` (`/mcp`,
|
|
149
|
+
`MCP_PATH`, unless configured); a FastMCP-shaped app that does not expose that setting declares no
|
|
150
|
+
entry. That is the streamable-HTTP endpoint, which `serve()` runs by default and agent-env deploys
|
|
151
|
+
against; an app served over another transport, such as SSE, must declare its own entry. A card
|
|
152
|
+
that already declares an interface with the `mcp` transport (`MCP_TRANSPORT`) keeps it; interfaces
|
|
153
|
+
with any other transport are kept beside the SDK's entry. Card URLs are paths, relative to the
|
|
154
|
+
address the card was fetched from. On the client side, `client.mcp_path(card)` returns the
|
|
155
|
+
declared path, or `/mcp` for a card without one.
|
|
156
|
+
|
|
157
|
+
Extensions are invoked from what the card advertises. `client.find_extension_method(card, uri,
|
|
158
|
+
method)` returns one advertised method, whose `endpoint` is the method's own or else the
|
|
159
|
+
extension's, and `client.invoke_extension(base_url, card, uri, params, method=...)` calls it with
|
|
160
|
+
its HTTP verb. Without `method`, `invoke_extension` calls the first method listed; an extension
|
|
161
|
+
with several methods, such as a gateway's `urn:agentenv:clock/v1`, should always be called by name.
|
|
162
|
+
|
|
163
|
+
Dependencies are intentionally light (`pydantic`, `starlette`) so the package can be added to environment server images without pulling a heavier framework — `mcp` is imported lazily inside `create_fastmcp_app()` and is deliberately not a dependency.
|
|
164
|
+
|
|
165
|
+
## A2A agent framework
|
|
166
|
+
|
|
167
|
+
The distribution exposes two unrelated decorators named `extension`:
|
|
168
|
+
`agentenv_protocol.extension` declares environment/MCP extensions, while
|
|
169
|
+
`agentenv_protocol.a2a_agent.extension` binds an operation handler on an A2A
|
|
170
|
+
agent. Import the decorator from the namespace matching the application you are
|
|
171
|
+
building.
|
|
172
|
+
|
|
173
|
+
Install the optional agent dependencies with
|
|
174
|
+
`agentenv-framework-protocol[agent]`. The framework generates the Agent Card,
|
|
175
|
+
extension routes, A2A task lifecycle, and detached task boundary from one
|
|
176
|
+
agent definition:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from agentenv_protocol.a2a_agent import (
|
|
180
|
+
MCP_CONFIG_V1,
|
|
181
|
+
TRAJECTORY_V1,
|
|
182
|
+
TRIGGERS_V1,
|
|
183
|
+
AgentConfig,
|
|
184
|
+
AgentEnvAgent,
|
|
185
|
+
AgentIdentity,
|
|
186
|
+
TaskRequest,
|
|
187
|
+
TaskResult,
|
|
188
|
+
Usage,
|
|
189
|
+
a2a_agent,
|
|
190
|
+
create_app,
|
|
191
|
+
enable,
|
|
192
|
+
serve,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class MyAgentConfig(AgentConfig):
|
|
197
|
+
model: str | None = "my-default-model"
|
|
198
|
+
system_prompt: str | None = None
|
|
199
|
+
timeout_seconds: int = 1800
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@a2a_agent(
|
|
203
|
+
identity=AgentIdentity(
|
|
204
|
+
name="my-cli-agent",
|
|
205
|
+
description="Runs My CLI",
|
|
206
|
+
version="1.0.0",
|
|
207
|
+
input_modes=("text", "image/png"),
|
|
208
|
+
),
|
|
209
|
+
config=MyAgentConfig,
|
|
210
|
+
config_description="Configure the My CLI runtime.",
|
|
211
|
+
extensions=(
|
|
212
|
+
MCP_CONFIG_V1,
|
|
213
|
+
enable(
|
|
214
|
+
TRAJECTORY_V1,
|
|
215
|
+
description="Retrieve the My CLI native event trajectory.",
|
|
216
|
+
),
|
|
217
|
+
TRIGGERS_V1,
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
class MyAgent(AgentEnvAgent):
|
|
221
|
+
async def run(self, request: TaskRequest[MyAgentConfig]) -> TaskResult:
|
|
222
|
+
execution = await run_my_cli(request)
|
|
223
|
+
return (
|
|
224
|
+
TaskResult.builder()
|
|
225
|
+
.succeeded()
|
|
226
|
+
.add_text(execution.output)
|
|
227
|
+
.session_ref(execution.session_id)
|
|
228
|
+
.usage(Usage(tool_call_count=execution.tool_calls))
|
|
229
|
+
.native_trajectory(format="my-cli-events/v1", payload=execution.events)
|
|
230
|
+
.build()
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
agent = MyAgent()
|
|
235
|
+
app = agent.create_app() # equivalently: create_app(agent)
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
agent.serve() # equivalently: serve(agent)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`AgentEnvAgent` mirrors `AgentEnvEnvironment`: it makes the `run()`,
|
|
242
|
+
`create_app()`, and `serve()` authoring surface visible to static type checking.
|
|
243
|
+
`@a2a_agent(...)` attaches declarative metadata to that base class; it does not
|
|
244
|
+
inject methods dynamically. The concrete `TaskRequest[ConfigT]` annotation on
|
|
245
|
+
`run()` provides typed configuration access without repeating the config type
|
|
246
|
+
in the base class.
|
|
247
|
+
When `AgentIdentity.skills` is omitted, the generated Agent Card advertises an
|
|
248
|
+
empty skill list. Declare explicit skills when clients need capability discovery.
|
|
249
|
+
|
|
250
|
+
The framework derives core A2A capabilities from implemented behavior.
|
|
251
|
+
Async-generator `run()` methods advertise streaming; coroutine `run()` methods
|
|
252
|
+
do not. Synchronous `run()` methods are rejected at startup. Push notifications
|
|
253
|
+
and state-transition history remain `False` until the framework supplies their
|
|
254
|
+
required runtime services.
|
|
255
|
+
Extensions come from explicit definitions, configured activations, and
|
|
256
|
+
decorated handlers in one validated registry, so routes and card advertisement
|
|
257
|
+
cannot drift.
|
|
258
|
+
|
|
259
|
+
`run(request)` is the required execution contract. It is an ordinary method
|
|
260
|
+
and needs no decorator.
|
|
261
|
+
|
|
262
|
+
For request-scoped streaming, implement `run()` as an async generator. Yield
|
|
263
|
+
`TaskProgress` for informational updates or validated non-terminal A2A status
|
|
264
|
+
updates. End every stream with one authoritative `TaskResult`; the framework
|
|
265
|
+
closes the generator after that result and owns terminal task state and
|
|
266
|
+
persistence. A runtime that already has a final file may return it as a
|
|
267
|
+
`FilePart` in the result message. Files created in an agent workspace remain a
|
|
268
|
+
runtime/control-plane collection concern rather than an A2A task-result API.
|
|
269
|
+
|
|
270
|
+
Each `run()` invocation maps to one A2A task execution. Related tasks share a
|
|
271
|
+
`context_id`. When a native runtime assigns a different conversation, session,
|
|
272
|
+
or thread identifier, return it as `TaskResult.session_ref`; the framework
|
|
273
|
+
supplies it as `TaskRequest.session_ref` on the next task in that context.
|
|
274
|
+
`session_ref` is SDK-local runtime state and is not added to the A2A wire
|
|
275
|
+
protocol. `TaskRequest` and `TaskResult` are framework boundary types; the
|
|
276
|
+
executor maps them to and from the wire-level `a2a.types.Task` lifecycle.
|
|
277
|
+
|
|
278
|
+
`TaskRequest` is a frozen record whose nested JSON values are detached copies.
|
|
279
|
+
Its `config`, `mcp_servers`, `skills`, `metadata`, and inbound `DataPart.data`
|
|
280
|
+
retain their declared `dict`/`list` types, so normal Pydantic serialization,
|
|
281
|
+
copying, and `json.dumps(...)` work. Within `tasks.v1`, new request fields are
|
|
282
|
+
additive and have framework defaults. Agent code returns a `TaskResult` through
|
|
283
|
+
its factories or builder so additions to the result contract do not break
|
|
284
|
+
existing handlers.
|
|
285
|
+
|
|
286
|
+
Expected execution failures are returned as `TaskResult.failure(code, message)`
|
|
287
|
+
and become failed A2A tasks with `error_type`, `error_code`, and `error_message`
|
|
288
|
+
in the terminal message. `error_type` is the platform classification
|
|
289
|
+
(`agent_error` by default, or explicitly `infra_error` for a retryable
|
|
290
|
+
infrastructure failure); `error_code` preserves the author's machine-readable
|
|
291
|
+
code. An exception escaping `run()`, an invalid return value, or a
|
|
292
|
+
result-mapping failure is logged with a correlation ID and reported as an
|
|
293
|
+
`infra_error` with code `framework.unhandled_exception`; raw exception text is
|
|
294
|
+
never sent to callers.
|
|
295
|
+
Invalid input that prevents task creation returns JSON-RPC `InvalidParams`.
|
|
296
|
+
Once a task exists, setup failures—including config construction—also produce
|
|
297
|
+
a terminal failed task rather than leaving it submitted or working.
|
|
298
|
+
A successful `TaskResult` must contain at least one text, file, or data part;
|
|
299
|
+
the framework rejects empty successes rather than emitting an ungradeable task.
|
|
300
|
+
The SDK does not retry tasks.
|
|
301
|
+
|
|
302
|
+
`enable(..., description="...")` is reserved for declarations carrying
|
|
303
|
+
configuration or metadata. It preserves the agent-specific extension prose
|
|
304
|
+
published in the Agent Card. The versioned SDK definition provides a generic
|
|
305
|
+
fallback, while the activation can describe runtime-specific behavior without
|
|
306
|
+
putting mutable card metadata on `@extension(...)` operation references.
|
|
307
|
+
A bare definition in `extensions=` declares support implemented opaquely inside
|
|
308
|
+
`run()` or entirely by the framework. Binding a standard or custom operation
|
|
309
|
+
with `@extension(...)` automatically activates its extension, so no duplicate
|
|
310
|
+
entry in `extensions=` is required.
|
|
311
|
+
|
|
312
|
+
Configuration keywords are definition-owned rather than hardcoded in
|
|
313
|
+
`enable()`. A configurable `ExtensionDefinition` supplies a named keyword-only
|
|
314
|
+
`configuration_validator` returning `ExtensionConfiguration` with card
|
|
315
|
+
`wire_params`, internal `options`, and optional `features`. `enable()` only
|
|
316
|
+
dispatches to that callable. Definitions without a validator reject
|
|
317
|
+
configuration keywords, and third-party definitions use the same public API as
|
|
318
|
+
the built-ins.
|
|
319
|
+
|
|
320
|
+
Extension request parsing and schema validation failures return HTTP 400.
|
|
321
|
+
Unhandled exceptions raised by an implementation handler return HTTP 500;
|
|
322
|
+
handlers use `HTTPException` when they intentionally need another status.
|
|
323
|
+
|
|
324
|
+
Passing `config=MyAgentConfig` automatically enables `AGENT_CONFIG_V1`; direct
|
|
325
|
+
`enable(AGENT_CONFIG_V1, ...)` declarations are rejected. The model's fields become the Agent Card's
|
|
326
|
+
supported config fields, its defaults seed every task, and Pydantic validates
|
|
327
|
+
each deployment-time update. `request.config` is a detached, frozen
|
|
328
|
+
`MyAgentConfig`; its JSON-native nested fields remain mutable and serializable.
|
|
329
|
+
Runtime code uses typed attributes such as `request.config.model` rather than
|
|
330
|
+
string-keyed lookups. `AgentConfig` supplies the platform-owned `name`,
|
|
331
|
+
`description`, `role`, and `timeout_seconds` fields. Agents apply
|
|
332
|
+
`request.config.timeout_seconds` to their runtime, model, or subprocess call.
|
|
333
|
+
Simple Pydantic field aliases are the corresponding wire names in the Agent
|
|
334
|
+
Card and `/ext/agent-config` payloads.
|
|
335
|
+
The card publishes the validation schema but omits literal default values;
|
|
336
|
+
runtime-derived defaults therefore remain private to the agent process.
|
|
337
|
+
For compatibility with the current AgentEnv control plane, `role` is also
|
|
338
|
+
projected into the generic `TaskRequest.metadata` mapping. Incoming A2A message
|
|
339
|
+
metadata is preserved, and a non-null configured value takes precedence.
|
|
340
|
+
Runtime-specific fields must also have defaults, allowing partial updates to be
|
|
341
|
+
validated against a complete model.
|
|
342
|
+
Readback returns only explicitly set values.
|
|
343
|
+
Declare sensitive fields as `WriteOnly[T]`; the generated schema advertises
|
|
344
|
+
them as `writeOnly`, readback returns `"***"`, and agent code still receives the
|
|
345
|
+
validated value as type `T`. Pass `config_readback=False` to `@a2a_agent(...)`
|
|
346
|
+
to omit the GET operation entirely.
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
from agentenv_protocol.a2a_agent import AgentConfig, WriteOnly
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class MyAgentConfig(AgentConfig):
|
|
353
|
+
provider_token: WriteOnly[str | None] = None
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`output_format` is author-owned in v1 rather than a field on the base
|
|
357
|
+
`AgentConfig`. An agent that supports structured output must declare the field
|
|
358
|
+
on its config subclass, apply it to its model or runtime, and return the value
|
|
359
|
+
with `TaskResult.builder().add_structured_output(...)`. When an agent does not
|
|
360
|
+
advertise the field, AgentEnv's config negotiation omits it; the task may still
|
|
361
|
+
succeed with text-only output, and callers must not assume `structured_output`
|
|
362
|
+
will be present.
|
|
363
|
+
|
|
364
|
+
`request.metadata` exposes generic A2A message metadata. The SDK does not assign
|
|
365
|
+
provider-specific attribution semantics to it; an agent may pass the mapping to
|
|
366
|
+
downstream clients that accept metadata. The examples forward it unchanged to
|
|
367
|
+
their model client rather than declaring provider-specific config fields.
|
|
368
|
+
|
|
369
|
+
Runtime-owned extension behavior is attached with the single generic
|
|
370
|
+
`@extension(...)` decorator. Its argument is a versioned SDK operation
|
|
371
|
+
reference, so agent code does not repeat URIs, paths, or wire schemas. The
|
|
372
|
+
following decorators activate `SNAPSHOT_V1` and `TRAJECTORY_V1` automatically:
|
|
373
|
+
|
|
374
|
+
Extension handlers must be async functions and use the request type owned by
|
|
375
|
+
their operation. Operations with a body require exactly one argument annotated
|
|
376
|
+
with that public Pydantic model; bodyless operations require a zero-argument
|
|
377
|
+
handler. The framework validates the handler and request before invocation and
|
|
378
|
+
derives the Agent Card's required and optional fields from the same model.
|
|
379
|
+
Optional fields are part of the operation contract: every implementation
|
|
380
|
+
accepts them, while callers may omit them. Agent-specific capabilities use
|
|
381
|
+
explicit features or request variants.
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
from agentenv_protocol.a2a_agent import (
|
|
385
|
+
ContextObjectTrajectoryRequest,
|
|
386
|
+
ObjectSnapshotLoadRequest,
|
|
387
|
+
ObjectSnapshotSaveRequest,
|
|
388
|
+
SNAPSHOT_V1,
|
|
389
|
+
TRAJECTORY_V1,
|
|
390
|
+
extension,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@extension(SNAPSHOT_V1.save)
|
|
395
|
+
async def save_snapshot(self, request: ObjectSnapshotSaveRequest):
|
|
396
|
+
...
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@extension(SNAPSHOT_V1.load)
|
|
400
|
+
async def load_snapshot(self, request: ObjectSnapshotLoadRequest):
|
|
401
|
+
...
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@extension(TRAJECTORY_V1.get.context_objects)
|
|
405
|
+
async def get_live_trajectory(self, request: ContextObjectTrajectoryRequest):
|
|
406
|
+
...
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The last handler opts that agent into the optional live-context variant of
|
|
410
|
+
`TRAJECTORY_V1.get` that uploads through an object grant;
|
|
411
|
+
`TRAJECTORY_V1.get.context` (`ContextTrajectoryRequest`) is its inline
|
|
412
|
+
counterpart. Without them the generated card advertises only the
|
|
413
|
+
framework-owned completed-task variants: `{task_id}`, answered inline, and
|
|
414
|
+
`{task_id, objects}`, answered by upload.
|
|
415
|
+
Snapshot `save` and `load` are an atomic core contract, while its optional
|
|
416
|
+
changelog handlers are enabled as an atomic feature group.
|
|
417
|
+
Each operation has at most one response model. The framework validates a
|
|
418
|
+
handler's return value against it before serializing, so a response with a
|
|
419
|
+
missing or unknown field is an HTTP 500.
|
|
420
|
+
|
|
421
|
+
After a successful `SKILL_CONFIG_V1.add` handler call, the framework records
|
|
422
|
+
the skill's `name` and `description`, plus `skill_md` for an inline skill, and
|
|
423
|
+
includes that record in the detached `TaskRequest.skills` snapshot for later
|
|
424
|
+
task executions; a bundle's read grants are not kept. It also owns
|
|
425
|
+
`SKILL_CONFIG_V1.list` and projects the installed skill onto the live Agent
|
|
426
|
+
Card. Agent implementations only install the skill into their runtime; they do
|
|
427
|
+
not implement listing or mutate framework/card state. The SDK contract accepts
|
|
428
|
+
inline `skill_md` for simple single-file skills and `BundleSkillRequest` for
|
|
429
|
+
multi-file or stored skills. A skill name is one path segment matching
|
|
430
|
+
`[A-Za-z0-9][A-Za-z0-9._-]{0,127}`, so a runtime can use it as a directory name.
|
|
431
|
+
Identity skills remain discoverable but are not injected into
|
|
432
|
+
`TaskRequest.skills`. Duplicate names are rejected before installation.
|
|
433
|
+
|
|
434
|
+
An agent can narrowly replace an SDK implementation while retaining the SDK's
|
|
435
|
+
wire contract:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from agentenv_protocol.a2a_agent import (
|
|
439
|
+
TRIGGERS_V1,
|
|
440
|
+
TriggerDecideRequest,
|
|
441
|
+
TriggerRegisterRequest,
|
|
442
|
+
extension,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@extension(TRIGGERS_V1.register)
|
|
447
|
+
async def register_triggers(self, request: TriggerRegisterRequest):
|
|
448
|
+
return await self.default_handlers.call(TRIGGERS_V1.register, request)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@extension(TRIGGERS_V1.decide)
|
|
452
|
+
async def decide_trigger(self, request: TriggerDecideRequest):
|
|
453
|
+
decision = await self.default_handlers.call(TRIGGERS_V1.decide, request)
|
|
454
|
+
# Augment the SDK decision while preserving register/decide/state storage.
|
|
455
|
+
...
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@extension(TRIGGERS_V1.state)
|
|
459
|
+
async def trigger_state(self):
|
|
460
|
+
return await self.default_handlers.call(TRIGGERS_V1.state)
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Because `TRIGGERS_V1.decide` is SDK-owned, the registry automatically
|
|
464
|
+
classifies this handler as an override. Such overrides are logged at startup
|
|
465
|
+
and reported by `app.state.agentenv_a2a.registry.conformance()`. The override API
|
|
466
|
+
deliberately accepts no operational metadata. Non-standard extensions use
|
|
467
|
+
`@custom_extension(...)`; that escape hatch rejects the `urn:agentenv:*`
|
|
468
|
+
namespace.
|
|
469
|
+
|
|
470
|
+
All active SDK-owned operations for an extension form one override group. An
|
|
471
|
+
agent must override every operation in that group or none of them, preventing
|
|
472
|
+
custom and default handlers from observing different state. Partial overrides
|
|
473
|
+
fail during application creation. A complete override can reuse SDK behavior
|
|
474
|
+
through `await self.default_handlers.call(OPERATION, request)` and augment the
|
|
475
|
+
returned value while retaining the default shared state.
|
|
476
|
+
|
|
477
|
+
`@custom_extension(...)` is single-operation sugar. A custom URI with multiple
|
|
478
|
+
operations must use one shared public `ExtensionDefinition`, with each method
|
|
479
|
+
bound through `@extension(DEFINITION.operation)`. Repeating
|
|
480
|
+
`@custom_extension(...)` for the same URI creates conflicting definitions.
|
|
481
|
+
Shared custom definitions activate from their discovered handlers and produce
|
|
482
|
+
one Agent Card extension containing all operations.
|
|
483
|
+
|
|
484
|
+
Reserved `urn:agentenv:*` URIs must use the canonical SDK definition even when
|
|
485
|
+
constructing `ExtensionDefinition` or `OperationReference` directly. Extension
|
|
486
|
+
routes are rejected when they collide with `GET /health`, the Agent Card route,
|
|
487
|
+
or `POST` on the configured A2A JSON-RPC URL.
|
|
488
|
+
|
|
489
|
+
Existing v1 extensions keep their frozen unversioned routes. New extension
|
|
490
|
+
versions must use distinct resource-local versioned paths (for example
|
|
491
|
+
`/ext/mcp-config/v2`), and startup rejects duplicate `(path, HTTP method)`
|
|
492
|
+
registrations. Consumers use the endpoint advertised by the selected Agent Card
|
|
493
|
+
extension rather than constructing paths.
|
|
494
|
+
|
|
495
|
+
`MCP_CONFIG_V1.list` returns a name-keyed object, never a bare list:
|
|
496
|
+
`{"mcp_servers": {name: {"url": url, "has_headers": bool}}}`. Header values
|
|
497
|
+
are not exposed. `MCP_CONFIG_V1.add` requires `url` and advertises `headers` and
|
|
498
|
+
`name` as optional request fields: `headers` so authenticated deployments match
|
|
499
|
+
discovery, `name` so the caller can choose the server alias the harness prefixes
|
|
500
|
+
tools with (agent-env relays the env card's name: the MultiEnv's declared name, else
|
|
501
|
+
`env` + 4 random digits, giving e.g. `mcp__env4821__<tool>`); when absent the agent mints
|
|
502
|
+
`mcp_<8 hex>`.
|
|
503
|
+
|
|
504
|
+
Runnable, self-contained reference agents live in [`examples/`](https://github.com/scaleapi/agentenv-framework/tree/main/packages/agentenv-protocol/examples):
|
|
505
|
+
the normal, streaming, and multimodal `run(request)` paths, single- and
|
|
506
|
+
multi-operation custom extensions, and advanced ASGI-lifespan plus
|
|
507
|
+
common SDK-operation override hooks.
|
|
508
|
+
|
|
509
|
+
The agent examples make real OpenAI-compatible model calls. Set
|
|
510
|
+
`LITELLM_API_KEY` and, when needed, `LITELLM_BASE_URL`; their typed agent config
|
|
511
|
+
selects the model and system prompt for each deployment. Tests inject a fake
|
|
512
|
+
model client, so the example suite remains offline and deterministic.
|
|
513
|
+
|
|
514
|
+
### Object transfer
|
|
515
|
+
|
|
516
|
+
Skill bundles, trajectories, snapshots and changelog increments move as bytes
|
|
517
|
+
through short-lived HTTPS grants that AgentEnv issues from its object store, so
|
|
518
|
+
an agent never holds storage credentials or a provider location. The types and
|
|
519
|
+
helpers live in `agentenv_protocol.transfers`, which depends only on `pydantic`
|
|
520
|
+
and `httpx`; `agentenv_protocol.a2a_agent` re-exports them.
|
|
521
|
+
|
|
522
|
+
| Type | Wire shape |
|
|
523
|
+
| --- | --- |
|
|
524
|
+
| `HttpGetGrant`, `HttpPutGrant` | `{kind: "http-get" \| "http-put", url, expires_at, headers?}`, one exact object |
|
|
525
|
+
| `HttpPostPolicyGrant` | `{kind: "http-post-policy", url, fields, path_field, file_field, headers?}`, multipart POST |
|
|
526
|
+
| `WriteNamespaceGrant` | `{root_path, expires_at, max_objects, max_object_bytes, max_total_bytes, write: HttpPostPolicyGrant}` |
|
|
527
|
+
| `ReadObject` | `{media_type, max_bytes, size_bytes?, sha256?, read: HttpGetGrant}` |
|
|
528
|
+
| `WriteObject` | `{media_type, max_bytes, write: HttpPutGrant}` |
|
|
529
|
+
| `Uploaded` | `{size_bytes, sha256?}` |
|
|
530
|
+
|
|
531
|
+
URLs are absolute HTTPS and timestamps are UTC. `size_bytes` and `sha256`
|
|
532
|
+
describe the stored bytes. The extensions use them as follows (`?` marks an
|
|
533
|
+
optional field, `|` an alternative request):
|
|
534
|
+
|
|
535
|
+
| Operation | Request | Response |
|
|
536
|
+
| --- | --- | --- |
|
|
537
|
+
| skill `add` | `{name, description, skill_md}` \| `{name, description, skill_bundle: {max_total_bytes, files: [{path, object: ReadObject}]}}` | `{name}` |
|
|
538
|
+
| trajectory `get` | `{task_id}` \| `{context_id}` | `{trajectory}` |
|
|
539
|
+
| | `{task_id \| context_id, objects: {trajectory: WriteObject}}` | `{objects: {trajectory: Uploaded}}` |
|
|
540
|
+
| snapshot `save` | `{context_id, objects: {trajectory: WriteObject, workspace?: WriteObject}}` | `{context_id, objects: {trajectory: Uploaded, workspace?: Uploaded}}` |
|
|
541
|
+
| snapshot `load` | `{objects: {trajectory: ReadObject, workspace?: ReadObject}, target_context_id?}` | `{context_id}` |
|
|
542
|
+
| `enable-changelog` | `{write_namespace: WriteNamespaceGrant, roots?}` | `{roots}` |
|
|
543
|
+
| `apply-changelog` | `{increments: [{sequence, object: ReadObject}], resume_conversation?, target_context_id?}` | `{count, context_id?}` |
|
|
544
|
+
|
|
545
|
+
A bundle contains a root `SKILL.md`; its paths are unique, normalized and
|
|
546
|
+
relative, and their `max_bytes` sum to at most `max_total_bytes`. An uploaded
|
|
547
|
+
trajectory is the JSON encoding of the trajectory value. Snapshot objects are
|
|
548
|
+
opaque `application/octet-stream` in the runtime's own format; when AgentEnv
|
|
549
|
+
sends a workspace grant, the agent uploads the workspace. A changelog agent
|
|
550
|
+
names each increment under the namespace root by its absolute zero-based
|
|
551
|
+
tool-call position, six digits plus an optional extension (`000042.tar`);
|
|
552
|
+
positions are unique but may be sparse, and apply receives them in increasing
|
|
553
|
+
`sequence` order, or none when a rewind stops before the first tool call.
|
|
554
|
+
AgentEnv ignores response fields it does not know, so a response may carry more
|
|
555
|
+
than these shapes; the SDK still refuses unknown request fields.
|
|
556
|
+
|
|
557
|
+
`upload(target, source)` and `download(source, destination)` move one object;
|
|
558
|
+
`NamespaceUploader(grant).upload(relative_path, source)` writes under a
|
|
559
|
+
namespace. An upload's source is a file path or bytes already in memory. Use one uploader per grant: it runs uploads one at a time, counts an
|
|
560
|
+
overwrite once and enforces the grant's limits. The helpers stream within the
|
|
561
|
+
limits, refuse expired grants and redirects, download with identity encoding and
|
|
562
|
+
check the raw bytes' size and hash, and retry `transfer_unavailable` and
|
|
563
|
+
`transfer_timeout` up to three attempts while the grant is unexpired. A
|
|
564
|
+
`TransferError` raised by a handler is returned with the status below and the
|
|
565
|
+
body `{"error": {"code", "message", "retryable"}}`; the message never carries
|
|
566
|
+
grant material, provider response bodies or local paths.
|
|
567
|
+
|
|
568
|
+
| Code | HTTP | Retryable with the same grant |
|
|
569
|
+
| --- | ---: | --- |
|
|
570
|
+
| `invalid_transfer` | 400 | No |
|
|
571
|
+
| `grant_expired` | 410 | No |
|
|
572
|
+
| `transfer_too_large` | 413 | No |
|
|
573
|
+
| `integrity_mismatch` | 422 | No |
|
|
574
|
+
| `transfer_rejected` | 502 | No |
|
|
575
|
+
| `transfer_unavailable` | 502 | Yes |
|
|
576
|
+
| `transfer_timeout` | 504 | While unexpired |
|
|
577
|
+
|
|
578
|
+
Grants are secrets: agents must not log, store or echo them, and the helpers
|
|
579
|
+
keep grant URLs out of `httpx` logs. A provider signature authorizes storage
|
|
580
|
+
access but does not prove that AgentEnv chose the URL, so endpoint and egress
|
|
581
|
+
controls remain the trust boundary. The limits are enforced by the helpers,
|
|
582
|
+
that is by the uploading agent. AgentEnv accepts a trajectory upload response
|
|
583
|
+
without reading the object back, and registers a snapshot only once both its
|
|
584
|
+
objects are in the store.
|
|
585
|
+
|
|
586
|
+
SDK agents advertise only these shapes. AgentEnv reads each Agent Card, sends
|
|
587
|
+
the object variants when the agent advertises them and its object store issues
|
|
588
|
+
grants (the S3 store does, and namespace grants for changelog capture only when
|
|
589
|
+
it signs with long-term credentials), and keeps the older `s3_prefix`, `skill_s3_url` and
|
|
590
|
+
`trajectory_s3_prefix` shapes for agents that advertise those instead. An SDK
|
|
591
|
+
agent built on this protocol therefore needs an agent-env release that includes
|
|
592
|
+
it: an older release sends the older shapes, which such an agent refuses apart
|
|
593
|
+
from inline skills and trajectories. Roll out in this
|
|
594
|
+
order: release agent-env and `agentenv-framework-protocol` together, move every service
|
|
595
|
+
that embeds agent-env to that release, and only then build agents on the new
|
|
596
|
+
SDK. A snapshot or changelog is restored in the form it was captured in: one
|
|
597
|
+
captured as objects only through the object variants, an older one only
|
|
598
|
+
through `s3_prefix`. Do not roll agent-env back once portable snapshots or
|
|
599
|
+
changelogs exist, because older releases cannot load them.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
agentenv_protocol/__init__.py,sha256=Z247Fd0glb-U-fuqiAmwas5yEeuhz2hTFLttMxbcvn0,2453
|
|
2
|
+
agentenv_protocol/agent_env_environment.py,sha256=WWZ4iQc6jyoLxO3JeapbCqqj7EfnYfHYQh5Awct8JBA,29162
|
|
3
|
+
agentenv_protocol/client.py,sha256=ShDTS2PGbOGPP6WxrsVXIS0y4wkwK9zCTfD1UbZbOG0,8506
|
|
4
|
+
agentenv_protocol/manifest.py,sha256=T-bOI6LKpFvR4iFZ0rg3iYPDz2NpOIaJWofygDiJusc,7049
|
|
5
|
+
agentenv_protocol/preflight.py,sha256=8BGVgSJ4nOhEIEh9iWgrPc7J5Bun1giYp445AFdkGd0,3155
|
|
6
|
+
agentenv_protocol/transfers.py,sha256=oAZjCRPM2NovxuOv8nAFGw47ZYqtMUEam90fUs2wWcs,18917
|
|
7
|
+
agentenv_protocol/types.py,sha256=71yxo9xHy1NNuSRChs6yHwZEnfmruU_wVbufLHLeFqU,5106
|
|
8
|
+
agentenv_protocol/a2a_agent/__init__.py,sha256=Ks-TvsXYu3rdmbHcgpT0iT1jIBP1AH-Oawe4nWsibmg,4581
|
|
9
|
+
agentenv_protocol/a2a_agent/_triggers.py,sha256=TC_Y7k4Fv13TIBH6CSvOAQaN7jPdIAfrAjW0ekx5-Es,19249
|
|
10
|
+
agentenv_protocol/a2a_agent/extensions.py,sha256=VHMSYSh85cGIXYtUWZZhPZQHtJBkV-DO32i1wp9no4s,38970
|
|
11
|
+
agentenv_protocol/a2a_agent/framework.py,sha256=2FjXiVR_AgRktj6WxiLY2FHxruuTzGs8DLgOsVcb_F0,49751
|
|
12
|
+
agentenv_protocol/a2a_agent/registry.py,sha256=TCfmvLfYthVSyH2xPLJXiG545mo7_ITiUd7oVZWXXV8,18469
|
|
13
|
+
agentenv_protocol/a2a_agent/tasks/__init__.py,sha256=TbdAY18WuSDxKo3TeDjFF9z3VvL4UMgSxWT6DlK2Nd8,667
|
|
14
|
+
agentenv_protocol/a2a_agent/tasks/v1.py,sha256=YQEZbkGUQhGy2IErel5X5elqRQwdVhuyQ_DgMadthyg,13972
|
|
15
|
+
agentenv_framework_protocol-0.1.269.dist-info/METADATA,sha256=wqu7kUHwLSZ4KIBoY4H8EoAFTvLq87qDPZZpFlKk9lI,29879
|
|
16
|
+
agentenv_framework_protocol-0.1.269.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
17
|
+
agentenv_framework_protocol-0.1.269.dist-info/licenses/LICENSE,sha256=tDzJZMZjW-flCBnzsHtIbtYZ6O0TrZxH7lQFPCzdWNo,11339
|
|
18
|
+
agentenv_framework_protocol-0.1.269.dist-info/licenses/NOTICE,sha256=VpxCebMPcCz2R4HL8BD4M2hHjKWnUPmKSrng8h33ykc,127
|
|
19
|
+
agentenv_framework_protocol-0.1.269.dist-info/licenses/THIRD_PARTY_NOTICES.md,sha256=MNvR7fi3VB6LzEiv2D6Xr6TcNtMwjVCqlu9pWVsCnGs,81835
|
|
20
|
+
agentenv_framework_protocol-0.1.269.dist-info/RECORD,,
|