agentenv-framework-protocol 0.1.391__tar.gz → 0.1.392__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.391 → agentenv_framework_protocol-0.1.392}/PKG-INFO +43 -7
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/README.md +42 -6
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/__init__.py +4 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/agent_env_environment.py +28 -1
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/client.py +6 -2
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/_declare.py +2 -1
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/card.py +14 -8
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/conformance/checks.py +14 -2
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/dataplane.py +17 -8
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/driver.py +2 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/extensions.py +14 -4
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/recording.py +12 -1
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/settings.py +1 -4
- agentenv_framework_protocol-0.1.392/src/agentenv_protocol/computer/skill.py +68 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/types.py +5 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_agent_env_environment.py +40 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_conformance.py +1 -1
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_dataplane.py +31 -9
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_driver.py +23 -3
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_recording.py +27 -4
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_data_objects.py +3 -1
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/linux.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.391 → agentenv_framework_protocol-0.1.392}/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.
|
|
3
|
+
Version: 0.1.392
|
|
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
|
|
462
|
-
`
|
|
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`): `
|
|
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
|
|
513
|
-
|
|
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
|
|
409
|
-
`
|
|
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`): `
|
|
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
|
|
460
|
-
|
|
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`,
|
|
@@ -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(
|
|
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
|
|
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
|
|
83
|
-
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
|
51
|
-
|
|
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,
|
|
102
|
-
"""Every file under
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
"""
|
|
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)
|