agentenv-framework-protocol 0.1.428__tar.gz → 0.1.432__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.428 → agentenv_framework_protocol-0.1.432}/PKG-INFO +59 -6
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/README.md +58 -5
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/__init__.py +6 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/extensions.py +68 -1
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/framework.py +44 -4
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/tasks/v1.py +35 -0
- agentenv_framework_protocol-0.1.432/src/agentenv_protocol/computer/_with_fds.py +26 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/app.py +24 -3
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/app_runtime.py +29 -8
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/session.py +10 -3
- agentenv_framework_protocol-0.1.432/src/agentenv_protocol/website_browser.py +296 -0
- agentenv_framework_protocol-0.1.432/tests/test_a2a_cdp_config.py +207 -0
- agentenv_framework_protocol-0.1.432/tests/test_website_browser.py +361 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/app_data.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/apps.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/driver.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_app_server.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/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.432
|
|
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
|
|
@@ -743,7 +743,7 @@ class MailApp(ComputerAppEnvironment):
|
|
|
743
743
|
- **`launch=Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
|
|
744
744
|
the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
|
|
745
745
|
launch: a window matching `launch.window` by its class, instance or title, a port answering on
|
|
746
|
-
loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
|
|
746
|
+
loopback, or a command exiting 0. `env` sets variables in the app's environment, over its server's. `@ready_check` marks a method for readiness that needs logic (a
|
|
747
747
|
sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
|
|
748
748
|
app, after its run scripts, offline and without the class's `__init__`. Each runs on every OS the
|
|
749
749
|
app installs on, or on those its `os=` names (`@install_step(os="linux")`, `@ready_check(os=[...])`);
|
|
@@ -772,9 +772,10 @@ desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `la
|
|
|
772
772
|
and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
|
|
773
773
|
that can't start, or whose start in the session fails (adding the launcher, say), writes why to
|
|
774
774
|
`.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
|
|
775
|
-
the
|
|
776
|
-
-m agentenv_protocol.computer.launch <folder>` to ask the server, so the
|
|
777
|
-
the app, and launches a `boot` app, once per boot.
|
|
775
|
+
the X display is up, when `DISPLAY` names one, the server adds the app's launcher to the session user's
|
|
776
|
+
menu, which runs `python -m agentenv_protocol.computer.launch <folder>` to ask the server, so the
|
|
777
|
+
server starts every copy of the app, and launches a `boot` app, once per boot. An app that runs with
|
|
778
|
+
no desktop, a website browser alone, gets no launcher. The driver finds each app by the socket in its
|
|
778
779
|
folder: the computer is ready once every app's server answers and each `boot` app, and each app with
|
|
779
780
|
no launch, says it's ready; a `demand` app counts once something launches it. What the server does
|
|
780
781
|
in the session (`agentenv_protocol.computer.session`) is Linux's.
|
|
@@ -795,6 +796,44 @@ requests to the app's server.
|
|
|
795
796
|
env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
796
797
|
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
|
|
797
798
|
|
|
799
|
+
### Website browsers
|
|
800
|
+
|
|
801
|
+
`agentenv_protocol.website_browser.WebsiteBrowserEnvironment` is the computer app class for a Chromium
|
|
802
|
+
website browser, run inside a computer or alone. A subclass declares its build as any app does, its
|
|
803
|
+
`Launch` running the browser itself. It sets `version`, its build, and `policy_dir`, where that build
|
|
804
|
+
reads managed policies; `policies` adds its own over agentenv's, and `profile` is its profile folder
|
|
805
|
+
under the session user's home (`.config/chromium` by default), whose contents from its install steps
|
|
806
|
+
are the profile's template. The base class adds:
|
|
807
|
+
|
|
808
|
+
- **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
|
|
809
|
+
tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
|
|
810
|
+
the first from its command line and the rest over its pipe, since headless Chromium takes one; `window`,
|
|
811
|
+
1280 × 800, alone only; `locale`; and `time_zone`, set as `TZ` in its launch's `env`.
|
|
812
|
+
- **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile,
|
|
813
|
+
`--remote-debugging-pipe`, `--no-first-run`, `--no-default-browser-check` and `--lang`. With
|
|
814
|
+
`AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
|
|
815
|
+
Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
|
|
816
|
+
`[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
|
|
817
|
+
through. Alone (`AGENTENV_BROWSER_ALONE=1`) it also takes `--window-size` and `--headless=new`,
|
|
818
|
+
except on the open web (`AGENTENV_BROWSER_OPEN_WEB=1`), and refuses to start with no proxy.
|
|
819
|
+
`declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
|
|
820
|
+
any other way to open a debugging port.
|
|
821
|
+
- **Alone, no desktop**: its launch matches and waits on no window, its server adds no launcher and,
|
|
822
|
+
without `DISPLAY`, waits for no display, and its ready checks say when it's ready.
|
|
823
|
+
- **`write_policies`**, an install step that writes agentenv's unattended-use policies (`POLICIES`)
|
|
824
|
+
and the subclass's to `agentenv.json` in `policy_dir`.
|
|
825
|
+
- **A debugging pipe, not a port.** Its server launches it with a new pipe's ends as its fds 3 and 4
|
|
826
|
+
(`session.launch`'s `fds`, moved into place by `computer/_with_fds.py`, run alone with `-I -S`) and keeps the
|
|
827
|
+
other ends as `self.devtools`, a `DevToolsPipe`: `await self.devtools.call(method, params)` returns
|
|
828
|
+
the result or raises `DevToolsError`, and `ConnectionError` with no browser connected. No port
|
|
829
|
+
listens and no `DevToolsActivePort` is written, so nothing else in the computer can reach it over
|
|
830
|
+
CDP. Since only the server that launched it holds the pipe, a server that restarts stops it and
|
|
831
|
+
launches it again. The ready check `devtools_answers` asks `Browser.getVersion` over the pipe.
|
|
832
|
+
- **`browser/v1`** on its card, params only: `chromium` (its `version`), `clock: "real"` and, alone,
|
|
833
|
+
`window`.
|
|
834
|
+
- **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
|
|
835
|
+
`~/Downloads`, folders it makes.
|
|
836
|
+
|
|
798
837
|
## A2A agent framework
|
|
799
838
|
|
|
800
839
|
The distribution exposes two unrelated decorators named `extension`:
|
|
@@ -909,7 +948,7 @@ protocol. `TaskRequest` and `TaskResult` are framework boundary types; the
|
|
|
909
948
|
executor maps them to and from the wire-level `a2a.types.Task` lifecycle.
|
|
910
949
|
|
|
911
950
|
`TaskRequest` is a frozen record whose nested JSON values are detached copies.
|
|
912
|
-
Its `config`, `mcp_servers`, `skills`, `metadata`, and inbound `DataPart.data`
|
|
951
|
+
Its `config`, `mcp_servers`, `cdp_endpoints`, `skills`, `metadata`, and inbound `DataPart.data`
|
|
913
952
|
retain their declared `dict`/`list` types, so normal Pydantic serialization,
|
|
914
953
|
copying, and `json.dumps(...)` work. `TaskRequest.trajectory` is the exception:
|
|
915
954
|
it is the task's live trajectory log, described below. Within `tasks.v1`, new
|
|
@@ -1228,6 +1267,20 @@ tools with (agent-env relays the env card's name: the MultiEnv's declared name,
|
|
|
1228
1267
|
`env` + 4 random digits, giving e.g. `mcp__env4821__<tool>`); when absent the agent mints
|
|
1229
1268
|
`mcp_<8 hex>`.
|
|
1230
1269
|
|
|
1270
|
+
`CDP_CONFIG_V1` gives an agent the CDP endpoints it drives itself. `add` takes
|
|
1271
|
+
`name`, `http_url` (`http` or `https`, for clients that discover through
|
|
1272
|
+
`/json/version`) and `ws_url` (`ws` or `wss`, for clients that connect
|
|
1273
|
+
directly), with optional `headers` and `metadata`; adding a name again replaces
|
|
1274
|
+
it. `GET /ext/cdp-config` returns `{"cdp_endpoints": {name: {"has_headers",
|
|
1275
|
+
"metadata"}}}`: neither the URLs nor the header values, since either can carry
|
|
1276
|
+
the endpoint's credential. `DELETE /ext/cdp-config` with `{"name"}` removes one;
|
|
1277
|
+
removing a name that isn't there is not an error. Every task's
|
|
1278
|
+
`TaskRequest.cdp_endpoints` holds them in full. `TaskRequest.cdp_endpoint(name=None)`
|
|
1279
|
+
returns the named endpoint, or the only one, and otherwise fails the task as an
|
|
1280
|
+
`infra_error` with `cdp.no_endpoint` (none, or none by that name) or
|
|
1281
|
+
`cdp.ambiguous` (several and no name). It fails the task by raising, so call it
|
|
1282
|
+
before any handler of your own that catches every exception.
|
|
1283
|
+
|
|
1231
1284
|
`PEER_AGENTS_V1` lets an agent delegate to the other agents in its task. AgentEnv
|
|
1232
1285
|
sets the routing table with `POST /ext/peer-agents` (`{"peers": [{"name", "url",
|
|
1233
1286
|
"card", "description"}]}`; an empty list clears it, and names must be unique), and
|
|
@@ -687,7 +687,7 @@ class MailApp(ComputerAppEnvironment):
|
|
|
687
687
|
- **`launch=Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
|
|
688
688
|
the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
|
|
689
689
|
launch: a window matching `launch.window` by its class, instance or title, a port answering on
|
|
690
|
-
loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
|
|
690
|
+
loopback, or a command exiting 0. `env` sets variables in the app's environment, over its server's. `@ready_check` marks a method for readiness that needs logic (a
|
|
691
691
|
sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
|
|
692
692
|
app, after its run scripts, offline and without the class's `__init__`. Each runs on every OS the
|
|
693
693
|
app installs on, or on those its `os=` names (`@install_step(os="linux")`, `@ready_check(os=[...])`);
|
|
@@ -716,9 +716,10 @@ desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `la
|
|
|
716
716
|
and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
|
|
717
717
|
that can't start, or whose start in the session fails (adding the launcher, say), writes why to
|
|
718
718
|
`.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
|
|
719
|
-
the
|
|
720
|
-
-m agentenv_protocol.computer.launch <folder>` to ask the server, so the
|
|
721
|
-
the app, and launches a `boot` app, once per boot.
|
|
719
|
+
the X display is up, when `DISPLAY` names one, the server adds the app's launcher to the session user's
|
|
720
|
+
menu, which runs `python -m agentenv_protocol.computer.launch <folder>` to ask the server, so the
|
|
721
|
+
server starts every copy of the app, and launches a `boot` app, once per boot. An app that runs with
|
|
722
|
+
no desktop, a website browser alone, gets no launcher. The driver finds each app by the socket in its
|
|
722
723
|
folder: the computer is ready once every app's server answers and each `boot` app, and each app with
|
|
723
724
|
no launch, says it's ready; a `demand` app counts once something launches it. What the server does
|
|
724
725
|
in the session (`agentenv_protocol.computer.session`) is Linux's.
|
|
@@ -739,6 +740,44 @@ requests to the app's server.
|
|
|
739
740
|
env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
740
741
|
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
|
|
741
742
|
|
|
743
|
+
### Website browsers
|
|
744
|
+
|
|
745
|
+
`agentenv_protocol.website_browser.WebsiteBrowserEnvironment` is the computer app class for a Chromium
|
|
746
|
+
website browser, run inside a computer or alone. A subclass declares its build as any app does, its
|
|
747
|
+
`Launch` running the browser itself. It sets `version`, its build, and `policy_dir`, where that build
|
|
748
|
+
reads managed policies; `policies` adds its own over agentenv's, and `profile` is its profile folder
|
|
749
|
+
under the session user's home (`.config/chromium` by default), whose contents from its install steps
|
|
750
|
+
are the profile's template. The base class adds:
|
|
751
|
+
|
|
752
|
+
- **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
|
|
753
|
+
tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
|
|
754
|
+
the first from its command line and the rest over its pipe, since headless Chromium takes one; `window`,
|
|
755
|
+
1280 × 800, alone only; `locale`; and `time_zone`, set as `TZ` in its launch's `env`.
|
|
756
|
+
- **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile,
|
|
757
|
+
`--remote-debugging-pipe`, `--no-first-run`, `--no-default-browser-check` and `--lang`. With
|
|
758
|
+
`AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
|
|
759
|
+
Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
|
|
760
|
+
`[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
|
|
761
|
+
through. Alone (`AGENTENV_BROWSER_ALONE=1`) it also takes `--window-size` and `--headless=new`,
|
|
762
|
+
except on the open web (`AGENTENV_BROWSER_OPEN_WEB=1`), and refuses to start with no proxy.
|
|
763
|
+
`declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
|
|
764
|
+
any other way to open a debugging port.
|
|
765
|
+
- **Alone, no desktop**: its launch matches and waits on no window, its server adds no launcher and,
|
|
766
|
+
without `DISPLAY`, waits for no display, and its ready checks say when it's ready.
|
|
767
|
+
- **`write_policies`**, an install step that writes agentenv's unattended-use policies (`POLICIES`)
|
|
768
|
+
and the subclass's to `agentenv.json` in `policy_dir`.
|
|
769
|
+
- **A debugging pipe, not a port.** Its server launches it with a new pipe's ends as its fds 3 and 4
|
|
770
|
+
(`session.launch`'s `fds`, moved into place by `computer/_with_fds.py`, run alone with `-I -S`) and keeps the
|
|
771
|
+
other ends as `self.devtools`, a `DevToolsPipe`: `await self.devtools.call(method, params)` returns
|
|
772
|
+
the result or raises `DevToolsError`, and `ConnectionError` with no browser connected. No port
|
|
773
|
+
listens and no `DevToolsActivePort` is written, so nothing else in the computer can reach it over
|
|
774
|
+
CDP. Since only the server that launched it holds the pipe, a server that restarts stops it and
|
|
775
|
+
launches it again. The ready check `devtools_answers` asks `Browser.getVersion` over the pipe.
|
|
776
|
+
- **`browser/v1`** on its card, params only: `chromium` (its `version`), `clock: "real"` and, alone,
|
|
777
|
+
`window`.
|
|
778
|
+
- **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
|
|
779
|
+
`~/Downloads`, folders it makes.
|
|
780
|
+
|
|
742
781
|
## A2A agent framework
|
|
743
782
|
|
|
744
783
|
The distribution exposes two unrelated decorators named `extension`:
|
|
@@ -853,7 +892,7 @@ protocol. `TaskRequest` and `TaskResult` are framework boundary types; the
|
|
|
853
892
|
executor maps them to and from the wire-level `a2a.types.Task` lifecycle.
|
|
854
893
|
|
|
855
894
|
`TaskRequest` is a frozen record whose nested JSON values are detached copies.
|
|
856
|
-
Its `config`, `mcp_servers`, `skills`, `metadata`, and inbound `DataPart.data`
|
|
895
|
+
Its `config`, `mcp_servers`, `cdp_endpoints`, `skills`, `metadata`, and inbound `DataPart.data`
|
|
857
896
|
retain their declared `dict`/`list` types, so normal Pydantic serialization,
|
|
858
897
|
copying, and `json.dumps(...)` work. `TaskRequest.trajectory` is the exception:
|
|
859
898
|
it is the task's live trajectory log, described below. Within `tasks.v1`, new
|
|
@@ -1172,6 +1211,20 @@ tools with (agent-env relays the env card's name: the MultiEnv's declared name,
|
|
|
1172
1211
|
`env` + 4 random digits, giving e.g. `mcp__env4821__<tool>`); when absent the agent mints
|
|
1173
1212
|
`mcp_<8 hex>`.
|
|
1174
1213
|
|
|
1214
|
+
`CDP_CONFIG_V1` gives an agent the CDP endpoints it drives itself. `add` takes
|
|
1215
|
+
`name`, `http_url` (`http` or `https`, for clients that discover through
|
|
1216
|
+
`/json/version`) and `ws_url` (`ws` or `wss`, for clients that connect
|
|
1217
|
+
directly), with optional `headers` and `metadata`; adding a name again replaces
|
|
1218
|
+
it. `GET /ext/cdp-config` returns `{"cdp_endpoints": {name: {"has_headers",
|
|
1219
|
+
"metadata"}}}`: neither the URLs nor the header values, since either can carry
|
|
1220
|
+
the endpoint's credential. `DELETE /ext/cdp-config` with `{"name"}` removes one;
|
|
1221
|
+
removing a name that isn't there is not an error. Every task's
|
|
1222
|
+
`TaskRequest.cdp_endpoints` holds them in full. `TaskRequest.cdp_endpoint(name=None)`
|
|
1223
|
+
returns the named endpoint, or the only one, and otherwise fails the task as an
|
|
1224
|
+
`infra_error` with `cdp.no_endpoint` (none, or none by that name) or
|
|
1225
|
+
`cdp.ambiguous` (several and no name). It fails the task by raising, so call it
|
|
1226
|
+
before any handler of your own that catches every exception.
|
|
1227
|
+
|
|
1175
1228
|
`PEER_AGENTS_V1` lets an agent delegate to the other agents in its task. AgentEnv
|
|
1176
1229
|
sets the routing table with `POST /ext/peer-agents` (`{"peers": [{"name", "url",
|
|
1177
1230
|
"card", "description"}]}`; an empty list clears it, and names must be unique), and
|
|
@@ -5,6 +5,7 @@ from a2a.types import AgentCapabilities
|
|
|
5
5
|
from .extensions import (
|
|
6
6
|
AGENT_CONFIG_V1,
|
|
7
7
|
ATTRIBUTION_PROBE_V1,
|
|
8
|
+
CDP_CONFIG_V1,
|
|
8
9
|
PROXY_V1,
|
|
9
10
|
INSTALL_V1,
|
|
10
11
|
MCP_CONFIG_V1,
|
|
@@ -16,6 +17,8 @@ from .extensions import (
|
|
|
16
17
|
TRIGGERS_V1,
|
|
17
18
|
AttributionProbeResponse,
|
|
18
19
|
BundleSkillRequest,
|
|
20
|
+
CdpAddRequest,
|
|
21
|
+
CdpRemoveRequest,
|
|
19
22
|
ChangelogIncrement,
|
|
20
23
|
ContextEventsTrajectoryRequest,
|
|
21
24
|
ContextObjectTrajectoryRequest,
|
|
@@ -137,6 +140,7 @@ from ..transfers import (
|
|
|
137
140
|
__all__ = [
|
|
138
141
|
"AGENT_CONFIG_V1",
|
|
139
142
|
"ATTRIBUTION_PROBE_V1",
|
|
143
|
+
"CDP_CONFIG_V1",
|
|
140
144
|
"PROXY_V1",
|
|
141
145
|
"INSTALL_V1",
|
|
142
146
|
"MCP_CONFIG_V1",
|
|
@@ -157,6 +161,8 @@ __all__ = [
|
|
|
157
161
|
"AttributionProbeResponse",
|
|
158
162
|
"BuiltinTools",
|
|
159
163
|
"BundleSkillRequest",
|
|
164
|
+
"CdpAddRequest",
|
|
165
|
+
"CdpRemoveRequest",
|
|
160
166
|
"ChangelogIncrement",
|
|
161
167
|
"ContextEventsTrajectoryRequest",
|
|
162
168
|
"ContextObjectTrajectoryRequest",
|
|
@@ -11,6 +11,7 @@ from dataclasses import dataclass, field
|
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from types import MappingProxyType
|
|
13
13
|
from typing import Annotated, Any, Literal
|
|
14
|
+
from urllib.parse import urlsplit
|
|
14
15
|
|
|
15
16
|
from pydantic import (
|
|
16
17
|
BaseModel,
|
|
@@ -19,6 +20,7 @@ from pydantic import (
|
|
|
19
20
|
PositiveInt,
|
|
20
21
|
StrictInt,
|
|
21
22
|
ValidationError,
|
|
23
|
+
field_validator,
|
|
22
24
|
model_validator,
|
|
23
25
|
)
|
|
24
26
|
|
|
@@ -50,7 +52,9 @@ class ExtensionResponse(BaseModel):
|
|
|
50
52
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
_NAME_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
|
|
56
|
+
_SkillName = Annotated[str, Field(pattern=_NAME_PATTERN)]
|
|
57
|
+
_CdpEndpointName = Annotated[str, Field(pattern=_NAME_PATTERN)]
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
class McpAddRequest(ExtensionRequest):
|
|
@@ -59,6 +63,43 @@ class McpAddRequest(ExtensionRequest):
|
|
|
59
63
|
name: str | None = None
|
|
60
64
|
|
|
61
65
|
|
|
66
|
+
def _absolute_url(url: str, schemes: tuple[str, ...]) -> str:
|
|
67
|
+
"""``url``, once it names one of ``schemes``, a host and, if it has one, a valid port."""
|
|
68
|
+
parts = urlsplit(url)
|
|
69
|
+
try:
|
|
70
|
+
valid = parts.scheme in schemes and bool(parts.hostname) and (parts.port is None or parts.port >= 0)
|
|
71
|
+
except ValueError:
|
|
72
|
+
valid = False
|
|
73
|
+
if not valid:
|
|
74
|
+
raise ValueError(f"must be an absolute {' or '.join(schemes)} URL with a host and a valid port")
|
|
75
|
+
return url
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CdpAddRequest(ExtensionRequest):
|
|
79
|
+
"""A CDP endpoint: ``http_url`` for clients that discover through ``/json/version``, ``ws_url`` for clients
|
|
80
|
+
that connect to the browser directly."""
|
|
81
|
+
|
|
82
|
+
name: _CdpEndpointName
|
|
83
|
+
http_url: str
|
|
84
|
+
ws_url: str
|
|
85
|
+
headers: dict[str, str] | None = None
|
|
86
|
+
metadata: dict[str, Any] | None = None
|
|
87
|
+
|
|
88
|
+
@field_validator("http_url")
|
|
89
|
+
@classmethod
|
|
90
|
+
def _http_url(cls, url: str) -> str:
|
|
91
|
+
return _absolute_url(url, ("http", "https"))
|
|
92
|
+
|
|
93
|
+
@field_validator("ws_url")
|
|
94
|
+
@classmethod
|
|
95
|
+
def _ws_url(cls, url: str) -> str:
|
|
96
|
+
return _absolute_url(url, ("ws", "wss"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CdpRemoveRequest(ExtensionRequest):
|
|
100
|
+
name: _CdpEndpointName
|
|
101
|
+
|
|
102
|
+
|
|
62
103
|
class InlineSkillRequest(ExtensionRequest):
|
|
63
104
|
name: _SkillName
|
|
64
105
|
description: str
|
|
@@ -1101,6 +1142,31 @@ MCP_CONFIG_V1 = ExtensionDefinition(
|
|
|
1101
1142
|
},
|
|
1102
1143
|
)
|
|
1103
1144
|
|
|
1145
|
+
CDP_CONFIG_V1 = ExtensionDefinition(
|
|
1146
|
+
uri="urn:agentenv:cdp-config/v1",
|
|
1147
|
+
description="Register CDP endpoints the agent drives itself.",
|
|
1148
|
+
endpoint="/ext/cdp-config",
|
|
1149
|
+
core_operations={
|
|
1150
|
+
"add": _op(
|
|
1151
|
+
"add",
|
|
1152
|
+
"POST",
|
|
1153
|
+
"/ext/cdp-config",
|
|
1154
|
+
SDK,
|
|
1155
|
+
request_model=CdpAddRequest,
|
|
1156
|
+
),
|
|
1157
|
+
"list": _op(
|
|
1158
|
+
"list", "GET", "/ext/cdp-config", SDK, response_required=("cdp_endpoints",)
|
|
1159
|
+
),
|
|
1160
|
+
"remove": _op(
|
|
1161
|
+
"remove",
|
|
1162
|
+
"DELETE",
|
|
1163
|
+
"/ext/cdp-config",
|
|
1164
|
+
SDK,
|
|
1165
|
+
request_model=CdpRemoveRequest,
|
|
1166
|
+
),
|
|
1167
|
+
},
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1104
1170
|
SKILL_CONFIG_V1 = ExtensionDefinition(
|
|
1105
1171
|
uri="urn:agentenv:skill-config/v1",
|
|
1106
1172
|
description="Register skills made available to the agent.",
|
|
@@ -1319,6 +1385,7 @@ STANDARD_EXTENSIONS: Mapping[str, ExtensionDefinition] = MappingProxyType(
|
|
|
1319
1385
|
for extension in (
|
|
1320
1386
|
AGENT_CONFIG_V1,
|
|
1321
1387
|
MCP_CONFIG_V1,
|
|
1388
|
+
CDP_CONFIG_V1,
|
|
1322
1389
|
SKILL_CONFIG_V1,
|
|
1323
1390
|
TRAJECTORY_V1,
|
|
1324
1391
|
SNAPSHOT_V1,
|
|
@@ -32,6 +32,7 @@ from ._trajectory_log import MAX_TRAJECTORY_READ_BYTES, TaskTrajectories
|
|
|
32
32
|
from ._triggers import TriggerEngine, TriggerError
|
|
33
33
|
from .extensions import (
|
|
34
34
|
AGENT_CONFIG_V1,
|
|
35
|
+
CDP_CONFIG_V1,
|
|
35
36
|
MAX_TRAJECTORY_READ_EVENTS,
|
|
36
37
|
MCP_CONFIG_V1,
|
|
37
38
|
PEER_AGENTS_V1,
|
|
@@ -41,6 +42,8 @@ from .extensions import (
|
|
|
41
42
|
ContextEventsTrajectoryRequest,
|
|
42
43
|
ContextObjectTrajectoryRequest,
|
|
43
44
|
ContextTrajectoryRequest,
|
|
45
|
+
CdpAddRequest,
|
|
46
|
+
CdpRemoveRequest,
|
|
44
47
|
ExtensionActivation,
|
|
45
48
|
ExtensionDefinition,
|
|
46
49
|
ImplementationOwner,
|
|
@@ -66,6 +69,7 @@ from .tasks.v1 import (
|
|
|
66
69
|
TaskProgress,
|
|
67
70
|
TaskRequest,
|
|
68
71
|
TaskResult,
|
|
72
|
+
_TaskFailed,
|
|
69
73
|
)
|
|
70
74
|
from .tasks.v1 import (
|
|
71
75
|
DataPart as TaskDataPart,
|
|
@@ -541,6 +545,7 @@ class _SdkServices:
|
|
|
541
545
|
self.config_model
|
|
542
546
|
)
|
|
543
547
|
self.mcp_servers: dict[str, dict[str, Any]] = {}
|
|
548
|
+
self.cdp_endpoints: dict[str, dict[str, Any]] = {}
|
|
544
549
|
self.task_trajectories = TaskTrajectories()
|
|
545
550
|
self._context_sessions = _BoundedContextSessions()
|
|
546
551
|
self.skills: list[Mapping[str, Any]] = []
|
|
@@ -616,6 +621,9 @@ class _SdkServices:
|
|
|
616
621
|
(AGENT_CONFIG_V1.uri, "get"): self.agent_config_get,
|
|
617
622
|
(MCP_CONFIG_V1.uri, "add"): self.mcp_add,
|
|
618
623
|
(MCP_CONFIG_V1.uri, "list"): self.mcp_list,
|
|
624
|
+
(CDP_CONFIG_V1.uri, "add"): self.cdp_add,
|
|
625
|
+
(CDP_CONFIG_V1.uri, "list"): self.cdp_list,
|
|
626
|
+
(CDP_CONFIG_V1.uri, "remove"): self.cdp_remove,
|
|
619
627
|
(PEER_AGENTS_V1.uri, "set"): self.peer_agents_set,
|
|
620
628
|
(PEER_AGENTS_V1.uri, "list"): self.peer_agents_list,
|
|
621
629
|
(SKILL_CONFIG_V1.uri, "list"): self.skill_list,
|
|
@@ -721,6 +729,32 @@ class _SdkServices:
|
|
|
721
729
|
}
|
|
722
730
|
}
|
|
723
731
|
|
|
732
|
+
async def cdp_add(self, request: CdpAddRequest) -> dict[str, Any]:
|
|
733
|
+
"""Adding a name again replaces its endpoint."""
|
|
734
|
+
self.cdp_endpoints[request.name] = {
|
|
735
|
+
"http_url": request.http_url,
|
|
736
|
+
"ws_url": request.ws_url,
|
|
737
|
+
"headers": dict(request.headers) if request.headers else None,
|
|
738
|
+
"metadata": dict(request.metadata or {}),
|
|
739
|
+
}
|
|
740
|
+
return {"status": "added", "name": request.name}
|
|
741
|
+
|
|
742
|
+
async def cdp_list(self) -> dict[str, Any]:
|
|
743
|
+
"""Names and metadata only: an endpoint's URLs can carry its credential, as its headers can."""
|
|
744
|
+
return {
|
|
745
|
+
"cdp_endpoints": {
|
|
746
|
+
name: {
|
|
747
|
+
"has_headers": bool(endpoint["headers"]),
|
|
748
|
+
"metadata": endpoint["metadata"],
|
|
749
|
+
}
|
|
750
|
+
for name, endpoint in self.cdp_endpoints.items()
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async def cdp_remove(self, request: CdpRemoveRequest) -> dict[str, Any]:
|
|
755
|
+
removed = self.cdp_endpoints.pop(request.name, None) is not None
|
|
756
|
+
return {"status": "removed" if removed else "absent", "name": request.name}
|
|
757
|
+
|
|
724
758
|
def task_mcp_servers(self) -> dict[str, dict[str, Any]]:
|
|
725
759
|
"""The MCP servers a task gets: those registered, and the SDK's peer-agents server when it serves one."""
|
|
726
760
|
peer_tools = self.peers.registration() if self.peers is not None else None
|
|
@@ -1238,16 +1272,22 @@ class _StandardExecutor:
|
|
|
1238
1272
|
),
|
|
1239
1273
|
config=task_config,
|
|
1240
1274
|
mcp_servers=self._services.task_mcp_servers(),
|
|
1275
|
+
cdp_endpoints=self._services.cdp_endpoints,
|
|
1241
1276
|
skills=tuple(self._services.skills),
|
|
1242
1277
|
metadata=metadata,
|
|
1243
1278
|
session_ref=self._services.session_ref_for_context(task.context_id),
|
|
1244
1279
|
workspace=self._workspace,
|
|
1245
1280
|
trajectory=trajectory,
|
|
1246
1281
|
)
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1282
|
+
try:
|
|
1283
|
+
if self._streaming:
|
|
1284
|
+
result = await self._run_streaming(request, updater, event_queue)
|
|
1285
|
+
else:
|
|
1286
|
+
result = await self._handler(request)
|
|
1287
|
+
except _TaskFailed as failed:
|
|
1288
|
+
result = TaskResult.failure(
|
|
1289
|
+
failed.error.code, failed.error.message, error_type=failed.error.error_type
|
|
1290
|
+
)
|
|
1251
1291
|
if not isinstance(result, TaskResult):
|
|
1252
1292
|
raise TypeError("agent.run must return TaskResult")
|
|
1253
1293
|
if result.session_ref is not None:
|
|
@@ -217,6 +217,7 @@ class TaskRequest(Generic[ConfigT]):
|
|
|
217
217
|
parts: tuple[TaskPart, ...]
|
|
218
218
|
config: ConfigT
|
|
219
219
|
mcp_servers: Mapping[str, Any] = field(default_factory=dict)
|
|
220
|
+
cdp_endpoints: Mapping[str, Any] = field(default_factory=dict)
|
|
220
221
|
skills: tuple[Mapping[str, Any], ...] = ()
|
|
221
222
|
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
222
223
|
session_ref: str | None = None
|
|
@@ -233,11 +234,31 @@ class TaskRequest(Generic[ConfigT]):
|
|
|
233
234
|
# JSON-native types declared by the config model and request contract.
|
|
234
235
|
object.__setattr__(self, "config", self.config.model_copy(deep=True))
|
|
235
236
|
object.__setattr__(self, "mcp_servers", _copy_json_value(self.mcp_servers))
|
|
237
|
+
object.__setattr__(self, "cdp_endpoints", _copy_json_value(self.cdp_endpoints))
|
|
236
238
|
object.__setattr__(
|
|
237
239
|
self, "skills", tuple(_copy_json_value(skill) for skill in self.skills)
|
|
238
240
|
)
|
|
239
241
|
object.__setattr__(self, "metadata", _copy_json_value(self.metadata))
|
|
240
242
|
|
|
243
|
+
def cdp_endpoint(self, name: str | None = None) -> Mapping[str, Any]:
|
|
244
|
+
"""The CDP endpoint named ``name``, or the turn's only one. Anything else fails the turn, so an agent
|
|
245
|
+
never falls back to a browser of its own."""
|
|
246
|
+
if name is not None:
|
|
247
|
+
if name in self.cdp_endpoints:
|
|
248
|
+
return self.cdp_endpoints[name]
|
|
249
|
+
raise _TaskFailed(
|
|
250
|
+
"cdp.no_endpoint", f"the task gave no browser named {name!r}", error_type="infra_error"
|
|
251
|
+
)
|
|
252
|
+
if len(self.cdp_endpoints) == 1:
|
|
253
|
+
return next(iter(self.cdp_endpoints.values()))
|
|
254
|
+
if not self.cdp_endpoints:
|
|
255
|
+
raise _TaskFailed("cdp.no_endpoint", "the task gave no browser", error_type="infra_error")
|
|
256
|
+
raise _TaskFailed(
|
|
257
|
+
"cdp.ambiguous",
|
|
258
|
+
f"the task gave several browsers; name one of {', '.join(sorted(self.cdp_endpoints))}",
|
|
259
|
+
error_type="infra_error",
|
|
260
|
+
)
|
|
261
|
+
|
|
241
262
|
|
|
242
263
|
class TaskOutcome(str, Enum):
|
|
243
264
|
SUCCEEDED = "succeeded"
|
|
@@ -255,6 +276,20 @@ class TaskError:
|
|
|
255
276
|
raise ValueError("error_type must be 'agent_error' or 'infra_error'")
|
|
256
277
|
|
|
257
278
|
|
|
279
|
+
class _TaskFailed(Exception):
|
|
280
|
+
"""Ends the turn as ``TaskResult.failure(code, message, error_type=error_type)``; ``cdp_endpoint`` raises it."""
|
|
281
|
+
|
|
282
|
+
def __init__(
|
|
283
|
+
self,
|
|
284
|
+
code: str,
|
|
285
|
+
message: str,
|
|
286
|
+
*,
|
|
287
|
+
error_type: Literal["agent_error", "infra_error"] = "agent_error",
|
|
288
|
+
) -> None:
|
|
289
|
+
super().__init__(message)
|
|
290
|
+
self.error = TaskError(code=code, message=message, error_type=error_type)
|
|
291
|
+
|
|
292
|
+
|
|
258
293
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
259
294
|
class NativeTrajectory:
|
|
260
295
|
format: str
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Run a program with fds it inherits moved to the numbers it expects: ``python -I -S _with_fds.py <from>=<to> ... --
|
|
2
|
+
<argv>``. ``session.launch`` runs an app launched with ``fds`` through it, by path, so its server runs no Python between
|
|
3
|
+
its fork and its exec and this loads nothing but the standard library."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import fcntl
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv: list[str]) -> None:
|
|
12
|
+
split = argv.index("--")
|
|
13
|
+
moves = [tuple(int(number) for number in pair.split("=")) for pair in argv[:split]]
|
|
14
|
+
# Each source copied above every number named first, so no copy, source or target is another.
|
|
15
|
+
above = max((number for move in moves for number in move), default=2) + 1
|
|
16
|
+
copies = [(fcntl.fcntl(source, fcntl.F_DUPFD_CLOEXEC, above), target) for source, target in moves]
|
|
17
|
+
for source, _ in moves:
|
|
18
|
+
os.close(source)
|
|
19
|
+
for copy, target in copies:
|
|
20
|
+
os.dup2(copy, target)
|
|
21
|
+
os.close(copy)
|
|
22
|
+
os.execvp(argv[split + 1], argv[split + 1:])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main(sys.argv[1:])
|
|
@@ -169,6 +169,8 @@ class Launch(BaseModel):
|
|
|
169
169
|
#: Matches the app's window by its WM_CLASS ``class`` or ``instance`` and its ``title``, as given.
|
|
170
170
|
window: Optional[dict[str, str]] = None
|
|
171
171
|
ready: Optional[Ready] = None
|
|
172
|
+
#: Variables set in the app's environment, over its server's.
|
|
173
|
+
env: Optional[dict[str, str]] = None
|
|
172
174
|
|
|
173
175
|
@field_validator("window")
|
|
174
176
|
@classmethod
|
|
@@ -399,17 +401,36 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
|
|
|
399
401
|
self.settings = cls.settings_on(on).model_validate(values)
|
|
400
402
|
self.build_settings = _built_settings(cls, on)
|
|
401
403
|
declared = cls._installs.get(on)
|
|
402
|
-
|
|
404
|
+
launch = declared.launch if declared is not None else None
|
|
405
|
+
self.launch = self._launch_for(launch) if launch is not None else None
|
|
403
406
|
self.paths = list(declared.paths) if declared is not None else []
|
|
404
|
-
ready =
|
|
407
|
+
ready = launch.ready if launch is not None else None
|
|
405
408
|
self._computer_app = AppRuntime(
|
|
406
409
|
self.launch, {fn.__name__: getattr(self, fn.__name__) for fn in _methods(cls)
|
|
407
410
|
if hasattr(fn, _READY_CHECK_ATTR) and _runs_on(getattr(fn, _READY_CHECK_ATTR), on)},
|
|
408
|
-
ready.timeout_s if ready is not None else DEFAULT_TIMEOUT_S)
|
|
411
|
+
ready.timeout_s if ready is not None else DEFAULT_TIMEOUT_S, desktop=self._on_desktop(),
|
|
412
|
+
launch_fds=self._launch_fds, relaunch_on_start=self._relaunch_on_start())
|
|
409
413
|
self._computer_app_data = AppDataPlane(
|
|
410
414
|
self._computer_app, {op: getattr(self, name) for op, name in cls._data_hooks.items()}, self.paths,
|
|
411
415
|
cls.data_timeout_s)
|
|
412
416
|
|
|
417
|
+
def _launch_for(self, launch: Launch) -> Launch:
|
|
418
|
+
"""The launch its server uses, from the one declared for its OS, whose ``ready`` timeout it keeps: as
|
|
419
|
+
declared."""
|
|
420
|
+
return launch
|
|
421
|
+
|
|
422
|
+
def _on_desktop(self) -> bool:
|
|
423
|
+
"""Whether it runs in a computer's desktop session, which lists it in its menu."""
|
|
424
|
+
return True
|
|
425
|
+
|
|
426
|
+
async def _launch_fds(self) -> dict[int, int]:
|
|
427
|
+
"""The fds each launch hands it, by the number each gets in it, made new for that launch: none."""
|
|
428
|
+
return {}
|
|
429
|
+
|
|
430
|
+
def _relaunch_on_start(self) -> bool:
|
|
431
|
+
"""Whether only the server that launched it can talk to it, so a server that restarts launches it again."""
|
|
432
|
+
return False
|
|
433
|
+
|
|
413
434
|
@classmethod
|
|
414
435
|
def settings_on(cls, os_type: Optional[str]) -> type[BaseModel]:
|
|
415
436
|
"""Its ``Settings`` on ``os_type``, with the fields its OS adds."""
|
|
@@ -48,30 +48,51 @@ class AppRuntime:
|
|
|
48
48
|
"""An app's server's state: its ``launch``, its ``@ready_check`` methods by name, bound to the app, how long it may
|
|
49
49
|
take to be ready, why its last launch failed, and each sync check still running in its thread."""
|
|
50
50
|
|
|
51
|
-
def __init__(self, launch: Optional[Launch], ready_checks: dict[str, Callable[[], Any]], timeout_s: float
|
|
51
|
+
def __init__(self, launch: Optional[Launch], ready_checks: dict[str, Callable[[], Any]], timeout_s: float, *,
|
|
52
|
+
desktop: bool = True, launch_fds: Optional[Callable[[], Awaitable[dict[int, int]]]] = None,
|
|
53
|
+
relaunch_on_start: bool = False) -> None:
|
|
52
54
|
self.launch = launch
|
|
53
55
|
self.ready_checks = ready_checks
|
|
54
56
|
self.timeout_s = timeout_s
|
|
57
|
+
self.desktop = desktop
|
|
58
|
+
self.launch_fds = launch_fds
|
|
59
|
+
self.relaunch_on_start = relaunch_on_start
|
|
55
60
|
self.started = time.time()
|
|
56
61
|
self.launch_failed: Optional[str] = None
|
|
57
62
|
self.checking: dict[str, asyncio.Future] = {}
|
|
58
63
|
|
|
59
64
|
async def start(self) -> None:
|
|
60
|
-
"""Once
|
|
61
|
-
launched it since it booted, at boot or on demand: a server that restarts leaves the
|
|
65
|
+
"""Once its X display is up, when it has one, add the app's menu launcher on a desktop and launch a boot app,
|
|
66
|
+
unless the computer has launched it since it booted, at boot or on demand: a server that restarts leaves the
|
|
67
|
+
app be, or with ``relaunch_on_start``, for an app only the server that launched it can talk to, stops it and
|
|
68
|
+
launches it again, whether it was launched at boot or on demand."""
|
|
62
69
|
if self.launch is None:
|
|
63
70
|
return
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if self.
|
|
71
|
+
if os.environ.get("DISPLAY"):
|
|
72
|
+
await session.wait_until_up()
|
|
73
|
+
if self.desktop:
|
|
74
|
+
session.add_launcher(os.environ["ENVIRONMENT_NAME"], Path.cwd())
|
|
75
|
+
launched = _launched_at() is not None
|
|
76
|
+
relaunch = self.relaunch_on_start and launched
|
|
77
|
+
if relaunch or (self.launch.at == "boot" and not launched):
|
|
67
78
|
try:
|
|
79
|
+
if relaunch:
|
|
80
|
+
await self.stop_app()
|
|
68
81
|
await self.launch_app()
|
|
69
82
|
except Exception as e:
|
|
70
83
|
self.launch_failed = f"{type(e).__name__}: {e}"
|
|
71
84
|
|
|
72
85
|
async def launch_app(self) -> int:
|
|
73
|
-
"""Start the app from its folder, marked as launched
|
|
74
|
-
|
|
86
|
+
"""Start the app from its folder, marked as launched, with the fds the app gives it, whose copies here are
|
|
87
|
+
closed once it has them; its pid."""
|
|
88
|
+
fds = await self.launch_fds() if self.launch_fds is not None else {}
|
|
89
|
+
try:
|
|
90
|
+
pid = await session.launch(self.launch.command, Path.cwd(),
|
|
91
|
+
{**(self.launch.env or {}), "ENVIRONMENT_NAME": os.environ["ENVIRONMENT_NAME"]},
|
|
92
|
+
**({"fds": fds} if fds else {}))
|
|
93
|
+
finally:
|
|
94
|
+
for fd in fds.values():
|
|
95
|
+
os.close(fd)
|
|
75
96
|
self.launch_failed = None
|
|
76
97
|
PID.parent.mkdir(parents=True, exist_ok=True)
|
|
77
98
|
PID.write_text(str(pid))
|