agentenv-framework-protocol 0.1.398__tar.gz → 0.1.400__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.398 → agentenv_framework_protocol-0.1.400}/.gitignore +1 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/PKG-INFO +35 -12
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/README.md +31 -11
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/pyproject.toml +7 -1
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/_declare.py +6 -0
- agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/_load.py +22 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/app.py +66 -4
- agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/app_runtime.py +155 -0
- agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/apps.py +63 -0
- agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/launch.py +25 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/runtime.py +3 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/serve.py +2 -12
- agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/serve_app.py +64 -0
- agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/session.py +74 -0
- agentenv_framework_protocol-0.1.400/tests/test_computer_app_server.py +341 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_declare.py +21 -2
- agentenv_framework_protocol-0.1.400/tests/test_computer_serve.py +180 -0
- agentenv_framework_protocol-0.1.398/tests/test_computer_serve.py +0 -75
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/driver.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/linux.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.398 → agentenv_framework_protocol-0.1.400}/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.400
|
|
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
|
|
@@ -34,6 +34,9 @@ Requires-Dist: mcp<2,>=1.25; extra == 'agent'
|
|
|
34
34
|
Requires-Dist: python-multipart>=0.0.13; extra == 'agent'
|
|
35
35
|
Requires-Dist: regex>=2023.12.25; extra == 'agent'
|
|
36
36
|
Requires-Dist: uvicorn>=0.30; extra == 'agent'
|
|
37
|
+
Provides-Extra: app
|
|
38
|
+
Requires-Dist: mcp<2,>=1.25; extra == 'app'
|
|
39
|
+
Requires-Dist: uvicorn>=0.30; extra == 'app'
|
|
37
40
|
Provides-Extra: computer
|
|
38
41
|
Requires-Dist: av>=17; extra == 'computer'
|
|
39
42
|
Requires-Dist: mcp<2,>=1.25; extra == 'computer'
|
|
@@ -481,8 +484,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
|
|
|
481
484
|
the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
|
|
482
485
|
class's `notes`, markdown for what its card can't say.
|
|
483
486
|
- **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
|
|
484
|
-
capture,
|
|
485
|
-
pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
|
|
487
|
+
capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
|
|
488
|
+
own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
|
|
486
489
|
descriptions), `computer-keyboard/v1`, `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
|
|
487
490
|
request_id}`: output capped per stream, a repeated `request_id` answered with the first result) and,
|
|
488
491
|
with `recording`, `computer-recording/v1` (below).
|
|
@@ -594,9 +597,10 @@ and gives it back.
|
|
|
594
597
|
|
|
595
598
|
A computer app is software an agent uses on a computer, beside the driver: a desktop app, a service
|
|
596
599
|
or both. Write one as a `ComputerAppEnvironment` subclass from `agentenv_protocol.computer`, an
|
|
597
|
-
`AgentEnvEnvironment` whose class also says what it is to a computer.
|
|
598
|
-
package: `agentenv_protocol.computer` imports what a driver needs
|
|
599
|
-
when a driver's names are used.
|
|
600
|
+
`AgentEnvEnvironment` whose class also says what it is to a computer. Writing one and reading what
|
|
601
|
+
it declares need only the plain package: `agentenv_protocol.computer` imports what a driver needs
|
|
602
|
+
(mcp, uvicorn, Pillow, PyAV) only when a driver's names are used. On a computer it runs as its own
|
|
603
|
+
server, which needs the `app` extra (below).
|
|
600
604
|
|
|
601
605
|
```python
|
|
602
606
|
from pydantic import BaseModel, SecretStr
|
|
@@ -617,18 +621,37 @@ class MailApp(ComputerAppEnvironment):
|
|
|
617
621
|
`permissions` it needs of the computer. Its env.toml names the rest: the app itself
|
|
618
622
|
(`environment_name`) and the `computer` and `driver` it deploys alone on.
|
|
619
623
|
- **`linux = Linux(...)`** is how a Linux computer installs it: `files`, a folder of the app's
|
|
620
|
-
copied under its prefix; `python`, a requirements file there
|
|
621
|
-
`services`, programs the computer's init runs
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
624
|
+
copied under its prefix, which holds the class's file; `python`, a requirements file there,
|
|
625
|
+
installed into the venv every app gets beside them; `services`, programs the computer's init runs
|
|
626
|
+
from the prefix as the session user.
|
|
627
|
+
- **`launch = Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
|
|
628
|
+
the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
|
|
629
|
+
launch: a window matching `launch.window` by its class, instance or title, a port answering on
|
|
630
|
+
loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
|
|
631
|
+
sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
|
|
632
|
+
app.
|
|
626
633
|
- **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both;
|
|
627
634
|
its data hooks are the usual `@add_data`, `@get_data` and `@reset_data`.
|
|
628
635
|
- **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
|
|
629
636
|
in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
|
|
630
637
|
its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
|
|
631
638
|
|
|
639
|
+
On a computer each app is its own server: `python -m agentenv_protocol.computer.serve_app
|
|
640
|
+
<file>:<Class>`, a program the computer's init runs from the app's folder in the app's venv, which
|
|
641
|
+
has the `app` extra (`agentenv-framework-protocol[app]`: mcp and uvicorn). It serves the class's
|
|
642
|
+
card and tools on the Unix socket `.agentenv/server.sock` in that folder, with `computer-app/v1`:
|
|
643
|
+
the app's `launch` as its params, and the methods `launch` (POST), which starts the app in the
|
|
644
|
+
desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `launched`, `waited_s`
|
|
645
|
+
and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
|
|
646
|
+
that can't start, or whose start in the session fails (adding the launcher, say), writes why to
|
|
647
|
+
`.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
|
|
648
|
+
the session is up the server adds the app's launcher to the session user's menu, which runs `python
|
|
649
|
+
-m agentenv_protocol.computer.launch <folder>` to ask the server, so the server starts every copy of
|
|
650
|
+
the app, and launches a `boot` app, once per boot. The driver finds each app by the socket in its
|
|
651
|
+
folder: the computer is ready once every app's server answers and each `boot` app, and each app with
|
|
652
|
+
no launch, says it's ready; a `demand` app counts once something launches it. What the server does
|
|
653
|
+
in the session (`agentenv_protocol.computer.session`) is Linux's.
|
|
654
|
+
|
|
632
655
|
`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
|
|
633
656
|
env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
634
657
|
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
|
|
@@ -428,8 +428,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
|
|
|
428
428
|
the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
|
|
429
429
|
class's `notes`, markdown for what its card can't say.
|
|
430
430
|
- **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
|
|
431
|
-
capture,
|
|
432
|
-
pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
|
|
431
|
+
capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
|
|
432
|
+
own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
|
|
433
433
|
descriptions), `computer-keyboard/v1`, `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
|
|
434
434
|
request_id}`: output capped per stream, a repeated `request_id` answered with the first result) and,
|
|
435
435
|
with `recording`, `computer-recording/v1` (below).
|
|
@@ -541,9 +541,10 @@ and gives it back.
|
|
|
541
541
|
|
|
542
542
|
A computer app is software an agent uses on a computer, beside the driver: a desktop app, a service
|
|
543
543
|
or both. Write one as a `ComputerAppEnvironment` subclass from `agentenv_protocol.computer`, an
|
|
544
|
-
`AgentEnvEnvironment` whose class also says what it is to a computer.
|
|
545
|
-
package: `agentenv_protocol.computer` imports what a driver needs
|
|
546
|
-
when a driver's names are used.
|
|
544
|
+
`AgentEnvEnvironment` whose class also says what it is to a computer. Writing one and reading what
|
|
545
|
+
it declares need only the plain package: `agentenv_protocol.computer` imports what a driver needs
|
|
546
|
+
(mcp, uvicorn, Pillow, PyAV) only when a driver's names are used. On a computer it runs as its own
|
|
547
|
+
server, which needs the `app` extra (below).
|
|
547
548
|
|
|
548
549
|
```python
|
|
549
550
|
from pydantic import BaseModel, SecretStr
|
|
@@ -564,18 +565,37 @@ class MailApp(ComputerAppEnvironment):
|
|
|
564
565
|
`permissions` it needs of the computer. Its env.toml names the rest: the app itself
|
|
565
566
|
(`environment_name`) and the `computer` and `driver` it deploys alone on.
|
|
566
567
|
- **`linux = Linux(...)`** is how a Linux computer installs it: `files`, a folder of the app's
|
|
567
|
-
copied under its prefix; `python`, a requirements file there
|
|
568
|
-
`services`, programs the computer's init runs
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
568
|
+
copied under its prefix, which holds the class's file; `python`, a requirements file there,
|
|
569
|
+
installed into the venv every app gets beside them; `services`, programs the computer's init runs
|
|
570
|
+
from the prefix as the session user.
|
|
571
|
+
- **`launch = Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
|
|
572
|
+
the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
|
|
573
|
+
launch: a window matching `launch.window` by its class, instance or title, a port answering on
|
|
574
|
+
loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
|
|
575
|
+
sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
|
|
576
|
+
app.
|
|
573
577
|
- **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both;
|
|
574
578
|
its data hooks are the usual `@add_data`, `@get_data` and `@reset_data`.
|
|
575
579
|
- **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
|
|
576
580
|
in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
|
|
577
581
|
its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
|
|
578
582
|
|
|
583
|
+
On a computer each app is its own server: `python -m agentenv_protocol.computer.serve_app
|
|
584
|
+
<file>:<Class>`, a program the computer's init runs from the app's folder in the app's venv, which
|
|
585
|
+
has the `app` extra (`agentenv-framework-protocol[app]`: mcp and uvicorn). It serves the class's
|
|
586
|
+
card and tools on the Unix socket `.agentenv/server.sock` in that folder, with `computer-app/v1`:
|
|
587
|
+
the app's `launch` as its params, and the methods `launch` (POST), which starts the app in the
|
|
588
|
+
desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `launched`, `waited_s`
|
|
589
|
+
and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
|
|
590
|
+
that can't start, or whose start in the session fails (adding the launcher, say), writes why to
|
|
591
|
+
`.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
|
|
592
|
+
the session is up the server adds the app's launcher to the session user's menu, which runs `python
|
|
593
|
+
-m agentenv_protocol.computer.launch <folder>` to ask the server, so the server starts every copy of
|
|
594
|
+
the app, and launches a `boot` app, once per boot. The driver finds each app by the socket in its
|
|
595
|
+
folder: the computer is ready once every app's server answers and each `boot` app, and each app with
|
|
596
|
+
no launch, says it's ready; a `demand` app counts once something launches it. What the server does
|
|
597
|
+
in the session (`agentenv_protocol.computer.session`) is Linux's.
|
|
598
|
+
|
|
579
599
|
`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
|
|
580
600
|
env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
581
601
|
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentenv-framework-protocol"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.400"
|
|
4
4
|
description = "Open data-plane protocol and server SDK for agent environments."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -35,6 +35,12 @@ agent = [
|
|
|
35
35
|
"regex>=2023.12.25",
|
|
36
36
|
"uvicorn>=0.30",
|
|
37
37
|
]
|
|
38
|
+
# A computer app's own server: ComputerAppEnvironment serves its card and computer-app/v1 over mcp's FastMCP, on a Unix
|
|
39
|
+
# socket through uvicorn.
|
|
40
|
+
app = [
|
|
41
|
+
"mcp>=1.25,<2",
|
|
42
|
+
"uvicorn>=0.30",
|
|
43
|
+
]
|
|
38
44
|
# A computer driver: ComputerDriverEnvironment serves its tools over mcp's FastMCP, crops and encodes frames, and
|
|
39
45
|
# records the display as H.264 with PyAV (computer-recording/v1).
|
|
40
46
|
computer = [
|
|
@@ -14,6 +14,8 @@ import sys
|
|
|
14
14
|
import types
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
from pydantic import ValidationError
|
|
18
|
+
|
|
17
19
|
from .app import AppDeclaration, ComputerAppEnvironment
|
|
18
20
|
from .card import COMPUTER
|
|
19
21
|
from .driver import ComputerDriverEnvironment, DriverDeclaration, DriverPlatform
|
|
@@ -95,6 +97,10 @@ def main(argv: list[str]) -> int:
|
|
|
95
97
|
try:
|
|
96
98
|
with contextlib.redirect_stdout(sys.stderr):
|
|
97
99
|
declared = declare(Path(folder), file, name)
|
|
100
|
+
except ValidationError as e:
|
|
101
|
+
print("ValueError: " + "; ".join(error["msg"].removeprefix("Value error, ") for error in e.errors()),
|
|
102
|
+
file=sys.stderr)
|
|
103
|
+
return 1
|
|
98
104
|
except Exception as e:
|
|
99
105
|
print(f"{type(e).__name__}: {e}", file=sys.stderr)
|
|
100
106
|
return 1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Import a class from a file of a driver's or an app's folder, as its computer's program starts it."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import importlib.util
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_class(file: Path, name: str, base: type) -> type:
|
|
10
|
+
"""The class ``name`` in ``file``, imported as a module of its own with the file's folder importable; exits saying
|
|
11
|
+
why when it isn't a subclass of ``base``."""
|
|
12
|
+
if not file.is_file():
|
|
13
|
+
raise SystemExit(f"{file} isn't a file")
|
|
14
|
+
sys.path.insert(0, str(file.resolve().parent))
|
|
15
|
+
spec = importlib.util.spec_from_file_location(file.stem, file)
|
|
16
|
+
module = importlib.util.module_from_spec(spec)
|
|
17
|
+
sys.modules[file.stem] = module
|
|
18
|
+
spec.loader.exec_module(module)
|
|
19
|
+
cls = getattr(module, name, None)
|
|
20
|
+
if not (isinstance(cls, type) and issubclass(cls, base)):
|
|
21
|
+
raise SystemExit(f"{file} has no {base.__name__} subclass named {name}")
|
|
22
|
+
return cls
|
|
@@ -2,24 +2,37 @@
|
|
|
2
2
|
computer: the platforms it supports and the permissions it needs (``@computer_app``), how each OS installs it
|
|
3
3
|
(``linux = Linux(...)``), how it launches (``launch``), its install steps and ready checks, its skill, and its typed
|
|
4
4
|
``Settings``. A ``computer_app`` env's put reads that declaration from the class; its data hooks are the base class's.
|
|
5
|
-
Its name is its env's ``environment_name``.
|
|
5
|
+
Its name is its env's ``environment_name``. On the computer the class runs as the app's own server (``serve_app``),
|
|
6
|
+
which launches the app, adds its launcher and says whether it's ready (``computer-app/v1``)."""
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
import inspect
|
|
9
10
|
import json
|
|
10
11
|
import os
|
|
12
|
+
from pathlib import Path
|
|
11
13
|
from typing import Any, Callable, ClassVar, Literal, Optional
|
|
12
14
|
|
|
13
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
14
16
|
|
|
15
|
-
from ..agent_env_environment import _EXT_ATTR, _OP_ATTR, AgentEnvEnvironment
|
|
17
|
+
from ..agent_env_environment import _EXT_ATTR, _OP_ATTR, AgentEnvEnvironment, extension
|
|
16
18
|
from ..types import METHOD_ADD, METHOD_GET, METHOD_RESET, SKILL_EXTENSION_URI
|
|
19
|
+
from .app_runtime import AppRuntime
|
|
20
|
+
from .errors import InvalidArgument
|
|
17
21
|
|
|
18
22
|
#: The environment variable a computer delivers its apps' settings in: JSON of each app's values by its name.
|
|
19
23
|
APP_SETTINGS_ENV = "AGENTENV_APP_SETTINGS"
|
|
20
24
|
_INSTALL_STEP_ATTR = "_aee_install_step"
|
|
21
25
|
_READY_CHECK_ATTR = "_aee_ready_check"
|
|
22
26
|
_DATA_METHODS = {"reset_data": METHOD_RESET, "add_data": METHOD_ADD, "get_data": METHOD_GET}
|
|
27
|
+
#: What a launch's window can be matched by, as the session lists each window.
|
|
28
|
+
WINDOW_KEYS = frozenset({"class", "instance", "title"})
|
|
29
|
+
#: The framework extension an app's own server serves: its launch as params, and its ``launch`` and ``ready`` methods.
|
|
30
|
+
COMPUTER_APP = "urn:agentenv:computer-app/v1"
|
|
31
|
+
#: Where an app's own server listens, in the app's folder, and where it says why it last failed to start.
|
|
32
|
+
SOCKET = Path(".agentenv/server.sock")
|
|
33
|
+
FAILURE = Path(".agentenv/server.failure")
|
|
34
|
+
#: How long an app may take to be ready when its launch doesn't say.
|
|
35
|
+
DEFAULT_TIMEOUT_S = 60.0
|
|
23
36
|
|
|
24
37
|
|
|
25
38
|
class Service(BaseModel):
|
|
@@ -51,7 +64,13 @@ class Ready(BaseModel):
|
|
|
51
64
|
window: bool = False
|
|
52
65
|
port: Optional[int] = None
|
|
53
66
|
command: Optional[list[str]] = None
|
|
54
|
-
timeout_s: float =
|
|
67
|
+
timeout_s: float = DEFAULT_TIMEOUT_S
|
|
68
|
+
|
|
69
|
+
@model_validator(mode="after")
|
|
70
|
+
def _waits_on_something(self) -> Ready:
|
|
71
|
+
if not self.window and self.port is None and not self.command:
|
|
72
|
+
raise ValueError("Ready waits on nothing: set window, port or command")
|
|
73
|
+
return self
|
|
55
74
|
|
|
56
75
|
|
|
57
76
|
class Launch(BaseModel):
|
|
@@ -61,9 +80,23 @@ class Launch(BaseModel):
|
|
|
61
80
|
|
|
62
81
|
at: Literal["boot", "demand"] = "boot"
|
|
63
82
|
command: list[str]
|
|
83
|
+
#: Matches the app's window by its WM_CLASS ``class`` or ``instance`` and its ``title``, as given.
|
|
64
84
|
window: Optional[dict[str, str]] = None
|
|
65
85
|
ready: Optional[Ready] = None
|
|
66
86
|
|
|
87
|
+
@field_validator("window")
|
|
88
|
+
@classmethod
|
|
89
|
+
def _matchable(cls, window: Optional[dict[str, str]]) -> Optional[dict[str, str]]:
|
|
90
|
+
if window is not None and (not window or set(window) - WINDOW_KEYS):
|
|
91
|
+
raise ValueError(f"window matches by {', '.join(sorted(WINDOW_KEYS))}, not {window!r}")
|
|
92
|
+
return window
|
|
93
|
+
|
|
94
|
+
@model_validator(mode="after")
|
|
95
|
+
def _window_to_wait_for(self) -> Launch:
|
|
96
|
+
if self.ready is not None and self.ready.window and self.window is None:
|
|
97
|
+
raise ValueError("ready waits on a window: name it with Launch(window={'class': ...})")
|
|
98
|
+
return self
|
|
99
|
+
|
|
67
100
|
|
|
68
101
|
class AppSkill(BaseModel):
|
|
69
102
|
"""Where an app's skill for agents comes from: a ``file`` of its folder, or the ``@agent_skill`` ``method`` that
|
|
@@ -134,6 +167,7 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
|
|
|
134
167
|
|
|
135
168
|
supports: ClassVar[list] = []
|
|
136
169
|
permissions: ClassVar[list[str]] = []
|
|
170
|
+
_computer_app: Optional[AppRuntime] = None
|
|
137
171
|
linux: ClassVar[Optional[Linux]] = None
|
|
138
172
|
launch: ClassVar[Optional[Launch]] = None
|
|
139
173
|
#: A file of the app's folder holding its skill for agents, when no ``@agent_skill`` method writes it.
|
|
@@ -145,6 +179,33 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
|
|
|
145
179
|
def __init__(self) -> None:
|
|
146
180
|
values = json.loads(os.environ.get(APP_SETTINGS_ENV) or "{}").get(os.environ.get("ENVIRONMENT_NAME"), {})
|
|
147
181
|
self.settings = self.Settings.model_validate(values)
|
|
182
|
+
ready = self.launch.ready if self.launch is not None else None
|
|
183
|
+
self._computer_app = AppRuntime(
|
|
184
|
+
self.launch, {fn.__name__: getattr(self, fn.__name__) for fn in _methods(type(self))
|
|
185
|
+
if hasattr(fn, _READY_CHECK_ATTR)},
|
|
186
|
+
ready.timeout_s if ready is not None else DEFAULT_TIMEOUT_S)
|
|
187
|
+
|
|
188
|
+
@extension(COMPUTER_APP, op="launch", description=(
|
|
189
|
+
"The app's own server: launch launches the app from its folder, as its launcher and a boot do; ready says "
|
|
190
|
+
"whether it's ready, by its launch's ready and its @ready_check methods."))
|
|
191
|
+
async def _computer_app_launch(self) -> dict:
|
|
192
|
+
if self.launch is None:
|
|
193
|
+
raise InvalidArgument(f"{type(self).__name__} declares no launch")
|
|
194
|
+
return {"pid": await self._computer_app_runtime().launch_app()}
|
|
195
|
+
|
|
196
|
+
@extension(COMPUTER_APP, op="ready", method="GET")
|
|
197
|
+
async def _computer_app_ready(self) -> dict:
|
|
198
|
+
return (await self._computer_app_runtime().readiness()).model_dump()
|
|
199
|
+
|
|
200
|
+
def _computer_app_runtime(self) -> AppRuntime:
|
|
201
|
+
if self._computer_app is None:
|
|
202
|
+
raise TypeError(f"{type(self).__qualname__}.__init__ must call super().__init__()")
|
|
203
|
+
return self._computer_app
|
|
204
|
+
|
|
205
|
+
def extension_params(self, uri: str) -> dict:
|
|
206
|
+
if uri == COMPUTER_APP:
|
|
207
|
+
return {"launch": self.launch.model_dump(mode="json", exclude_none=True) if self.launch else None}
|
|
208
|
+
return super().extension_params(uri)
|
|
148
209
|
|
|
149
210
|
@classmethod
|
|
150
211
|
def declaration(cls) -> AppDeclaration:
|
|
@@ -176,3 +237,4 @@ def _methods(cls: type) -> list[Callable]:
|
|
|
176
237
|
names = dict.fromkeys(name for klass in reversed(cls.__mro__) for name, member in vars(klass).items()
|
|
177
238
|
if inspect.isfunction(member))
|
|
178
239
|
return [method for name in names if inspect.isfunction(method := getattr(cls, name))]
|
|
240
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""A computer app's own server at work: it launches the app into the desktop session, marking each launch for the rest
|
|
2
|
+
of the boot, and says whether the app is ready, its checks run at once, each within ``CHECK_TIMEOUT_S``
|
|
3
|
+
(``AppReadiness``). ``ComputerAppEnvironment`` keeps one as ``self._computer_app``."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import inspect
|
|
8
|
+
import os
|
|
9
|
+
import shlex
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict
|
|
16
|
+
|
|
17
|
+
from . import session
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .app import Launch
|
|
21
|
+
|
|
22
|
+
#: Where an app's server marks that it launched the app since the computer booted, at boot or on demand.
|
|
23
|
+
LAUNCHED = Path("/run/agentenv/apps")
|
|
24
|
+
#: How long one readiness check may take; an app's checks run at once.
|
|
25
|
+
CHECK_TIMEOUT_S = 10.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AppReadiness(BaseModel):
|
|
29
|
+
"""What an app's server says of its readiness, ``computer-app/v1``'s ``ready``: the ``problems`` that keep it from
|
|
30
|
+
being ``ready``, whether it was ``launched`` since the computer booted, and ``waited_s`` since then, or for an app
|
|
31
|
+
with no launch since its server started, against its ``timeout_s``."""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(extra="forbid")
|
|
34
|
+
|
|
35
|
+
ready: bool
|
|
36
|
+
problems: list[str]
|
|
37
|
+
launched: bool
|
|
38
|
+
waited_s: float
|
|
39
|
+
timeout_s: float
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AppRuntime:
|
|
43
|
+
"""An app's server's state: its ``launch``, its ``@ready_check`` methods by name, bound to the app, how long it may
|
|
44
|
+
take to be ready, why its last launch failed, and each sync check still running in its thread."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, launch: Optional[Launch], ready_checks: dict[str, Callable[[], Any]], timeout_s: float) -> None:
|
|
47
|
+
self.launch = launch
|
|
48
|
+
self.ready_checks = ready_checks
|
|
49
|
+
self.timeout_s = timeout_s
|
|
50
|
+
self.started = time.time()
|
|
51
|
+
self.launch_failed: Optional[str] = None
|
|
52
|
+
self.checking: dict[str, asyncio.Future] = {}
|
|
53
|
+
|
|
54
|
+
async def start(self) -> None:
|
|
55
|
+
"""Once the desktop session is up, add the app's menu launcher and launch a boot app, unless the computer has
|
|
56
|
+
launched it since it booted, at boot or on demand: a server that restarts leaves the app be."""
|
|
57
|
+
if self.launch is None:
|
|
58
|
+
return
|
|
59
|
+
await session.wait_until_up()
|
|
60
|
+
session.add_launcher(os.environ["ENVIRONMENT_NAME"], Path.cwd())
|
|
61
|
+
if self.launch.at == "boot" and _launched_at() is None:
|
|
62
|
+
try:
|
|
63
|
+
await self.launch_app()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.launch_failed = f"{type(e).__name__}: {e}"
|
|
66
|
+
|
|
67
|
+
async def launch_app(self) -> int:
|
|
68
|
+
"""Start the app from its folder, marked as launched; its pid."""
|
|
69
|
+
pid = await session.launch(self.launch.command, Path.cwd(), {"ENVIRONMENT_NAME": os.environ["ENVIRONMENT_NAME"]})
|
|
70
|
+
self.launch_failed = None
|
|
71
|
+
_mark().parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
_mark().touch()
|
|
73
|
+
return pid
|
|
74
|
+
|
|
75
|
+
async def readiness(self) -> AppReadiness:
|
|
76
|
+
ready = self.launch.ready if self.launch is not None else None
|
|
77
|
+
checks: list[tuple[str, Awaitable[Optional[str]]]] = []
|
|
78
|
+
if ready is not None and ready.window:
|
|
79
|
+
checks.append(("window", _window(self.launch.window)))
|
|
80
|
+
if ready is not None and ready.port is not None:
|
|
81
|
+
checks.append(("port", _port(ready.port)))
|
|
82
|
+
if ready is not None and ready.command:
|
|
83
|
+
checks.append(("command", _command(ready.command)))
|
|
84
|
+
checks += [(f"@ready_check {name}", self._ready_check(name, fn)) for name, fn in self.ready_checks.items()]
|
|
85
|
+
found = await asyncio.gather(*(_bounded(name, check) for name, check in checks))
|
|
86
|
+
problems = [problem for problem in found if problem is not None]
|
|
87
|
+
launched = _launched_at()
|
|
88
|
+
if self.launch is not None and launched is None:
|
|
89
|
+
problems.insert(0, f"its launch failed: {self.launch_failed}" if self.launch_failed else "not launched yet")
|
|
90
|
+
return AppReadiness(ready=not problems, problems=problems, launched=launched is not None,
|
|
91
|
+
waited_s=round(time.time() - (launched or self.started), 1), timeout_s=self.timeout_s)
|
|
92
|
+
|
|
93
|
+
async def _ready_check(self, name: str, fn: Callable[[], Any]) -> Optional[str]:
|
|
94
|
+
"""A sync check runs in a thread, so it can't hold up the server, and once at a time: a thread can't be
|
|
95
|
+
stopped, so a poll while one runs past its bound waits on that same call."""
|
|
96
|
+
if inspect.iscoroutinefunction(fn):
|
|
97
|
+
passed = await fn()
|
|
98
|
+
else:
|
|
99
|
+
running = self.checking.get(name)
|
|
100
|
+
if running is None or running.done():
|
|
101
|
+
running = self.checking[name] = asyncio.ensure_future(asyncio.to_thread(fn))
|
|
102
|
+
running.add_done_callback(lambda done: done.cancelled() or done.exception())
|
|
103
|
+
passed = await asyncio.shield(running)
|
|
104
|
+
return None if passed else f"@ready_check {name} returned {passed!r}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _mark() -> Path:
|
|
108
|
+
return LAUNCHED / Path.cwd().name
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _launched_at() -> Optional[float]:
|
|
112
|
+
"""When the app was last launched since the computer booted, by its mark."""
|
|
113
|
+
try:
|
|
114
|
+
return _mark().stat().st_mtime
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def _bounded(name: str, check: Awaitable[Optional[str]]) -> Optional[str]:
|
|
120
|
+
"""What ``check`` finds wrong, within ``CHECK_TIMEOUT_S``; a check that runs past it or raises says so."""
|
|
121
|
+
try:
|
|
122
|
+
return await asyncio.wait_for(check, CHECK_TIMEOUT_S)
|
|
123
|
+
except TimeoutError:
|
|
124
|
+
return f"{name} ran past {CHECK_TIMEOUT_S:g} s"
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return f"{name}: {type(e).__name__}: {e}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def _window(match: dict[str, str]) -> Optional[str]:
|
|
130
|
+
if any(all(window.get(key) == value for key, value in match.items()) for window in await session.windows()):
|
|
131
|
+
return None
|
|
132
|
+
return f"no window with {', '.join(f'{key} {value!r}' for key, value in match.items())}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def _port(port: int) -> Optional[str]:
|
|
136
|
+
try:
|
|
137
|
+
_, writer = await asyncio.open_connection("127.0.0.1", port)
|
|
138
|
+
except OSError:
|
|
139
|
+
return f"nothing answers on port {port}"
|
|
140
|
+
writer.close()
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def _command(argv: list[str]) -> Optional[str]:
|
|
145
|
+
process = await asyncio.create_subprocess_exec(*argv, cwd=Path.cwd(), stdout=asyncio.subprocess.DEVNULL,
|
|
146
|
+
stderr=asyncio.subprocess.PIPE)
|
|
147
|
+
try:
|
|
148
|
+
_, err = await process.communicate()
|
|
149
|
+
except asyncio.CancelledError:
|
|
150
|
+
process.kill()
|
|
151
|
+
raise
|
|
152
|
+
if process.returncode == 0:
|
|
153
|
+
return None
|
|
154
|
+
reason = (err.decode(errors="replace").strip().splitlines() or [""])[-1]
|
|
155
|
+
return f"{shlex.join(argv)} exited {process.returncode}" + (f": {reason}" if reason else "")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""The apps composed onto a computer, as its driver finds them, and whether each is ready. Each app's own server
|
|
2
|
+
answers on its folder's socket (``serve_app``): its card says how the app launches, and its ``computer-app/v1``
|
|
3
|
+
``ready`` whether it's ready. Every app's server must answer before the computer is ready; an app that starts with the
|
|
4
|
+
computer must also be ready, and a ``demand`` app is checked once something launches it."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .. import client
|
|
12
|
+
from ..types import WELL_KNOWN_PATH
|
|
13
|
+
from .app import COMPUTER_APP, FAILURE, SOCKET
|
|
14
|
+
from .app_runtime import CHECK_TIMEOUT_S, AppReadiness
|
|
15
|
+
from .operations import ReadyCheck
|
|
16
|
+
|
|
17
|
+
#: Where a composed computer installs its apps, a folder each.
|
|
18
|
+
APPS = Path("/opt/agentenv/apps")
|
|
19
|
+
#: How long an app's server may take to answer, its checks run at once, before it counts as not answering.
|
|
20
|
+
ANSWER_TIMEOUT_S = CHECK_TIMEOUT_S + 5
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def expected() -> list[Path]:
|
|
24
|
+
"""The folders of the apps composed onto this computer."""
|
|
25
|
+
return sorted(path for path in APPS.iterdir() if path.is_dir()) if APPS.is_dir() else []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def check(folder: Path) -> ReadyCheck:
|
|
29
|
+
"""Whether the app in ``folder`` is ready, as its own server says, or why its server last failed to start; named by
|
|
30
|
+
its card once the server answers."""
|
|
31
|
+
name = folder.name
|
|
32
|
+
try:
|
|
33
|
+
async with _client(folder / SOCKET) as http:
|
|
34
|
+
card = (await http.get(WELL_KNOWN_PATH)).raise_for_status().json()
|
|
35
|
+
name = card["name"]
|
|
36
|
+
launch = client.extension_params(card, COMPUTER_APP).get("launch")
|
|
37
|
+
answer = AppReadiness.model_validate((await http.get(endpoint(card, "ready"))).raise_for_status().json())
|
|
38
|
+
except (httpx.HTTPError, OSError):
|
|
39
|
+
return ReadyCheck(f"app {name}", False, _unanswered(folder))
|
|
40
|
+
if launch is not None and launch["at"] == "demand" and not answer.launched:
|
|
41
|
+
return ReadyCheck(f"app {name}", True, "its server answers; it launches on demand")
|
|
42
|
+
if answer.ready:
|
|
43
|
+
return ReadyCheck(f"app {name}", True, "ready")
|
|
44
|
+
when = (f"not ready after {answer.timeout_s:.0f} s" if answer.waited_s > answer.timeout_s
|
|
45
|
+
else f"not ready yet, {answer.waited_s:.0f} s in")
|
|
46
|
+
return ReadyCheck(f"app {name}", False, f"{when}: {'; '.join(answer.problems)}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def endpoint(card: dict, method: str) -> str:
|
|
50
|
+
"""The path of a ``computer-app/v1`` method, as the app's card advertises it."""
|
|
51
|
+
return client.find_extension_method(card, COMPUTER_APP, method)["endpoint"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _unanswered(folder: Path) -> str:
|
|
55
|
+
try:
|
|
56
|
+
return f"its server failed to start: {(folder / FAILURE).read_text()}"
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
return "its server isn't answering yet"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _client(socket: Path) -> httpx.AsyncClient:
|
|
62
|
+
return httpx.AsyncClient(transport=httpx.AsyncHTTPTransport(uds=str(socket)), base_url="http://app",
|
|
63
|
+
timeout=ANSWER_TIMEOUT_S)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Launch a computer app through its own server, as its menu launcher does:
|
|
2
|
+
``python -m agentenv_protocol.computer.launch <app folder>`` asks the server on the folder's socket to launch it, so
|
|
3
|
+
the server starts every copy of the app."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ..types import WELL_KNOWN_PATH
|
|
12
|
+
from .app import SOCKET
|
|
13
|
+
from .apps import endpoint
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str]) -> None:
|
|
17
|
+
if len(argv) != 1:
|
|
18
|
+
raise SystemExit("usage: python -m agentenv_protocol.computer.launch <app folder>")
|
|
19
|
+
with httpx.Client(transport=httpx.HTTPTransport(uds=str(Path(argv[0]) / SOCKET)), base_url="http://app") as http:
|
|
20
|
+
card = http.get(WELL_KNOWN_PATH).raise_for_status().json()
|
|
21
|
+
http.post(endpoint(card, "launch"), json={}).raise_for_status()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main(sys.argv[1:])
|
|
@@ -13,6 +13,7 @@ from mcp.types import CallToolResult
|
|
|
13
13
|
|
|
14
14
|
from ..errors import AgentEnvError
|
|
15
15
|
from ..tool_context import ToolContext
|
|
16
|
+
from . import apps
|
|
16
17
|
from .errors import DeliveryFailed, DeviceUnavailable, InputBusy, Internal
|
|
17
18
|
from .frames import Frame, FrameStore, to_pil
|
|
18
19
|
from .operations import GROUPS, INPUT, Display, ReadyCheck, resolve
|
|
@@ -40,6 +41,7 @@ class ComputerRuntime:
|
|
|
40
41
|
self._commands_running: dict[str, asyncio.Task] = {}
|
|
41
42
|
self._commands_finished: OrderedDict = OrderedDict()
|
|
42
43
|
self.recordings = Recordings(self)
|
|
44
|
+
self.apps = apps.expected()
|
|
43
45
|
|
|
44
46
|
def has(self, capability: str) -> bool:
|
|
45
47
|
if capability in GROUPS:
|
|
@@ -175,6 +177,7 @@ class ComputerRuntime:
|
|
|
175
177
|
checks += list(await self.driver.ready_checks())
|
|
176
178
|
except Exception as e:
|
|
177
179
|
checks.append(ReadyCheck("driver", False, f"{type(e).__name__}: {e}"))
|
|
180
|
+
checks += await asyncio.gather(*(apps.check(folder) for folder in self.apps))
|
|
178
181
|
if all(c.ok for c in checks):
|
|
179
182
|
self.ready = True
|
|
180
183
|
return checks
|