agentenv-framework-protocol 0.1.319__tar.gz → 0.1.321__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/PKG-INFO +81 -8
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/README.md +80 -7
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/a2a_agent/__init__.py +10 -0
- agentenv_framework_protocol-0.1.321/src/agentenv_protocol/a2a_agent/_trajectory_log.py +244 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/a2a_agent/extensions.py +79 -3
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/a2a_agent/framework.py +93 -48
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +2 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/a2a_agent/tasks/v1.py +67 -1
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_a2a_agent.py +19 -14
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_a2a_object_transfers.py +5 -4
- agentenv_framework_protocol-0.1.321/tests/test_a2a_trajectory_log.py +634 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.319 → agentenv_framework_protocol-0.1.321}/tests/test_preflight.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentenv-framework-protocol
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.321
|
|
4
4
|
Summary: Open data-plane protocol and server SDK for agent environments.
|
|
5
5
|
Project-URL: Homepage, https://www.agentenvframework.com
|
|
6
6
|
Project-URL: Documentation, https://www.agentenvframework.com/docs
|
|
@@ -324,8 +324,9 @@ executor maps them to and from the wire-level `a2a.types.Task` lifecycle.
|
|
|
324
324
|
`TaskRequest` is a frozen record whose nested JSON values are detached copies.
|
|
325
325
|
Its `config`, `mcp_servers`, `skills`, `metadata`, and inbound `DataPart.data`
|
|
326
326
|
retain their declared `dict`/`list` types, so normal Pydantic serialization,
|
|
327
|
-
copying, and `json.dumps(...)` work.
|
|
328
|
-
|
|
327
|
+
copying, and `json.dumps(...)` work. `TaskRequest.trajectory` is the exception:
|
|
328
|
+
it is the task's live trajectory log, described below. Within `tasks.v1`, new
|
|
329
|
+
request fields are additive and have framework defaults. Agent code returns a `TaskResult` through
|
|
329
330
|
its factories or builder so additions to the result contract do not break
|
|
330
331
|
existing handlers.
|
|
331
332
|
|
|
@@ -348,7 +349,77 @@ The SDK does not retry tasks.
|
|
|
348
349
|
A client that gives up on a task sends `tasks/cancel`. The framework marks the
|
|
349
350
|
task canceled and cancels `run()`, which gets `asyncio.CancelledError` at its
|
|
350
351
|
next `await`. A process `run()` started keeps running unless `run()` stops it,
|
|
351
|
-
so kill it before re-raising.
|
|
352
|
+
so kill it before re-raising. Trajectory events it appended, including any it
|
|
353
|
+
appends while cleaning up, are kept, and its log ends `canceled`.
|
|
354
|
+
|
|
355
|
+
#### Live trajectories
|
|
356
|
+
|
|
357
|
+
A runtime that sees its native events as they happen appends each complete
|
|
358
|
+
event to `request.trajectory` and enables `TRAJECTORY_V1` with `live=True`:
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
@a2a_agent(identity=..., extensions=(enable(TRAJECTORY_V1, live=True),))
|
|
362
|
+
class MyAgent(AgentEnvAgent):
|
|
363
|
+
async def run(self, request: TaskRequest[MyAgentConfig]) -> TaskResult:
|
|
364
|
+
request.trajectory.set_format("my-cli-events/1")
|
|
365
|
+
execution = await run_my_cli(request, on_event=request.trajectory.append)
|
|
366
|
+
return TaskResult.text(execution.output)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`append(event)` takes a JSON object and serializes it at once, so an event
|
|
370
|
+
never changes after it is appended and a read never returns part of one. It does
|
|
371
|
+
no I/O; call it from one thread at a time. After the task ends the log refuses
|
|
372
|
+
further events. `set_format` names the events' versioned format, which readers
|
|
373
|
+
use to parse them; call it before the first `append`, which otherwise raises.
|
|
374
|
+
Once an event is appended the format cannot change.
|
|
375
|
+
|
|
376
|
+
The framework keeps each task's log and serves it while the task runs. When the
|
|
377
|
+
result carries no `native_trajectory`, the log is the task's final trajectory:
|
|
378
|
+
the completed-task reads `{task_id}` and `{task_id, objects}` return it as a JSON
|
|
379
|
+
array. A `native_trajectory` remains the final record of a runtime that can only
|
|
380
|
+
produce one at the end, and takes precedence over appended events.
|
|
381
|
+
|
|
382
|
+
`live=True` adds two cursor reads to `TRAJECTORY_V1.get`, both framework-owned:
|
|
383
|
+
|
|
384
|
+
| Request | Reads |
|
|
385
|
+
| --- | --- |
|
|
386
|
+
| `{task_id, after, limit?}` | one task's events |
|
|
387
|
+
| `{context_id, after, limit?}` | the events of every task in a context, in execution order |
|
|
388
|
+
|
|
389
|
+
Both answer `{context_id, task_id, state, format, events, next, has_more}`
|
|
390
|
+
(`TrajectoryEventsResponse`):
|
|
391
|
+
|
|
392
|
+
- `events` are the events from position `after`, zero-based. `next` is
|
|
393
|
+
`after` plus the number returned, the position to read from next; `has_more`
|
|
394
|
+
says more events are already readable past this page. A page holds at most
|
|
395
|
+
`limit` events (1 to 1,000, default 1,000) and about 4 MiB, but always at
|
|
396
|
+
least one event when one exists. A `limit` outside 1 to 1,000, or `after` past
|
|
397
|
+
the last event, is HTTP 400.
|
|
398
|
+
- `state` is `pending` before the first event, `running` while the task runs,
|
|
399
|
+
then `completed`, `failed` or `canceled`. A terminal state is reported only
|
|
400
|
+
once the log can no longer change, so a reader that has reached `has_more:
|
|
401
|
+
false` in a terminal state has every event. The terminal state is recorded
|
|
402
|
+
before the task's terminal A2A status, so the completed-task reads succeed as
|
|
403
|
+
soon as a client sees the task end.
|
|
404
|
+
- A context read reports the context's current task, the running one or else the
|
|
405
|
+
latest, and the most recent `format` any of its tasks set; its `state` is
|
|
406
|
+
`running` while any task runs.
|
|
407
|
+
- A task or context the agent has not seen, or no longer keeps, is HTTP 404. A
|
|
408
|
+
task is known from the moment `message/send` returns its id, so a 404 for an
|
|
409
|
+
id that was readable means it was evicted, and any cursor held for it, or for
|
|
410
|
+
its context, is no longer valid.
|
|
411
|
+
|
|
412
|
+
`live=True` also enables the context reads `{context_id}` and
|
|
413
|
+
`{context_id, objects}`; the framework answers them with the context's events
|
|
414
|
+
unless the agent binds its own handlers, which take precedence. An agent that
|
|
415
|
+
overrides the whole `get` operation must accept the cursor requests too.
|
|
416
|
+
Clients detect live support from the card, with
|
|
417
|
+
`card_request_accepts(get_request, {"task_id", "after"})`.
|
|
418
|
+
|
|
419
|
+
The logs are kept in memory for up to 1,024 contexts. Past that, whole contexts
|
|
420
|
+
are dropped, least recently used first; a context with a task still pending or
|
|
421
|
+
running is never dropped, nor is the one whose task has just ended. A running
|
|
422
|
+
task's log is logged as a warning once it passes 100 MiB.
|
|
352
423
|
|
|
353
424
|
`enable(..., description="...")` is reserved for declarations carrying
|
|
354
425
|
configuration or metadata. It preserves the agent-specific extension prose
|
|
@@ -363,7 +434,8 @@ entry in `extensions=` is required.
|
|
|
363
434
|
Configuration keywords are definition-owned rather than hardcoded in
|
|
364
435
|
`enable()`. A configurable `ExtensionDefinition` supplies a named keyword-only
|
|
365
436
|
`configuration_validator` returning `ExtensionConfiguration` with card
|
|
366
|
-
`wire_params`, internal `options`, and optional `features
|
|
437
|
+
`wire_params`, internal `options`, and optional `features` and request
|
|
438
|
+
`variants`. `enable()` only
|
|
367
439
|
dispatches to that callable. Definitions without a validator reject
|
|
368
440
|
configuration keywords, and third-party definitions use the same public API as
|
|
369
441
|
the built-ins.
|
|
@@ -460,9 +532,9 @@ async def get_live_trajectory(self, request: ContextObjectTrajectoryRequest):
|
|
|
460
532
|
The last handler opts that agent into the optional live-context variant of
|
|
461
533
|
`TRAJECTORY_V1.get` that uploads through an object grant;
|
|
462
534
|
`TRAJECTORY_V1.get.context` (`ContextTrajectoryRequest`) is its inline
|
|
463
|
-
counterpart. Without them
|
|
464
|
-
framework-owned completed-task variants:
|
|
465
|
-
`{task_id, objects}`, answered by upload.
|
|
535
|
+
counterpart. Without them, or `enable(TRAJECTORY_V1, live=True)`, the
|
|
536
|
+
generated card advertises only the framework-owned completed-task variants:
|
|
537
|
+
`{task_id}`, answered inline, and `{task_id, objects}`, answered by upload.
|
|
466
538
|
Snapshot `save` and `load` are an atomic core contract, while its optional
|
|
467
539
|
changelog handlers are enabled as an atomic feature group.
|
|
468
540
|
Each operation has at most one response model. The framework validates a
|
|
@@ -600,6 +672,7 @@ optional field, `|` an alternative request):
|
|
|
600
672
|
| skill `add` | `{name, description, skill_md}` \| `{name, description, skill_bundle: {max_total_bytes, files: [{path, object: ReadObject}]}}` | `{name}` |
|
|
601
673
|
| trajectory `get` | `{task_id}` \| `{context_id}` | `{trajectory}` |
|
|
602
674
|
| | `{task_id \| context_id, objects: {trajectory: WriteObject}}` | `{objects: {trajectory: Uploaded}}` |
|
|
675
|
+
| | `{task_id \| context_id, after, limit?}` | `{context_id, task_id, state, format, events, next, has_more}` |
|
|
603
676
|
| snapshot `save` | `{context_id, objects: {trajectory: WriteObject, workspace?: WriteObject}}` | `{context_id, objects: {trajectory: Uploaded, workspace?: Uploaded}}` |
|
|
604
677
|
| snapshot `load` | `{objects: {trajectory: ReadObject, workspace?: ReadObject}, target_context_id?}` | `{context_id}` |
|
|
605
678
|
| `enable-changelog` | `{write_namespace: WriteNamespaceGrant, roots?}` | `{roots}` |
|
|
@@ -280,8 +280,9 @@ executor maps them to and from the wire-level `a2a.types.Task` lifecycle.
|
|
|
280
280
|
`TaskRequest` is a frozen record whose nested JSON values are detached copies.
|
|
281
281
|
Its `config`, `mcp_servers`, `skills`, `metadata`, and inbound `DataPart.data`
|
|
282
282
|
retain their declared `dict`/`list` types, so normal Pydantic serialization,
|
|
283
|
-
copying, and `json.dumps(...)` work.
|
|
284
|
-
|
|
283
|
+
copying, and `json.dumps(...)` work. `TaskRequest.trajectory` is the exception:
|
|
284
|
+
it is the task's live trajectory log, described below. Within `tasks.v1`, new
|
|
285
|
+
request fields are additive and have framework defaults. Agent code returns a `TaskResult` through
|
|
285
286
|
its factories or builder so additions to the result contract do not break
|
|
286
287
|
existing handlers.
|
|
287
288
|
|
|
@@ -304,7 +305,77 @@ The SDK does not retry tasks.
|
|
|
304
305
|
A client that gives up on a task sends `tasks/cancel`. The framework marks the
|
|
305
306
|
task canceled and cancels `run()`, which gets `asyncio.CancelledError` at its
|
|
306
307
|
next `await`. A process `run()` started keeps running unless `run()` stops it,
|
|
307
|
-
so kill it before re-raising.
|
|
308
|
+
so kill it before re-raising. Trajectory events it appended, including any it
|
|
309
|
+
appends while cleaning up, are kept, and its log ends `canceled`.
|
|
310
|
+
|
|
311
|
+
#### Live trajectories
|
|
312
|
+
|
|
313
|
+
A runtime that sees its native events as they happen appends each complete
|
|
314
|
+
event to `request.trajectory` and enables `TRAJECTORY_V1` with `live=True`:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
@a2a_agent(identity=..., extensions=(enable(TRAJECTORY_V1, live=True),))
|
|
318
|
+
class MyAgent(AgentEnvAgent):
|
|
319
|
+
async def run(self, request: TaskRequest[MyAgentConfig]) -> TaskResult:
|
|
320
|
+
request.trajectory.set_format("my-cli-events/1")
|
|
321
|
+
execution = await run_my_cli(request, on_event=request.trajectory.append)
|
|
322
|
+
return TaskResult.text(execution.output)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`append(event)` takes a JSON object and serializes it at once, so an event
|
|
326
|
+
never changes after it is appended and a read never returns part of one. It does
|
|
327
|
+
no I/O; call it from one thread at a time. After the task ends the log refuses
|
|
328
|
+
further events. `set_format` names the events' versioned format, which readers
|
|
329
|
+
use to parse them; call it before the first `append`, which otherwise raises.
|
|
330
|
+
Once an event is appended the format cannot change.
|
|
331
|
+
|
|
332
|
+
The framework keeps each task's log and serves it while the task runs. When the
|
|
333
|
+
result carries no `native_trajectory`, the log is the task's final trajectory:
|
|
334
|
+
the completed-task reads `{task_id}` and `{task_id, objects}` return it as a JSON
|
|
335
|
+
array. A `native_trajectory` remains the final record of a runtime that can only
|
|
336
|
+
produce one at the end, and takes precedence over appended events.
|
|
337
|
+
|
|
338
|
+
`live=True` adds two cursor reads to `TRAJECTORY_V1.get`, both framework-owned:
|
|
339
|
+
|
|
340
|
+
| Request | Reads |
|
|
341
|
+
| --- | --- |
|
|
342
|
+
| `{task_id, after, limit?}` | one task's events |
|
|
343
|
+
| `{context_id, after, limit?}` | the events of every task in a context, in execution order |
|
|
344
|
+
|
|
345
|
+
Both answer `{context_id, task_id, state, format, events, next, has_more}`
|
|
346
|
+
(`TrajectoryEventsResponse`):
|
|
347
|
+
|
|
348
|
+
- `events` are the events from position `after`, zero-based. `next` is
|
|
349
|
+
`after` plus the number returned, the position to read from next; `has_more`
|
|
350
|
+
says more events are already readable past this page. A page holds at most
|
|
351
|
+
`limit` events (1 to 1,000, default 1,000) and about 4 MiB, but always at
|
|
352
|
+
least one event when one exists. A `limit` outside 1 to 1,000, or `after` past
|
|
353
|
+
the last event, is HTTP 400.
|
|
354
|
+
- `state` is `pending` before the first event, `running` while the task runs,
|
|
355
|
+
then `completed`, `failed` or `canceled`. A terminal state is reported only
|
|
356
|
+
once the log can no longer change, so a reader that has reached `has_more:
|
|
357
|
+
false` in a terminal state has every event. The terminal state is recorded
|
|
358
|
+
before the task's terminal A2A status, so the completed-task reads succeed as
|
|
359
|
+
soon as a client sees the task end.
|
|
360
|
+
- A context read reports the context's current task, the running one or else the
|
|
361
|
+
latest, and the most recent `format` any of its tasks set; its `state` is
|
|
362
|
+
`running` while any task runs.
|
|
363
|
+
- A task or context the agent has not seen, or no longer keeps, is HTTP 404. A
|
|
364
|
+
task is known from the moment `message/send` returns its id, so a 404 for an
|
|
365
|
+
id that was readable means it was evicted, and any cursor held for it, or for
|
|
366
|
+
its context, is no longer valid.
|
|
367
|
+
|
|
368
|
+
`live=True` also enables the context reads `{context_id}` and
|
|
369
|
+
`{context_id, objects}`; the framework answers them with the context's events
|
|
370
|
+
unless the agent binds its own handlers, which take precedence. An agent that
|
|
371
|
+
overrides the whole `get` operation must accept the cursor requests too.
|
|
372
|
+
Clients detect live support from the card, with
|
|
373
|
+
`card_request_accepts(get_request, {"task_id", "after"})`.
|
|
374
|
+
|
|
375
|
+
The logs are kept in memory for up to 1,024 contexts. Past that, whole contexts
|
|
376
|
+
are dropped, least recently used first; a context with a task still pending or
|
|
377
|
+
running is never dropped, nor is the one whose task has just ended. A running
|
|
378
|
+
task's log is logged as a warning once it passes 100 MiB.
|
|
308
379
|
|
|
309
380
|
`enable(..., description="...")` is reserved for declarations carrying
|
|
310
381
|
configuration or metadata. It preserves the agent-specific extension prose
|
|
@@ -319,7 +390,8 @@ entry in `extensions=` is required.
|
|
|
319
390
|
Configuration keywords are definition-owned rather than hardcoded in
|
|
320
391
|
`enable()`. A configurable `ExtensionDefinition` supplies a named keyword-only
|
|
321
392
|
`configuration_validator` returning `ExtensionConfiguration` with card
|
|
322
|
-
`wire_params`, internal `options`, and optional `features
|
|
393
|
+
`wire_params`, internal `options`, and optional `features` and request
|
|
394
|
+
`variants`. `enable()` only
|
|
323
395
|
dispatches to that callable. Definitions without a validator reject
|
|
324
396
|
configuration keywords, and third-party definitions use the same public API as
|
|
325
397
|
the built-ins.
|
|
@@ -416,9 +488,9 @@ async def get_live_trajectory(self, request: ContextObjectTrajectoryRequest):
|
|
|
416
488
|
The last handler opts that agent into the optional live-context variant of
|
|
417
489
|
`TRAJECTORY_V1.get` that uploads through an object grant;
|
|
418
490
|
`TRAJECTORY_V1.get.context` (`ContextTrajectoryRequest`) is its inline
|
|
419
|
-
counterpart. Without them
|
|
420
|
-
framework-owned completed-task variants:
|
|
421
|
-
`{task_id, objects}`, answered by upload.
|
|
491
|
+
counterpart. Without them, or `enable(TRAJECTORY_V1, live=True)`, the
|
|
492
|
+
generated card advertises only the framework-owned completed-task variants:
|
|
493
|
+
`{task_id}`, answered inline, and `{task_id, objects}`, answered by upload.
|
|
422
494
|
Snapshot `save` and `load` are an atomic core contract, while its optional
|
|
423
495
|
changelog handlers are enabled as an atomic feature group.
|
|
424
496
|
Each operation has at most one response model. The framework validates a
|
|
@@ -556,6 +628,7 @@ optional field, `|` an alternative request):
|
|
|
556
628
|
| skill `add` | `{name, description, skill_md}` \| `{name, description, skill_bundle: {max_total_bytes, files: [{path, object: ReadObject}]}}` | `{name}` |
|
|
557
629
|
| trajectory `get` | `{task_id}` \| `{context_id}` | `{trajectory}` |
|
|
558
630
|
| | `{task_id \| context_id, objects: {trajectory: WriteObject}}` | `{objects: {trajectory: Uploaded}}` |
|
|
631
|
+
| | `{task_id \| context_id, after, limit?}` | `{context_id, task_id, state, format, events, next, has_more}` |
|
|
559
632
|
| snapshot `save` | `{context_id, objects: {trajectory: WriteObject, workspace?: WriteObject}}` | `{context_id, objects: {trajectory: Uploaded, workspace?: Uploaded}}` |
|
|
560
633
|
| snapshot `load` | `{objects: {trajectory: ReadObject, workspace?: ReadObject}, target_context_id?}` | `{context_id}` |
|
|
561
634
|
| `enable-changelog` | `{write_namespace: WriteNamespaceGrant, roots?}` | `{roots}` |
|
|
@@ -16,6 +16,7 @@ from .extensions import (
|
|
|
16
16
|
AttributionProbeResponse,
|
|
17
17
|
BundleSkillRequest,
|
|
18
18
|
ChangelogIncrement,
|
|
19
|
+
ContextEventsTrajectoryRequest,
|
|
19
20
|
ContextObjectTrajectoryRequest,
|
|
20
21
|
ContextTrajectoryRequest,
|
|
21
22
|
ExtensionActivation,
|
|
@@ -48,9 +49,12 @@ from .extensions import (
|
|
|
48
49
|
SnapshotReadObjects,
|
|
49
50
|
SnapshotUploadedObjects,
|
|
50
51
|
SnapshotWriteObjects,
|
|
52
|
+
TaskEventsTrajectoryRequest,
|
|
51
53
|
TaskObjectTrajectoryRequest,
|
|
52
54
|
TaskTrajectoryRequest,
|
|
55
|
+
TrajectoryEventsResponse,
|
|
53
56
|
TrajectoryObjectsResponse,
|
|
57
|
+
TrajectoryState,
|
|
54
58
|
TrajectoryUploadedObjects,
|
|
55
59
|
TrajectoryWriteObjects,
|
|
56
60
|
TriggerDecideRequest,
|
|
@@ -92,6 +96,7 @@ from .tasks.v1 import (
|
|
|
92
96
|
TaskResultBuilder,
|
|
93
97
|
TaskStreamItem,
|
|
94
98
|
TextPart,
|
|
99
|
+
TrajectoryLog,
|
|
95
100
|
Usage,
|
|
96
101
|
WriteOnly,
|
|
97
102
|
)
|
|
@@ -131,6 +136,7 @@ __all__ = [
|
|
|
131
136
|
"AttributionProbeResponse",
|
|
132
137
|
"BundleSkillRequest",
|
|
133
138
|
"ChangelogIncrement",
|
|
139
|
+
"ContextEventsTrajectoryRequest",
|
|
134
140
|
"ContextObjectTrajectoryRequest",
|
|
135
141
|
"ContextTrajectoryRequest",
|
|
136
142
|
"DataPart",
|
|
@@ -177,6 +183,7 @@ __all__ = [
|
|
|
177
183
|
"SnapshotWriteObjects",
|
|
178
184
|
"StagingStore",
|
|
179
185
|
"TaskError",
|
|
186
|
+
"TaskEventsTrajectoryRequest",
|
|
180
187
|
"TaskObjectTrajectoryRequest",
|
|
181
188
|
"TaskOutcome",
|
|
182
189
|
"TaskPart",
|
|
@@ -187,7 +194,10 @@ __all__ = [
|
|
|
187
194
|
"TaskStreamItem",
|
|
188
195
|
"TaskTrajectoryRequest",
|
|
189
196
|
"TextPart",
|
|
197
|
+
"TrajectoryEventsResponse",
|
|
198
|
+
"TrajectoryLog",
|
|
190
199
|
"TrajectoryObjectsResponse",
|
|
200
|
+
"TrajectoryState",
|
|
191
201
|
"TrajectoryUploadedObjects",
|
|
192
202
|
"TrajectoryWriteObjects",
|
|
193
203
|
"TransferError",
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""The framework's in-process store of task trajectories, served by the trajectory extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from .extensions import TrajectoryState
|
|
10
|
+
from .tasks.v1 import TrajectoryLog
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
MAX_CACHED_CONTEXTS = 1_024
|
|
15
|
+
MAX_TRAJECTORY_READ_BYTES = 4 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class _TaskRecord:
|
|
20
|
+
context_id: str
|
|
21
|
+
log: TrajectoryLog
|
|
22
|
+
started: bool = False
|
|
23
|
+
cancel_requested: bool = False
|
|
24
|
+
final_state: TrajectoryState | None = None
|
|
25
|
+
native: bytes | None = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def state(self) -> TrajectoryState:
|
|
29
|
+
if self.final_state is not None:
|
|
30
|
+
return self.final_state
|
|
31
|
+
return TrajectoryState.RUNNING if len(self.log) else TrajectoryState.PENDING
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class TrajectoryPage:
|
|
36
|
+
context_id: str
|
|
37
|
+
task_id: str
|
|
38
|
+
state: TrajectoryState
|
|
39
|
+
format: str | None
|
|
40
|
+
events: list[bytes]
|
|
41
|
+
total: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TaskTrajectories:
|
|
45
|
+
"""Each task's log and final record, grouped by context in execution order.
|
|
46
|
+
|
|
47
|
+
Past ``max_contexts``, whole contexts are evicted, least recently used first, so
|
|
48
|
+
positions in a context read never shift. A context with a task pending or running is
|
|
49
|
+
never evicted, nor is the one whose task just ended, so its final record is readable."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, max_contexts: int = MAX_CACHED_CONTEXTS) -> None:
|
|
52
|
+
if max_contexts < 1:
|
|
53
|
+
raise ValueError("max_contexts must be positive")
|
|
54
|
+
self._max_contexts = max_contexts
|
|
55
|
+
self._tasks: dict[str, _TaskRecord] = {}
|
|
56
|
+
self._contexts: OrderedDict[str, list[str]] = OrderedDict()
|
|
57
|
+
|
|
58
|
+
def register(self, task_id: str, context_id: str) -> TrajectoryLog:
|
|
59
|
+
"""The task's log, created ``pending`` the first time the task is seen."""
|
|
60
|
+
record = self._tasks.get(task_id)
|
|
61
|
+
if record is None:
|
|
62
|
+
log = TrajectoryLog()
|
|
63
|
+
log._task_id = task_id
|
|
64
|
+
record = self._tasks[task_id] = _TaskRecord(context_id=context_id, log=log)
|
|
65
|
+
self._contexts.setdefault(context_id, []).append(task_id)
|
|
66
|
+
self._touch(record.context_id)
|
|
67
|
+
return record.log
|
|
68
|
+
|
|
69
|
+
def start(self, task_id: str) -> None:
|
|
70
|
+
"""Place the task after the context's started tasks, as it begins to run.
|
|
71
|
+
|
|
72
|
+
Only tasks that have not started move; they have no events, so no position shifts."""
|
|
73
|
+
record = self._tasks[task_id]
|
|
74
|
+
if record.started:
|
|
75
|
+
return
|
|
76
|
+
order = self._contexts[record.context_id]
|
|
77
|
+
order.remove(task_id)
|
|
78
|
+
started = sum(1 for other in order if self._tasks[other].started)
|
|
79
|
+
order.insert(started, task_id)
|
|
80
|
+
record.started = True
|
|
81
|
+
self._touch(record.context_id)
|
|
82
|
+
|
|
83
|
+
def request_cancel(self, task_id: str) -> None:
|
|
84
|
+
"""Make ``canceled`` the outcome the task is sealed with, whatever ``run`` returns."""
|
|
85
|
+
record = self._tasks.get(task_id)
|
|
86
|
+
if record is not None:
|
|
87
|
+
record.cancel_requested = True
|
|
88
|
+
|
|
89
|
+
def seal(
|
|
90
|
+
self, task_id: str, state: TrajectoryState, *, native: bytes | None = None
|
|
91
|
+
) -> None:
|
|
92
|
+
"""End the task's trajectory; the first seal wins."""
|
|
93
|
+
record = self._tasks[task_id]
|
|
94
|
+
if record.final_state is not None:
|
|
95
|
+
return
|
|
96
|
+
if native is not None:
|
|
97
|
+
if len(record.log):
|
|
98
|
+
logger.warning(
|
|
99
|
+
"task %s appended trajectory events and returned a native trajectory; "
|
|
100
|
+
"the native trajectory is its final record",
|
|
101
|
+
task_id,
|
|
102
|
+
)
|
|
103
|
+
record.native = native
|
|
104
|
+
record.log._sealed = True
|
|
105
|
+
record.final_state = TrajectoryState.CANCELED if record.cancel_requested else state
|
|
106
|
+
self._touch(record.context_id)
|
|
107
|
+
self._evict(keep=record.context_id)
|
|
108
|
+
|
|
109
|
+
def final(self, task_id: str) -> bytes | None:
|
|
110
|
+
"""The ended task's final trajectory as a JSON array, or None if there is none yet."""
|
|
111
|
+
record = self._tasks.get(task_id)
|
|
112
|
+
if record is None or record.final_state is None:
|
|
113
|
+
return None
|
|
114
|
+
self._touch(record.context_id)
|
|
115
|
+
if record.native is not None:
|
|
116
|
+
return record.native
|
|
117
|
+
if not len(record.log):
|
|
118
|
+
return None
|
|
119
|
+
return _json_array(record.log._events)
|
|
120
|
+
|
|
121
|
+
def context_events(self, context_id: str) -> bytes | None:
|
|
122
|
+
"""Every event of the context's tasks in order, as a JSON array."""
|
|
123
|
+
records = self._context_records(context_id)
|
|
124
|
+
if records is None:
|
|
125
|
+
return None
|
|
126
|
+
return _json_array([event for record in records for event in record.log._events])
|
|
127
|
+
|
|
128
|
+
def task_page(
|
|
129
|
+
self, task_id: str, after: int, max_events: int, max_bytes: int
|
|
130
|
+
) -> TrajectoryPage | None:
|
|
131
|
+
record = self._tasks.get(task_id)
|
|
132
|
+
if record is None:
|
|
133
|
+
return None
|
|
134
|
+
self._touch(record.context_id)
|
|
135
|
+
# State before length: a terminal state means the length can no longer grow.
|
|
136
|
+
state = record.state
|
|
137
|
+
events = record.log._events
|
|
138
|
+
total = len(events)
|
|
139
|
+
return TrajectoryPage(
|
|
140
|
+
context_id=record.context_id,
|
|
141
|
+
task_id=task_id,
|
|
142
|
+
state=state,
|
|
143
|
+
format=record.log.format,
|
|
144
|
+
events=_page(events, after, total, max_events, max_bytes),
|
|
145
|
+
total=total,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def context_page(
|
|
149
|
+
self, context_id: str, after: int, max_events: int, max_bytes: int
|
|
150
|
+
) -> TrajectoryPage | None:
|
|
151
|
+
records = self._context_records(context_id)
|
|
152
|
+
if records is None:
|
|
153
|
+
return None
|
|
154
|
+
task_ids = self._contexts[context_id]
|
|
155
|
+
states = [record.state for record in records]
|
|
156
|
+
lengths = [len(record.log) for record in records]
|
|
157
|
+
current = next(
|
|
158
|
+
(index for index, record in enumerate(records) if record.final_state is None),
|
|
159
|
+
len(records) - 1,
|
|
160
|
+
)
|
|
161
|
+
if TrajectoryState.RUNNING in states:
|
|
162
|
+
state = TrajectoryState.RUNNING
|
|
163
|
+
elif TrajectoryState.PENDING in states:
|
|
164
|
+
state = TrajectoryState.PENDING
|
|
165
|
+
else:
|
|
166
|
+
state = states[-1]
|
|
167
|
+
page: list[bytes] = []
|
|
168
|
+
offset = 0
|
|
169
|
+
for record, length in zip(records, lengths):
|
|
170
|
+
if len(page) >= max_events:
|
|
171
|
+
break
|
|
172
|
+
if after < offset + length:
|
|
173
|
+
start = max(after - offset, 0)
|
|
174
|
+
page.extend(
|
|
175
|
+
_page(
|
|
176
|
+
record.log._events,
|
|
177
|
+
start,
|
|
178
|
+
length,
|
|
179
|
+
max_events - len(page),
|
|
180
|
+
max_bytes - sum(map(len, page)),
|
|
181
|
+
first=not page,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
offset += length
|
|
185
|
+
return TrajectoryPage(
|
|
186
|
+
context_id=context_id,
|
|
187
|
+
task_id=task_ids[current],
|
|
188
|
+
state=state,
|
|
189
|
+
format=next(
|
|
190
|
+
(record.log.format for record in reversed(records) if record.log.format),
|
|
191
|
+
None,
|
|
192
|
+
),
|
|
193
|
+
events=page,
|
|
194
|
+
total=sum(lengths),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def _context_records(self, context_id: str) -> list[_TaskRecord] | None:
|
|
198
|
+
task_ids = self._contexts.get(context_id)
|
|
199
|
+
if not task_ids:
|
|
200
|
+
return None
|
|
201
|
+
self._touch(context_id)
|
|
202
|
+
return [self._tasks[task_id] for task_id in task_ids]
|
|
203
|
+
|
|
204
|
+
def _touch(self, context_id: str) -> None:
|
|
205
|
+
if context_id in self._contexts:
|
|
206
|
+
self._contexts.move_to_end(context_id)
|
|
207
|
+
|
|
208
|
+
def _evict(self, *, keep: str) -> None:
|
|
209
|
+
for context_id in tuple(self._contexts):
|
|
210
|
+
if len(self._contexts) <= self._max_contexts:
|
|
211
|
+
return
|
|
212
|
+
task_ids = self._contexts[context_id]
|
|
213
|
+
if context_id == keep or any(
|
|
214
|
+
self._tasks[task_id].final_state is None for task_id in task_ids
|
|
215
|
+
):
|
|
216
|
+
continue
|
|
217
|
+
del self._contexts[context_id]
|
|
218
|
+
for task_id in task_ids:
|
|
219
|
+
del self._tasks[task_id]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _page(
|
|
223
|
+
events: list[bytes],
|
|
224
|
+
start: int,
|
|
225
|
+
stop: int,
|
|
226
|
+
max_events: int,
|
|
227
|
+
max_bytes: int,
|
|
228
|
+
*,
|
|
229
|
+
first: bool = True,
|
|
230
|
+
) -> list[bytes]:
|
|
231
|
+
"""Events from ``start``, within both budgets; the first event of a read is always
|
|
232
|
+
returned, so one larger than ``max_bytes`` cannot stall a reader."""
|
|
233
|
+
page: list[bytes] = []
|
|
234
|
+
used = 0
|
|
235
|
+
for encoded in events[start:min(stop, start + max_events)]:
|
|
236
|
+
if used + len(encoded) > max_bytes and (page or not first):
|
|
237
|
+
break
|
|
238
|
+
page.append(encoded)
|
|
239
|
+
used += len(encoded)
|
|
240
|
+
return page
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _json_array(events: list[bytes]) -> bytes:
|
|
244
|
+
return b"[" + b",".join(events) + b"]"
|