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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any