agentenv-framework-protocol 0.1.391__tar.gz → 0.1.393__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/PKG-INFO +43 -7
  2. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/README.md +42 -6
  3. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/__init__.py +4 -0
  5. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/agent_env_environment.py +28 -1
  6. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/client.py +6 -2
  7. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/_declare.py +2 -1
  8. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/card.py +14 -8
  9. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/conformance/checks.py +14 -2
  10. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/dataplane.py +17 -8
  11. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/driver.py +2 -0
  12. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/extensions.py +14 -4
  13. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/recording.py +12 -1
  14. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/settings.py +1 -4
  15. agentenv_framework_protocol-0.1.393/src/agentenv_protocol/computer/skill.py +68 -0
  16. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/types.py +5 -0
  17. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_agent_env_environment.py +40 -0
  18. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_conformance.py +1 -1
  19. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_dataplane.py +31 -9
  20. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_driver.py +23 -3
  21. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_recording.py +27 -4
  22. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_data_objects.py +3 -1
  23. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/.gitignore +0 -0
  24. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/LICENSE +0 -0
  25. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/NOTICE +0 -0
  26. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/THIRD_PARTY_NOTICES.md +0 -0
  27. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/examples/__init__.py +0 -0
  28. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/examples/advanced_agent.py +0 -0
  29. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/examples/basic_agent.py +0 -0
  30. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/examples/custom_extensions_agent.py +0 -0
  31. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/examples/multimodal_agent.py +0 -0
  32. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/examples/reference_model.py +0 -0
  33. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/examples/streaming_agent.py +0 -0
  34. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  35. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  36. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  37. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  38. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  39. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  40. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  41. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  42. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  43. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  44. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  45. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/__init__.py +0 -0
  46. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/checks.py +0 -0
  47. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  48. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  49. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  50. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  51. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  52. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  53. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/errors.py +0 -0
  54. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/frames.py +0 -0
  55. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/keys.py +0 -0
  56. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/linux.py +0 -0
  57. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/manifest.py +0 -0
  58. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/operations.py +0 -0
  59. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/platform.py +0 -0
  60. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/queue.py +0 -0
  61. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/results.py +0 -0
  62. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/runtime.py +0 -0
  63. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/serve.py +0 -0
  64. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/spaces.py +0 -0
  65. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/computer/tools.py +0 -0
  66. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/errors.py +0 -0
  67. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/manifest.py +0 -0
  68. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/preflight.py +0 -0
  69. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/tool_access.py +0 -0
  70. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/tool_client.py +0 -0
  71. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/tool_context.py +0 -0
  72. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/src/agentenv_protocol/transfers.py +0 -0
  73. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/computer_fake.py +0 -0
  74. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/probe_fake.py +0 -0
  75. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_a2a_agent.py +0 -0
  76. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_a2a_agent_examples.py +0 -0
  77. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_a2a_object_transfers.py +0 -0
  78. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_a2a_peer_agents.py +0 -0
  79. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_a2a_proxy.py +0 -0
  80. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_a2a_staging.py +0 -0
  81. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_a2a_trajectory_log.py +0 -0
  82. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_client.py +0 -0
  83. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_conformance_checks.py +0 -0
  84. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_conformance_recording.py +0 -0
  85. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_declare.py +0 -0
  86. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_extensions.py +0 -0
  87. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_frames.py +0 -0
  88. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_queue.py +0 -0
  89. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_serve.py +0 -0
  90. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_computer_spaces.py +0 -0
  91. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_errors.py +0 -0
  92. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_extension_methods.py +0 -0
  93. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_intake.py +0 -0
  94. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_manifest.py +0 -0
  95. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_preflight.py +0 -0
  96. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_tool_access.py +0 -0
  97. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_tool_annotations.py +0 -0
  98. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_tool_client.py +0 -0
  99. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_tool_context.py +0 -0
  100. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_tool_context_fastmcp.py +0 -0
  101. {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.393}/tests/test_tool_context_injection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentenv-framework-protocol
3
- Version: 0.1.391
3
+ Version: 0.1.393
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
@@ -293,6 +293,32 @@ at the gateway.
293
293
 
294
294
  Where the binding lives depends on the app. On mcp's own FastMCP it wraps the tool manager's dispatch; on the standalone `fastmcp` package it joins the app's middleware chain as the outermost layer, so a handler's own middleware runs under it too. An app with neither seam mounts unbound, with a warning, unless a tool declares a `ToolContext` parameter or the handler defines `on_tool_call`: then the mount is refused rather than serving an empty caller.
295
295
 
296
+ ### Getting part of the data
297
+
298
+ A `@get_data` handler that takes a `paths` parameter returns only the data under the paths a caller
299
+ names (`client.get_data(base_url, paths=[...])`), and is called with None when the caller names
300
+ none. A handler without one answers `invalid_params` to a call that names paths, rather than
301
+ returning everything.
302
+
303
+ ### A skill for agents
304
+
305
+ An env can hand agents a skill: a SKILL.md an agent reads before it uses the env. Mark the method
306
+ that writes it with `@agent_skill`; it returns the skill's description and its markdown body,
307
+ sync or async:
308
+
309
+ ```python
310
+ @agent_skill
311
+ def guide(self):
312
+ return "How to place and track orders.", "# The shop\n\nPlace orders with `place_order`.\n"
313
+ ```
314
+
315
+ The card then declares `urn:agentenv:skill/v1` (`SKILL_EXTENSION_URI`), whose `get {name}`
316
+ answers `{name, description, skill_md}`, the shape `skill-config/v1` takes, with the frontmatter
317
+ written under the name the caller gives; a name that isn't lowercase letters, digits and single
318
+ hyphens, up to 64, is refused `invalid_argument`. agent-env's `add_skills` step installs it on
319
+ an agent through `env_ids`, named after the env's environment name; for an env behind a gateway,
320
+ whose card nests its servers' cards, it installs each server's skill under that server's name.
321
+
296
322
  ### Uploading an export through a grant
297
323
 
298
324
  An export too large to return from `data/get` can go straight to the caller's object store. A
@@ -449,6 +475,11 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
449
475
  (`stale_frame`) when the display geometry it was captured at changes.
450
476
  - **The input queue.** Input calls run one at a time in arrival order; one that waits past
451
477
  `queue_wait_s` is refused `input_busy`. Screenshots and waits never queue.
478
+ - **The computer skill.** `computer/v1` declares the seven tools, its params the driver's
479
+ features. The driver serves its skill through `skill/v1` (an `@agent_skill` handler on the base
480
+ class): a SKILL.md written from the computer as it booted (its platform, session user and home,
481
+ the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
482
+ class's `notes`, markdown for what its card can't say.
452
483
  - **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
453
484
  capture, plus the driver's own `ready_checks()`; tools refuse `device_unavailable` until they
454
485
  pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
@@ -458,8 +489,10 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
458
489
  - **The data plane.** `data/add` takes file parts that name their path, relative to the session
459
490
  user's home, under the `data-objects/v1` marker (`{"path": ...}`), as inline bytes, an https URL,
460
491
  or a `transfers.ReadObject` read grant under the marker's `read`, and writes them atomically.
461
- `data/get` returns the files under the settings' `data_paths`, inline or uploaded under a
462
- `write_namespace` grant.
492
+ `data/get` returns the files under the `paths` it's given, relative to the home, or under the
493
+ card's `default_data_paths` (`Desktop`, `Documents`, `Downloads`) when it's given none, inline or
494
+ uploaded under a `write_namespace` grant. An environment whose `@get_data` hook takes no `paths`
495
+ refuses them.
463
496
  - **The card** (the name from `ENVIRONMENT_NAME`, then `@computer_driver`'s), and per-role tool
464
497
  access (`tool_access` is on).
465
498
 
@@ -470,7 +503,7 @@ was sent (`applied: "no"`); `Timeout`, `DeliveryFailed` and `Internal` mean it m
470
503
  other exception from an input operation is reported `delivery_failed` with `applied: "unknown"`.
471
504
 
472
505
  **Settings** reach the driver as one JSON value in `AGENTENV_COMPUTER_SETTINGS`
473
- (`ComputerSettings`): `data_paths`, `default_space`, `default_norm_scale`, `queue_wait_s`,
506
+ (`ComputerSettings`): `default_space`, `default_norm_scale`, `queue_wait_s`,
474
507
  `tap_hold_ms`, `type_delay_ms`, `max_hold_ms`, `layout`, `drag_duration_ms`, `exec_output_bytes`
475
508
  and `exec_timeout_s`. A driver reads them as `self.settings`, for example its tap hold in `@press`.
476
509
 
@@ -509,13 +542,16 @@ class MyDriver(ComputerDriverEnvironment):
509
542
  `@pointer_position`: the slow path, at most 15 fps.
510
543
  - **The methods.** `start {request_id, scope?, fps?, max_edge?, cursor?, upload?}` returns
511
544
  `{recording_id, started_at, format}`; `end {recording_id, upload?, reason?}` closes the last segment
512
- and returns the manifest. A repeated `request_id`, or a repeated `end`, answers the same. A start
513
- past `max_concurrent` is refused `limit_exceeded`; `max_duration_s`, free disk under 512 MB or a stream
545
+ and returns the manifest; `end_all {reason?}` ends every recording not yet ended, each under the
546
+ grant it started with, and returns `{manifests}`, as the framework does before it resets or tears a
547
+ computer down. A repeated `request_id`, or a repeated `end`, answers the same. A start past
548
+ `max_concurrent` is refused `limit_exceeded`; `max_duration_s`, free disk under 512 MB or a stream
514
549
  failing for 60 s end a recording by itself.
515
550
  - **Segments.** A segment closes at `max_segment_s`, when the frame size changes or after a gap in the
516
551
  stream. Each uploads as it closes under `start`'s `write_namespace` grant, at
517
552
  `<recording_id>/<index>.mp4`, and is then deleted; `end`'s own grant uploads any that missed. With no
518
- grant they stay under `~/.agentenv-recordings/`.
553
+ grant they stay under `~/.agentenv-recordings/`. An ended recording's manifest uploads beside its
554
+ segments, at `<recording_id>/manifest.json`, so a reader that didn't end it finds it there.
519
555
  - **The manifest** (`RecordingManifest`). Its `segments` (`ManifestSegment`) carry each one's path,
520
556
  `started_at` and `ended_at` (its first and last frames' capture times), size, frame count, byte size,
521
557
  SHA-256 and whether it uploaded, and `ended_by` says why the recording ended: `end`, `reset`,
@@ -240,6 +240,32 @@ at the gateway.
240
240
 
241
241
  Where the binding lives depends on the app. On mcp's own FastMCP it wraps the tool manager's dispatch; on the standalone `fastmcp` package it joins the app's middleware chain as the outermost layer, so a handler's own middleware runs under it too. An app with neither seam mounts unbound, with a warning, unless a tool declares a `ToolContext` parameter or the handler defines `on_tool_call`: then the mount is refused rather than serving an empty caller.
242
242
 
243
+ ### Getting part of the data
244
+
245
+ A `@get_data` handler that takes a `paths` parameter returns only the data under the paths a caller
246
+ names (`client.get_data(base_url, paths=[...])`), and is called with None when the caller names
247
+ none. A handler without one answers `invalid_params` to a call that names paths, rather than
248
+ returning everything.
249
+
250
+ ### A skill for agents
251
+
252
+ An env can hand agents a skill: a SKILL.md an agent reads before it uses the env. Mark the method
253
+ that writes it with `@agent_skill`; it returns the skill's description and its markdown body,
254
+ sync or async:
255
+
256
+ ```python
257
+ @agent_skill
258
+ def guide(self):
259
+ return "How to place and track orders.", "# The shop\n\nPlace orders with `place_order`.\n"
260
+ ```
261
+
262
+ The card then declares `urn:agentenv:skill/v1` (`SKILL_EXTENSION_URI`), whose `get {name}`
263
+ answers `{name, description, skill_md}`, the shape `skill-config/v1` takes, with the frontmatter
264
+ written under the name the caller gives; a name that isn't lowercase letters, digits and single
265
+ hyphens, up to 64, is refused `invalid_argument`. agent-env's `add_skills` step installs it on
266
+ an agent through `env_ids`, named after the env's environment name; for an env behind a gateway,
267
+ whose card nests its servers' cards, it installs each server's skill under that server's name.
268
+
243
269
  ### Uploading an export through a grant
244
270
 
245
271
  An export too large to return from `data/get` can go straight to the caller's object store. A
@@ -396,6 +422,11 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
396
422
  (`stale_frame`) when the display geometry it was captured at changes.
397
423
  - **The input queue.** Input calls run one at a time in arrival order; one that waits past
398
424
  `queue_wait_s` is refused `input_busy`. Screenshots and waits never queue.
425
+ - **The computer skill.** `computer/v1` declares the seven tools, its params the driver's
426
+ features. The driver serves its skill through `skill/v1` (an `@agent_skill` handler on the base
427
+ class): a SKILL.md written from the computer as it booted (its platform, session user and home,
428
+ the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
429
+ class's `notes`, markdown for what its card can't say.
399
430
  - **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
400
431
  capture, plus the driver's own `ready_checks()`; tools refuse `device_unavailable` until they
401
432
  pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
@@ -405,8 +436,10 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
405
436
  - **The data plane.** `data/add` takes file parts that name their path, relative to the session
406
437
  user's home, under the `data-objects/v1` marker (`{"path": ...}`), as inline bytes, an https URL,
407
438
  or a `transfers.ReadObject` read grant under the marker's `read`, and writes them atomically.
408
- `data/get` returns the files under the settings' `data_paths`, inline or uploaded under a
409
- `write_namespace` grant.
439
+ `data/get` returns the files under the `paths` it's given, relative to the home, or under the
440
+ card's `default_data_paths` (`Desktop`, `Documents`, `Downloads`) when it's given none, inline or
441
+ uploaded under a `write_namespace` grant. An environment whose `@get_data` hook takes no `paths`
442
+ refuses them.
410
443
  - **The card** (the name from `ENVIRONMENT_NAME`, then `@computer_driver`'s), and per-role tool
411
444
  access (`tool_access` is on).
412
445
 
@@ -417,7 +450,7 @@ was sent (`applied: "no"`); `Timeout`, `DeliveryFailed` and `Internal` mean it m
417
450
  other exception from an input operation is reported `delivery_failed` with `applied: "unknown"`.
418
451
 
419
452
  **Settings** reach the driver as one JSON value in `AGENTENV_COMPUTER_SETTINGS`
420
- (`ComputerSettings`): `data_paths`, `default_space`, `default_norm_scale`, `queue_wait_s`,
453
+ (`ComputerSettings`): `default_space`, `default_norm_scale`, `queue_wait_s`,
421
454
  `tap_hold_ms`, `type_delay_ms`, `max_hold_ms`, `layout`, `drag_duration_ms`, `exec_output_bytes`
422
455
  and `exec_timeout_s`. A driver reads them as `self.settings`, for example its tap hold in `@press`.
423
456
 
@@ -456,13 +489,16 @@ class MyDriver(ComputerDriverEnvironment):
456
489
  `@pointer_position`: the slow path, at most 15 fps.
457
490
  - **The methods.** `start {request_id, scope?, fps?, max_edge?, cursor?, upload?}` returns
458
491
  `{recording_id, started_at, format}`; `end {recording_id, upload?, reason?}` closes the last segment
459
- and returns the manifest. A repeated `request_id`, or a repeated `end`, answers the same. A start
460
- past `max_concurrent` is refused `limit_exceeded`; `max_duration_s`, free disk under 512 MB or a stream
492
+ and returns the manifest; `end_all {reason?}` ends every recording not yet ended, each under the
493
+ grant it started with, and returns `{manifests}`, as the framework does before it resets or tears a
494
+ computer down. A repeated `request_id`, or a repeated `end`, answers the same. A start past
495
+ `max_concurrent` is refused `limit_exceeded`; `max_duration_s`, free disk under 512 MB or a stream
461
496
  failing for 60 s end a recording by itself.
462
497
  - **Segments.** A segment closes at `max_segment_s`, when the frame size changes or after a gap in the
463
498
  stream. Each uploads as it closes under `start`'s `write_namespace` grant, at
464
499
  `<recording_id>/<index>.mp4`, and is then deleted; `end`'s own grant uploads any that missed. With no
465
- grant they stay under `~/.agentenv-recordings/`.
500
+ grant they stay under `~/.agentenv-recordings/`. An ended recording's manifest uploads beside its
501
+ segments, at `<recording_id>/manifest.json`, so a reader that didn't end it finds it there.
466
502
  - **The manifest** (`RecordingManifest`). Its `segments` (`ManifestSegment`) carry each one's path,
467
503
  `started_at` and `ended_at` (its first and last frames' capture times), size, frame count, byte size,
468
504
  SHA-256 and whether it uploaded, and `ended_by` says why the recording ended: `end`, `reset`,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.391"
3
+ version = "0.1.393"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -12,6 +12,7 @@ from .agent_env_environment import (
12
12
  AgentEnvFastMCPApplication,
13
13
  AgentEnvStarletteApplication,
14
14
  add_data,
15
+ agent_skill,
15
16
  create_fastmcp_app,
16
17
  environment_card,
17
18
  extension,
@@ -58,6 +59,7 @@ from .types import (
58
59
  METHOD_RESET,
59
60
  PROTOCOL_VERSION,
60
61
  RPC_PATH,
62
+ SKILL_EXTENSION_URI,
61
63
  WELL_KNOWN_PATH,
62
64
  AddDataRequest,
63
65
  AddDataResponse,
@@ -91,6 +93,7 @@ __all__ = [
91
93
  "reset_data",
92
94
  "add_data",
93
95
  "get_data",
96
+ "agent_skill",
94
97
  "extension",
95
98
  "tool",
96
99
  "injecting",
@@ -117,6 +120,7 @@ __all__ = [
117
120
  "intake_fit_check",
118
121
  "INTAKE_EXTENSION_URI",
119
122
  "DATA_OBJECTS_EXTENSION_URI",
123
+ "SKILL_EXTENSION_URI",
120
124
  "uploaded_file_part",
121
125
  "uploaded_object_path",
122
126
  "MCP_PATH",
@@ -9,6 +9,7 @@ import inspect
9
9
  import json
10
10
  import logging
11
11
  import os
12
+ import re
12
13
  import sys
13
14
  import types
14
15
  import uuid
@@ -26,7 +27,7 @@ from .tool_access import DISABLE as TOOL_DISABLE, ENABLE as TOOL_ENABLE, PATHS a
26
27
  from .tool_access import extensions as tool_access_extensions
27
28
  from .tool_context import EMPTY, ToolContext, bound, context_from_http, context_from_mcp
28
29
  from .transfers import WriteNamespaceGrant
29
- from .types import DATA_OBJECTS_EXTENSION_URI, MCP_TRANSPORT, METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH, WELL_KNOWN_PATH, AddDataRequest, AddDataResponse, EnvironmentCapabilities, EnvironmentCard, EnvironmentExtension, EnvironmentInterface, EnvironmentTool, GetDataResponse, ResetDataResponse, error_body
30
+ from .types import DATA_OBJECTS_EXTENSION_URI, MCP_TRANSPORT, SKILL_EXTENSION_URI, METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH, WELL_KNOWN_PATH, AddDataRequest, AddDataResponse, EnvironmentCapabilities, EnvironmentCard, EnvironmentExtension, EnvironmentInterface, EnvironmentTool, GetDataResponse, ResetDataResponse, error_body
30
31
 
31
32
  logger = logging.getLogger(__name__)
32
33
 
@@ -53,6 +54,7 @@ RPC_ERROR_CODE_INVALID_REQUEST = -32600
53
54
  RPC_ERROR_CODE_METHOD_NOT_FOUND = -32601
54
55
  RPC_ERROR_CODE_INVALID_PARAMS = -32602
55
56
  RPC_ERROR_CODE_SERVER_ERROR = -32000
57
+ _SKILL_NAME = re.compile(r"[a-z0-9]+(-[a-z0-9]+)*")
56
58
 
57
59
 
58
60
  def reset_data(fn: Callable) -> Callable:
@@ -126,6 +128,23 @@ def extension(uri: str, *, op: str | None = None, description: str | None = None
126
128
  return deco
127
129
 
128
130
 
131
+ def agent_skill(fn: Callable) -> Callable:
132
+ """Mark the method that writes this env's skill for agents, returning its description and its markdown body; it
133
+ is served under ``SKILL_EXTENSION_URI`` as ``get {name}``, which writes the SKILL.md under the name asked for."""
134
+
135
+ async def get(self, name: str) -> dict:
136
+ if len(name) > 64 or not _SKILL_NAME.fullmatch(name):
137
+ raise AgentEnvError("invalid_argument",
138
+ f"a skill's name is lowercase letters, digits and single hyphens, up to 64, not {name!r}")
139
+ description, body = await fn(self) if inspect.iscoroutinefunction(fn) else fn(self)
140
+ frontmatter = f"---\nname: {name}\ndescription: {json.dumps(description)}\n---\n"
141
+ return {"name": name, "description": description, "skill_md": f"{frontmatter}\n{body}"}
142
+
143
+ get.__name__ = fn.__name__
144
+ return extension(SKILL_EXTENSION_URI, op="get", method="GET",
145
+ description="get returns a SKILL.md for agents under the name it's given.")(get)
146
+
147
+
129
148
  def _extension_slug(uri: str) -> str:
130
149
  """``urn:agentenv:computer-display/v1`` -> ``computer-display``; another namespace keeps its name as a path."""
131
150
  name = uri.removeprefix("urn:").removeprefix("agentenv:")
@@ -1162,6 +1181,14 @@ async def _invoke_add(fn: Callable, params: dict) -> dict:
1162
1181
 
1163
1182
  async def _invoke_get(fn: Callable, params: dict) -> dict:
1164
1183
  kwargs = {}
1184
+ takes_paths = "paths" in inspect.signature(fn).parameters
1185
+ if isinstance(params, dict) and "paths" in params:
1186
+ if not takes_paths:
1187
+ raise _InvalidParams("data/get on this environment takes no paths: it returns all of its data")
1188
+ if not isinstance(params["paths"], list) or not all(isinstance(path, str) for path in params["paths"]):
1189
+ raise _InvalidParams("paths must be a list of strings")
1190
+ if takes_paths:
1191
+ kwargs["paths"] = params.get("paths") if isinstance(params, dict) else None
1165
1192
  if _takes_write_namespace(fn):
1166
1193
  if not isinstance(params, dict):
1167
1194
  raise _InvalidParams("data/get takes its params by name")
@@ -57,11 +57,15 @@ async def add_data(base_url: str, parts: list[Part], timeout: int = 120, verify:
57
57
 
58
58
 
59
59
  async def get_data(
60
- base_url: str, timeout: int = 30, verify: bool = True, *, write_namespace: WriteNamespaceGrant | None = None
60
+ base_url: str, timeout: int = 30, verify: bool = True, *, write_namespace: WriteNamespaceGrant | None = None,
61
+ paths: list[str] | None = None,
61
62
  ) -> GetDataResponse:
62
63
  """`data/get`. A server advertising `DATA_OBJECTS_EXTENSION_URI` may be handed `write_namespace` to upload
63
- its export under; `uploaded_object_path` reads from the answer where it did."""
64
+ its export under; `uploaded_object_path` reads from the answer where it did. `paths` names what a computer
65
+ returns, relative to its user's home; an env that returns all of its data refuses them."""
64
66
  params = {} if write_namespace is None else {"write_namespace": write_namespace.model_dump(mode="json", exclude_none=True)}
67
+ if paths is not None:
68
+ params["paths"] = paths
65
69
  return GetDataResponse.model_validate(await _rpc(base_url, METHOD_GET, params, timeout, verify))
66
70
 
67
71
 
@@ -77,7 +77,8 @@ def declare(folder: Path, file: str, name: str) -> dict:
77
77
  return {"name": cards[0][1].name, "supports": list(cls.supports), "platforms": [
78
78
  {"platform": platform.as_dict(), "provides": [e.uri for e in card.capabilities.extensions],
79
79
  "tools": [t.name for t in card.capabilities.tools],
80
- "features": next(e.params for e in card.capabilities.extensions if e.uri == COMPUTER)}
80
+ "features": next({k: v for k, v in e.params.items() if k not in ("endpoint", "methods")}
81
+ for e in card.capabilities.extensions if e.uri == COMPUTER)}
81
82
  for platform, card in cards]}
82
83
 
83
84
 
@@ -1,10 +1,11 @@
1
1
  """What a driver's card says: the extensions, the tools, extensions and data-plane methods its operations support,
2
- the extensions' params, the extensions it declares without handlers, and the descriptions that name the default
3
- coordinate space."""
2
+ the extensions' params (computer/v1's are the driver's features), the extensions it declares without handlers, and the
3
+ descriptions that name the default coordinate space."""
4
4
  from __future__ import annotations
5
5
 
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from .dataplane import DEFAULT_DATA_PATHS
8
9
  from .recording import recording_params
9
10
  from ..types import METHOD_ADD, EnvironmentExtension, EnvironmentTool, IntakeDeclaration, IntakeFormat, intake_extension
10
11
 
@@ -79,18 +80,23 @@ def extension_params(driver: ComputerDriverEnvironment, uri: str) -> dict:
79
80
  return {}
80
81
 
81
82
 
82
- def declared_extensions(driver: ComputerDriverEnvironment) -> list[EnvironmentExtension]:
83
- features = {
83
+ def computer_features(driver: ComputerDriverEnvironment) -> dict:
84
+ """computer/v1's params: what the seven tools take, how input is delivered, the platform and the default data
85
+ paths."""
86
+ return {
84
87
  "buttons": list(driver.buttons), "max_click_count": driver.max_click_count,
85
88
  "keys": driver.keys if driver.keys == "all" else list(driver.keys), "max_text_length": driver.max_text_length,
86
89
  "max_wait_ms": driver.max_wait_ms, "max_drag_points": driver.max_drag_points,
87
90
  "max_duration_ms": driver.max_duration_ms, "max_key_repeat": driver.max_key_repeat,
88
- "max_scroll_amount": driver.max_scroll_amount,
91
+ "max_scroll_amount": driver.max_scroll_amount, "delivery": "foreground", "platform": driver.platform.as_dict(),
92
+ "supports": list(driver.supports), "default_data_paths": list(DEFAULT_DATA_PATHS),
89
93
  }
94
+
95
+
96
+ def declared_extensions(driver: ComputerDriverEnvironment) -> list[EnvironmentExtension]:
90
97
  declared = [EnvironmentExtension(
91
98
  uri=COMPUTER, description="The seven computer_* tools: screenshot, click, drag, scroll, type, key, wait.",
92
- params={**features, "delivery": "foreground", "platform": driver.platform.as_dict(),
93
- "supports": list(driver.supports)})]
99
+ params=computer_features(driver))]
94
100
  if driver.has("pointer"):
95
101
  declared.append(EnvironmentExtension(
96
102
  uri=COMPUTER_POINTER, description="The computer_pointer_* tools, and click and scroll at the pointer.",
@@ -99,7 +105,7 @@ def declared_extensions(driver: ComputerDriverEnvironment) -> list[EnvironmentEx
99
105
  if driver.has("write_files"):
100
106
  intake.add = [IntakeFormat(part="file", format="file-tree", load="additive")]
101
107
  declared.append(intake_extension(intake, "data/add writes files under the session user's home; data/get "
102
- "returns the env's data paths."))
108
+ "returns those under the paths it's given, or the default data paths."))
103
109
  return declared
104
110
 
105
111
 
@@ -21,7 +21,7 @@ from pydantic import ValidationError
21
21
  from ... import client as protocol
22
22
  from ...client import ExtensionCallError
23
23
  from ...tool_access import DISABLE_TOOL_EXTENSION_URI, ENABLE_TOOL_EXTENSION_URI
24
- from ...types import DATA_OBJECTS_EXTENSION_URI, INTAKE_EXTENSION_URI
24
+ from ...types import DATA_OBJECTS_EXTENSION_URI, INTAKE_EXTENSION_URI, SKILL_EXTENSION_URI
25
25
  from ..card import (
26
26
  COMPUTER,
27
27
  COMPUTER_DISPLAY,
@@ -88,7 +88,7 @@ class Probe:
88
88
  for check, section in (("P0", self._ready), ("P1", self._card), ("P2", self._frame), ("P5", self._refusals),
89
89
  ("X", self._display), ("X", self._spaces), ("X", self._keyboard_settings),
90
90
  ("X", self._exec), ("X", self._data_get), ("X", self._tool_access),
91
- ("X", self._pointer)):
91
+ ("X", self._pointer), ("X", self._skill)):
92
92
  await self._guarded(check, section)
93
93
  self._grants()
94
94
 
@@ -388,6 +388,18 @@ class Probe:
388
388
  await self.check("X", "output past max_output_bytes is cut there", capped, COMPUTER_EXEC)
389
389
  await self.check("X", "a program that isn't there is refused with a code", missing, COMPUTER_EXEC)
390
390
 
391
+ async def _skill(self) -> None:
392
+ if not self.driver.declares(SKILL_EXTENSION_URI):
393
+ return
394
+
395
+ async def written() -> str | None:
396
+ got = await self.driver.invoke(SKILL_EXTENSION_URI, "get", {"name": "probe-skill"})
397
+ if got.get("name") != "probe-skill" or not str(got.get("skill_md")).startswith("---\nname: probe-skill\n"):
398
+ return f"it answered {got}"
399
+ return None if got.get("description") else "its description is empty"
400
+
401
+ await self.check("X", "the skill is written under the name it's asked for", written, SKILL_EXTENSION_URI)
402
+
391
403
  async def _data_get(self) -> None:
392
404
  if not self.driver.declares(INTAKE_EXTENSION_URI):
393
405
  return
@@ -1,5 +1,6 @@
1
1
  """The data plane on a computer: ``data/add`` writes files under the session user's home, ``data/get`` returns the
2
- env's ``data_paths``. Each file part names its path, relative to the home, under the ``data-objects/v1`` marker."""
2
+ files under the paths its caller names, or under ``DEFAULT_DATA_PATHS`` when it names none. Each file part names its
3
+ path, relative to the home, under the ``data-objects/v1`` marker."""
3
4
  from __future__ import annotations
4
5
 
5
6
  import base64
@@ -10,10 +11,10 @@ from pathlib import Path
10
11
  from typing import TYPE_CHECKING, Optional
11
12
 
12
13
  import httpx
13
- from pydantic import ValidationError
14
+ from pydantic import TypeAdapter, ValidationError
14
15
 
15
16
  from ..agent_env_environment import add_data, get_data
16
- from ..transfers import NamespaceUploader, ReadObject, WriteNamespaceGrant, download
17
+ from ..transfers import NamespaceUploader, ReadObject, RelativePath, WriteNamespaceGrant, download
17
18
  from ..types import DATA_OBJECTS_EXTENSION_URI, FilePart, FileWithBytes, FileWithUri, uploaded_file_part, uploaded_object_path
18
19
 
19
20
  if TYPE_CHECKING:
@@ -22,6 +23,9 @@ if TYPE_CHECKING:
22
23
 
23
24
  #: The most an https URL part may fetch; a read grant carries its own limit.
24
25
  MAX_URL_BYTES = 1 << 30
26
+ #: What ``data/get`` returns when its caller names no paths: the folders a desktop keeps its user's files in.
27
+ DEFAULT_DATA_PATHS = ("Desktop", "Documents", "Downloads")
28
+ _PATHS = TypeAdapter(list[RelativePath])
25
29
 
26
30
 
27
31
  class DataPlane:
@@ -47,8 +51,13 @@ class DataPlane:
47
51
  await self._computer.call("write_files", files)
48
52
 
49
53
  @get_data
50
- async def _computer_data_get(self, write_namespace: Optional[WriteNamespaceGrant] = None) -> list:
51
- files = _data_files(Path.home(), self.settings.data_paths)
54
+ async def _computer_data_get(self, write_namespace: Optional[WriteNamespaceGrant] = None,
55
+ paths: Optional[list[str]] = None) -> list:
56
+ try:
57
+ named = _PATHS.validate_python(paths) if paths is not None else list(DEFAULT_DATA_PATHS)
58
+ except ValidationError:
59
+ raise ValueError(f"data/get on a computer takes paths relative to the home, not {paths!r}") from None
60
+ files = _data_files(Path.home(), named)
52
61
  if write_namespace is not None:
53
62
  uploader = NamespaceUploader(write_namespace)
54
63
  parts = []
@@ -98,11 +107,11 @@ async def _get_url(file: FileWithUri) -> bytes:
98
107
  return bytes(data)
99
108
 
100
109
 
101
- def _data_files(home: Path, data_paths: list[str]) -> list[tuple[str, Path]]:
102
- """Every file under the data paths, by its path relative to the home; a path not there yet is skipped."""
110
+ def _data_files(home: Path, paths: list[str]) -> list[tuple[str, Path]]:
111
+ """Every file under ``paths``, by its path relative to the home; a path not there yet is skipped."""
103
112
  root = home.resolve()
104
113
  found: dict[str, Path] = {}
105
- for relative in data_paths:
114
+ for relative in paths:
106
115
  target = home / relative
107
116
  if not target.exists() or not target.resolve().is_relative_to(root):
108
117
  continue
@@ -46,6 +46,8 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
46
46
  #: computer-recording/v1, from ``@record`` if the driver has one, else by polling ``@capture``; ``None`` turns
47
47
  #: it off.
48
48
  recording: ClassVar[Recording | None] = Recording()
49
+ #: What an agent should know about this driver's computer beyond what its card says, as markdown: the end of its skill.
50
+ notes: ClassVar[str] = ""
49
51
  tool_access = True
50
52
  #: What the base class keeps for the driver, built by ``__init__``.
51
53
  _computer: ComputerRuntime | None = None
@@ -1,10 +1,10 @@
1
- """The framework extensions agentenv calls, never agents: readiness, display, coordinate spaces, keyboard settings,
2
- commands in the session and recordings."""
1
+ """The framework extensions agentenv calls, never agents: the computer skill, readiness, display, coordinate spaces,
2
+ keyboard settings, commands in the session and recordings."""
3
3
  from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING, Callable, Optional
6
6
 
7
- from ..agent_env_environment import extension
7
+ from ..agent_env_environment import agent_skill, extension
8
8
  from . import card
9
9
  from .card import (
10
10
  COMPUTER_DISPLAY,
@@ -18,6 +18,7 @@ from .card import (
18
18
  )
19
19
  from .errors import Internal, InvalidArgument, Unsupported
20
20
  from .operations import Display, ExecResult
21
+ from .skill import skill
21
22
  from .spaces import check_space
22
23
 
23
24
  if TYPE_CHECKING:
@@ -36,6 +37,10 @@ class Extensions:
36
37
  layouts: tuple[str, ...]
37
38
  mounted_card: Callable[[], EnvironmentCard]
38
39
 
40
+ @agent_skill
41
+ async def _computer_skill(self) -> tuple[str, str]:
42
+ return await skill(self)
43
+
39
44
  @extension(COMPUTER_READY, op="status", method="GET",
40
45
  description="Whether the computer is usable now, by real checks; never sends input.")
41
46
  async def _computer_ready_status(self) -> dict:
@@ -114,7 +119,8 @@ class Extensions:
114
119
 
115
120
  @extension(COMPUTER_RECORDING, op="start",
116
121
  description="Record the display: start returns a recording whose segments upload through its grant as "
117
- "they close, and end seals it and returns its manifest; repeats answer the same.")
122
+ "they close; end seals it and returns its manifest, uploaded beside them; end_all ends "
123
+ "every one not yet ended; repeats answer the same.")
118
124
  async def _computer_recording_start(self, request_id: str, scope: str = "display", window: Optional[str] = None,
119
125
  fps: Optional[int] = None, max_edge: Optional[int] = None, cursor: bool = True,
120
126
  upload: Optional[dict] = None) -> dict:
@@ -125,6 +131,10 @@ class Extensions:
125
131
  reason: str = "end") -> dict:
126
132
  return await self._computer.recordings.end(recording_id, upload, reason)
127
133
 
134
+ @extension(COMPUTER_RECORDING, op="end_all")
135
+ async def _computer_recording_end_all(self, reason: str = "end") -> dict:
136
+ return {"manifests": await self._computer.recordings.end_all(reason)}
137
+
128
138
 
129
139
  async def _run(computer: ComputerRuntime, cap: int, argv: list, cwd: Optional[str], env: Optional[dict],
130
140
  timeout: float) -> dict:
@@ -7,6 +7,7 @@ import asyncio
7
7
  import contextlib
8
8
  import dataclasses
9
9
  import hashlib
10
+ import json
10
11
  import shutil
11
12
  import threading
12
13
  import time
@@ -38,6 +39,8 @@ MP4 = "video/mp4; codecs=avc1"
38
39
  RECORDINGS_DIR = ".agentenv-recordings"
39
40
  SCOPES = ("display", "window")
40
41
  REASONS = ("end", "reset", "teardown")
42
+ #: What an ended recording's manifest is uploaded as, under its id beside its segments.
43
+ MANIFEST = "manifest.json"
41
44
  DEFAULT_FPS = 10
42
45
  #: The fallback polls ``@capture``, a full read of the screen each pass, so it is capped lower than streams.
43
46
  MAX_POLLED_FPS = 15
@@ -179,6 +182,10 @@ class Recordings:
179
182
  self.ended.popitem(last=False)
180
183
  return manifest
181
184
 
185
+ async def end_all(self, reason: str) -> list[dict]:
186
+ """End every recording not yet ended, each uploading under the grant it was started with; their manifests."""
187
+ return [await self.end(recording_id, None, reason) for recording_id in list(self.running)]
188
+
182
189
  def _hints(self, features: Recording, scope: str, window: Optional[str], fps: Optional[int],
183
190
  max_edge: Optional[int], cursor: bool, upload: Optional[dict]) -> tuple[RecordHints, Any]:
184
191
  if scope not in SCOPES:
@@ -287,11 +294,15 @@ class _Recording:
287
294
  with contextlib.suppress(OSError):
288
295
  self.hints.directory.rmdir()
289
296
  entries = [s.entry for s in self.segments]
290
- return RecordingManifest(
297
+ manifest = RecordingManifest(
291
298
  recording_id=self.id, scope=self.hints.scope, format=MP4, fps=self.hints.fps, cursor=self.cursor,
292
299
  source="stream" if self.streams else "capture", started_at=entries[0].started_at if entries else None,
293
300
  ended_at=entries[-1].ended_at if entries else None, ended_by=self.ended_by, segments=entries,
294
301
  error=self.error if self.ended_by == "error" else None).wire()
302
+ if uploader is not None: # beside the segments, so a reader who didn't end the recording finds it
303
+ with contextlib.suppress(Exception):
304
+ await uploader.upload(f"{self.id}/{MANIFEST}", json.dumps(manifest).encode())
305
+ return manifest
295
306
 
296
307
  async def _add_frame(self, item: RecordedFrame) -> None:
297
308
  frame = await self._in_thread(_video_frame, item.image, self.hints.max_edge)
@@ -7,7 +7,6 @@ from typing import Literal
7
7
 
8
8
  from pydantic import BaseModel, ConfigDict, Field, ValidationError
9
9
 
10
- from ..transfers import RelativePath
11
10
 
12
11
  SETTINGS_ENV = "AGENTENV_COMPUTER_SETTINGS"
13
12
 
@@ -15,12 +14,10 @@ Space = Literal["frame", "norm"]
15
14
 
16
15
 
17
16
  class ComputerSettings(BaseModel):
18
- """``data_paths`` are relative to the session user's home and are what ``data/get`` returns; the rest are the
19
- defaults the card reports and the framework extensions change."""
17
+ """The defaults the card reports and the framework extensions change."""
20
18
 
21
19
  model_config = ConfigDict(extra="forbid", validate_assignment=True)
22
20
 
23
- data_paths: list[RelativePath] = Field(default_factory=list)
24
21
  default_space: Space = "frame"
25
22
  default_norm_scale: int = Field(default=1000, ge=1, le=1_000_000)
26
23
  queue_wait_s: float = Field(default=30.0, gt=0, le=3600)