agentenv-framework-protocol 0.1.443__tar.gz → 0.1.444__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.443 → agentenv_framework_protocol-0.1.444}/.gitignore +1 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/PKG-INFO +13 -7
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/README.md +12 -6
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/app.py +2 -1
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/apps.py +32 -4
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/card.py +1 -1
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/checks.py +2 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/driver.py +4 -1
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/extensions.py +6 -2
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/operations.py +2 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/platform.py +3 -2
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/website_browser.py +83 -26
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_driver.py +17 -1
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_website_browser.py +120 -20
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/_with_fds.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/app_data.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/app_runtime.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/session.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_cdp_config.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_app_server.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/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.444
|
|
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
|
|
@@ -561,7 +561,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
|
|
|
561
561
|
- **The input queue.** Input calls run one at a time in arrival order; one that waits past
|
|
562
562
|
`queue_wait_s` is refused `input_busy`. Screenshots and waits never queue.
|
|
563
563
|
- **The computer skill.** `computer/v1` declares the seven tools, its params the driver's
|
|
564
|
-
features
|
|
564
|
+
features, among them `screen`: `desktop`, or `page` for a driver whose screen is a website
|
|
565
|
+
browser's active tab, which a computer env pairs with exactly one website browser. The driver serves its skill through `skill/v1` (an `@agent_skill` handler on the base
|
|
565
566
|
class): a SKILL.md written from the computer as it booted (its platform, session user and home,
|
|
566
567
|
the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
|
|
567
568
|
class's `notes`, markdown for what its card can't say. A driver whose software ships a skill of
|
|
@@ -569,7 +570,9 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
|
|
|
569
570
|
computer skill leads with it, the computer's part following.
|
|
570
571
|
- **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
|
|
571
572
|
capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
|
|
572
|
-
own server (see Computer apps); tools refuse `device_unavailable` until they pass once
|
|
573
|
+
own server (see Computer apps); tools refuse `device_unavailable` until they pass once; a check
|
|
574
|
+
that can't pass again is `final`, and while one fails the status carries `failed`, saying why,
|
|
575
|
+
which `boot.ready()` raises at once rather than waiting out its timeout), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
|
|
573
576
|
descriptions), `computer-keyboard/v1`, `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
|
|
574
577
|
request_id}`: output capped per stream, a repeated `request_id` answered with the first result) and,
|
|
575
578
|
with `recording`, `computer-recording/v1` (below).
|
|
@@ -798,7 +801,7 @@ its card lists each one's card as a child environment, rooted at `/svc/<app>/`,
|
|
|
798
801
|
requests to the app's server.
|
|
799
802
|
|
|
800
803
|
`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
|
|
801
|
-
env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
804
|
+
env stores (`browser: true` for a website browser), and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
802
805
|
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
|
|
803
806
|
|
|
804
807
|
### Website browsers
|
|
@@ -825,8 +828,8 @@ are the profile's template. The base class adds:
|
|
|
825
828
|
`AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
|
|
826
829
|
Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
|
|
827
830
|
`[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
|
|
828
|
-
through. Alone
|
|
829
|
-
|
|
831
|
+
through. Alone, on a computer with no display (`XDG_SESSION_TYPE=none`, a browser computer's), it
|
|
832
|
+
also takes `--window-size` and `--headless=new`, and refuses to start with no proxy.
|
|
830
833
|
`declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
|
|
831
834
|
any other way to open a debugging port.
|
|
832
835
|
- **Before each launch** it empties its cache, so no launch inherits another's, and writes the deploy's
|
|
@@ -859,7 +862,10 @@ are the profile's template. The base class adds:
|
|
|
859
862
|
launch (with its cause: `boot`, `server_restart`, `launch`, `data/add` or `data/reset`), each stop it made
|
|
860
863
|
and each exit it didn't, with its exit `code` or the `signal` that ended it, since the computer booted.
|
|
861
864
|
Nothing relaunches a browser that exits on its own: in a computer the agent can, and alone it's an infra
|
|
862
|
-
failure, its ready check false.
|
|
865
|
+
failure, its ready check false. `page/viewport`, `page/capture` and `page/input`, framework-only, are
|
|
866
|
+
its active tab's page as a page driver sees it: its size, which headless is its window's; a PNG of it,
|
|
867
|
+
or `blank` for a tab that paints nothing within 3 s (1 s with no URL yet); and CDP input calls
|
|
868
|
+
(`Input.dispatchMouseEvent`, `Input.dispatchKeyEvent`, `Input.insertText`), sent to it in order.
|
|
863
869
|
- **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
|
|
864
870
|
`~/Downloads`, folders it makes.
|
|
865
871
|
|
|
@@ -505,7 +505,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
|
|
|
505
505
|
- **The input queue.** Input calls run one at a time in arrival order; one that waits past
|
|
506
506
|
`queue_wait_s` is refused `input_busy`. Screenshots and waits never queue.
|
|
507
507
|
- **The computer skill.** `computer/v1` declares the seven tools, its params the driver's
|
|
508
|
-
features
|
|
508
|
+
features, among them `screen`: `desktop`, or `page` for a driver whose screen is a website
|
|
509
|
+
browser's active tab, which a computer env pairs with exactly one website browser. The driver serves its skill through `skill/v1` (an `@agent_skill` handler on the base
|
|
509
510
|
class): a SKILL.md written from the computer as it booted (its platform, session user and home,
|
|
510
511
|
the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
|
|
511
512
|
class's `notes`, markdown for what its card can't say. A driver whose software ships a skill of
|
|
@@ -513,7 +514,9 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
|
|
|
513
514
|
computer skill leads with it, the computer's part following.
|
|
514
515
|
- **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
|
|
515
516
|
capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
|
|
516
|
-
own server (see Computer apps); tools refuse `device_unavailable` until they pass once
|
|
517
|
+
own server (see Computer apps); tools refuse `device_unavailable` until they pass once; a check
|
|
518
|
+
that can't pass again is `final`, and while one fails the status carries `failed`, saying why,
|
|
519
|
+
which `boot.ready()` raises at once rather than waiting out its timeout), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
|
|
517
520
|
descriptions), `computer-keyboard/v1`, `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
|
|
518
521
|
request_id}`: output capped per stream, a repeated `request_id` answered with the first result) and,
|
|
519
522
|
with `recording`, `computer-recording/v1` (below).
|
|
@@ -742,7 +745,7 @@ its card lists each one's card as a child environment, rooted at `/svc/<app>/`,
|
|
|
742
745
|
requests to the app's server.
|
|
743
746
|
|
|
744
747
|
`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
|
|
745
|
-
env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
748
|
+
env stores (`browser: true` for a website browser), and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
746
749
|
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
|
|
747
750
|
|
|
748
751
|
### Website browsers
|
|
@@ -769,8 +772,8 @@ are the profile's template. The base class adds:
|
|
|
769
772
|
`AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
|
|
770
773
|
Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
|
|
771
774
|
`[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
|
|
772
|
-
through. Alone
|
|
773
|
-
|
|
775
|
+
through. Alone, on a computer with no display (`XDG_SESSION_TYPE=none`, a browser computer's), it
|
|
776
|
+
also takes `--window-size` and `--headless=new`, and refuses to start with no proxy.
|
|
774
777
|
`declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
|
|
775
778
|
any other way to open a debugging port.
|
|
776
779
|
- **Before each launch** it empties its cache, so no launch inherits another's, and writes the deploy's
|
|
@@ -803,7 +806,10 @@ are the profile's template. The base class adds:
|
|
|
803
806
|
launch (with its cause: `boot`, `server_restart`, `launch`, `data/add` or `data/reset`), each stop it made
|
|
804
807
|
and each exit it didn't, with its exit `code` or the `signal` that ended it, since the computer booted.
|
|
805
808
|
Nothing relaunches a browser that exits on its own: in a computer the agent can, and alone it's an infra
|
|
806
|
-
failure, its ready check false.
|
|
809
|
+
failure, its ready check false. `page/viewport`, `page/capture` and `page/input`, framework-only, are
|
|
810
|
+
its active tab's page as a page driver sees it: its size, which headless is its window's; a PNG of it,
|
|
811
|
+
or `blank` for a tab that paints nothing within 3 s (1 s with no URL yet); and CDP input calls
|
|
812
|
+
(`Input.dispatchMouseEvent`, `Input.dispatchKeyEvent`, `Input.insertText`), sent to it in order.
|
|
807
813
|
- **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
|
|
808
814
|
`~/Downloads`, folders it makes.
|
|
809
815
|
|
|
@@ -236,7 +236,7 @@ class AppDeclaration(BaseModel):
|
|
|
236
236
|
"""What a ComputerAppEnvironment class declares, as a ``computer_app`` env stores it: the platforms it ``supports``,
|
|
237
237
|
as a driver names them; ``os``, what it declares for each OS type; ``settings`` and ``build_settings``, the JSON
|
|
238
238
|
schemas of its ``Settings`` and of its ``BuildSettings`` when it declares any; its ``skill``; and the ``data``
|
|
239
|
-
methods, ``install_steps`` and ``ready_checks`` it serves, by name."""
|
|
239
|
+
methods, ``install_steps`` and ``ready_checks`` it serves, by name; and ``browser``, true for a website browser."""
|
|
240
240
|
|
|
241
241
|
model_config = ConfigDict(extra="forbid")
|
|
242
242
|
|
|
@@ -249,6 +249,7 @@ class AppDeclaration(BaseModel):
|
|
|
249
249
|
data: list[str]
|
|
250
250
|
install_steps: list[InstallStep]
|
|
251
251
|
ready_checks: list[AppReadyCheck]
|
|
252
|
+
browser: Optional[bool] = None
|
|
252
253
|
|
|
253
254
|
@model_validator(mode="before")
|
|
254
255
|
@classmethod
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import copy
|
|
11
|
+
import json
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Optional
|
|
13
14
|
|
|
@@ -16,7 +17,7 @@ import httpx
|
|
|
16
17
|
from .. import client
|
|
17
18
|
from ..types import RPC_PATH, WELL_KNOWN_PATH, EnvironmentCard
|
|
18
19
|
from .app import COMPUTER_APP, FAILURE, SOCKET
|
|
19
|
-
from .app_runtime import CHECK_TIMEOUT_S, AppReadiness
|
|
20
|
+
from .app_runtime import CHECK_TIMEOUT_S, LAUNCHES, AppReadiness
|
|
20
21
|
from .operations import ReadyCheck
|
|
21
22
|
|
|
22
23
|
#: Where a composed computer installs its apps, a folder each.
|
|
@@ -54,7 +55,7 @@ async def check(folder: Path) -> ReadyCheck:
|
|
|
54
55
|
async def children(folders: list[Path]) -> list[EnvironmentCard]:
|
|
55
56
|
"""Each app in ``folders`` as a child environment of its computer: its server's card, rooted at ``/svc/<name>/``,
|
|
56
57
|
or, while its server isn't answering, its name and route alone."""
|
|
57
|
-
cards = await asyncio.gather(*(
|
|
58
|
+
cards = await asyncio.gather(*(served_card(folder) for folder in folders))
|
|
58
59
|
return [EnvironmentCard.model_validate(child_card(folder.name, card)) if card is not None
|
|
59
60
|
else EnvironmentCard(name=folder.name, url=f"/svc/{folder.name}{RPC_PATH}")
|
|
60
61
|
for folder, card in zip(folders, cards)]
|
|
@@ -85,7 +86,7 @@ def child_card(name: str, card: dict) -> dict:
|
|
|
85
86
|
|
|
86
87
|
async def owned_paths(folders: list[Path]) -> list[str]:
|
|
87
88
|
"""The paths under the session user's home that the apps in ``folders`` keep their data in, as their cards say."""
|
|
88
|
-
cards = await asyncio.gather(*(
|
|
89
|
+
cards = await asyncio.gather(*(served_card(folder) for folder in folders))
|
|
89
90
|
return [path for card in cards if card is not None
|
|
90
91
|
for path in client.extension_params(card, COMPUTER_APP).get("paths") or []]
|
|
91
92
|
|
|
@@ -104,7 +105,7 @@ def endpoint(card: dict, method: str) -> str:
|
|
|
104
105
|
return client.find_extension_method(card, COMPUTER_APP, method)["endpoint"]
|
|
105
106
|
|
|
106
107
|
|
|
107
|
-
async def
|
|
108
|
+
async def served_card(folder: Path) -> Optional[dict]:
|
|
108
109
|
"""The card the app in ``folder`` serves, or None while its server isn't answering."""
|
|
109
110
|
try:
|
|
110
111
|
async with _client(folder / SOCKET) as http:
|
|
@@ -113,6 +114,33 @@ async def _card(folder: Path) -> Optional[dict]:
|
|
|
113
114
|
return None
|
|
114
115
|
|
|
115
116
|
|
|
117
|
+
def ended_on_its_own(folder: Path) -> Optional[str]:
|
|
118
|
+
"""Why the app in ``folder`` can't be ready again where nothing launches it but its server: its launch log says it
|
|
119
|
+
ended on its own, an infrastructure failure; None while it hasn't."""
|
|
120
|
+
for entry in launch_log(folder):
|
|
121
|
+
if entry.get("event") == "exit":
|
|
122
|
+
how = (f"by signal {entry['signal']}" if "signal" in entry else
|
|
123
|
+
f"with code {entry['code']}" if "code" in entry else "in a way that wasn't recorded")
|
|
124
|
+
return (f"it ended on its own at {entry.get('at')}, {how}, and nothing launches it again: an "
|
|
125
|
+
"infrastructure failure")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def launch_log(folder: Path) -> list[dict]:
|
|
130
|
+
"""Each entry of the launch log of the app in ``folder`` that's whole, in order."""
|
|
131
|
+
try:
|
|
132
|
+
lines = (folder / LAUNCHES).read_text(encoding="utf-8").splitlines()
|
|
133
|
+
except FileNotFoundError:
|
|
134
|
+
return []
|
|
135
|
+
entries = []
|
|
136
|
+
for line in lines:
|
|
137
|
+
try:
|
|
138
|
+
entries.append(json.loads(line))
|
|
139
|
+
except ValueError:
|
|
140
|
+
continue
|
|
141
|
+
return entries
|
|
142
|
+
|
|
143
|
+
|
|
116
144
|
def _unanswered(folder: Path) -> str:
|
|
117
145
|
try:
|
|
118
146
|
return f"its server failed to start: {(folder / FAILURE).read_text()}"
|
|
@@ -89,7 +89,7 @@ def computer_features(driver: ComputerDriverEnvironment) -> dict:
|
|
|
89
89
|
"max_wait_ms": driver.max_wait_ms, "max_drag_points": driver.max_drag_points,
|
|
90
90
|
"max_duration_ms": driver.max_duration_ms, "max_key_repeat": driver.max_key_repeat,
|
|
91
91
|
"max_scroll_amount": driver.max_scroll_amount, "delivery": "foreground", "platform": driver.platform.as_dict(),
|
|
92
|
-
"supports": list(driver.supports), "default_data_paths": list(DEFAULT_DATA_PATHS),
|
|
92
|
+
"supports": list(driver.supports), "default_data_paths": list(DEFAULT_DATA_PATHS), "screen": driver.screen_kind,
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
|
|
@@ -37,6 +37,8 @@ def check_features(cls: type) -> None:
|
|
|
37
37
|
problems.append("keys are W3C key values in their W3C case")
|
|
38
38
|
except AgentEnvError as e:
|
|
39
39
|
problems.append(f"keys: {e.message}")
|
|
40
|
+
if cls.screen_kind not in ("desktop", "page"):
|
|
41
|
+
problems.append(f"screen_kind is desktop or page, got {cls.screen_kind!r}")
|
|
40
42
|
if ("set_display" in declared(cls)) != bool(cls.display_modes):
|
|
41
43
|
problems.append("display_modes and @set_display come together")
|
|
42
44
|
problems += check_recording(cls.recording)
|
|
@@ -4,7 +4,7 @@ computer's apps as child environments, each reached at ``/svc/<name>/`` through
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
|
-
from typing import Any, Callable, ClassVar
|
|
7
|
+
from typing import Any, Callable, ClassVar, Literal
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
from pydantic import BaseModel, ConfigDict
|
|
@@ -79,6 +79,9 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
|
|
|
79
79
|
recording: ClassVar[Recording | None] = Recording()
|
|
80
80
|
#: What an agent should know about this driver's computer beyond what its card says, as markdown: the end of its skill.
|
|
81
81
|
notes: ClassVar[str] = ""
|
|
82
|
+
#: What the computer's screen shows, its card's ``screen``: a ``desktop``, or a website browser's ``page``, its
|
|
83
|
+
#: active tab.
|
|
84
|
+
screen_kind: ClassVar[Literal["desktop", "page"]] = "desktop"
|
|
82
85
|
tool_access = True
|
|
83
86
|
#: What the base class keeps for the driver, built by ``__init__``.
|
|
84
87
|
_computer: ComputerRuntime | None = None
|
|
@@ -45,8 +45,12 @@ class Extensions:
|
|
|
45
45
|
description="Whether the computer is usable now, by real checks; never sends input.")
|
|
46
46
|
async def _computer_ready_status(self) -> dict:
|
|
47
47
|
checks = await self._computer.ready_checks()
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
status = {"ready": all(c.ok for c in checks),
|
|
49
|
+
"checks": [{"name": c.name, "ok": c.ok, "detail": c.detail, **({"final": True} if c.final else {})}
|
|
50
|
+
for c in checks]}
|
|
51
|
+
if failed := [c for c in checks if c.final and not c.ok]:
|
|
52
|
+
status["failed"] = "; ".join(f"{c.name}: {c.detail}" for c in failed)
|
|
53
|
+
return status
|
|
50
54
|
|
|
51
55
|
@extension(COMPUTER_DISPLAY, op="get", method="GET", description="Get or set the display's resolution and scale.")
|
|
52
56
|
async def _computer_display_get(self) -> dict:
|
|
@@ -28,14 +28,15 @@ class Platform:
|
|
|
28
28
|
|
|
29
29
|
def detect(environ: dict | None = None, os_release: Path = Path("/etc/os-release")) -> Platform:
|
|
30
30
|
"""The running platform: the OS, ``/etc/os-release``'s ID and VERSION_ID on Linux, the machine, and the
|
|
31
|
-
display from ``XDG_SESSION_TYPE``, else ``DISPLAY`` (x11) or
|
|
31
|
+
display from ``XDG_SESSION_TYPE``, ``none`` for a computer with no display, else ``DISPLAY`` (x11) or
|
|
32
|
+
``WAYLAND_DISPLAY`` (wayland)."""
|
|
32
33
|
environ = os.environ if environ is None else environ
|
|
33
34
|
name = _OS.get(sys.platform, sys.platform)
|
|
34
35
|
release = _os_release(os_release) if name == "linux" else {}
|
|
35
36
|
display = None
|
|
36
37
|
if name == "linux":
|
|
37
38
|
session = (environ.get("XDG_SESSION_TYPE") or "").strip().lower()
|
|
38
|
-
if session in ("x11", "wayland"):
|
|
39
|
+
if session in ("x11", "wayland", "none"):
|
|
39
40
|
display = session
|
|
40
41
|
elif environ.get("DISPLAY"):
|
|
41
42
|
display = "x11"
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
its build installs and starts; this class adds what agentenv owns, the same in both: its flags after the subclass's
|
|
3
3
|
command, the unattended-use policies, a CDP connection over the browser's debugging pipe, the settings every browser
|
|
4
4
|
takes, a fresh cache and the deploy's prompt answers at every launch, another window for a launch while it runs, its
|
|
5
|
-
skill and ``browser/v1`` on its card.
|
|
6
|
-
|
|
5
|
+
skill and ``browser/v1`` on its card. It runs alone, headless, on a computer with no display, a browser computer's,
|
|
6
|
+
whose screen is its page; which site proxy it goes through comes from a variable agentenv sets, never from its
|
|
7
|
+
settings."""
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import asyncio
|
|
@@ -23,11 +24,11 @@ from pydantic import BaseModel, Field, field_validator
|
|
|
23
24
|
|
|
24
25
|
from .agent_env_environment import agent_skill, extension
|
|
25
26
|
from .computer import AppDeclaration, ComputerAppEnvironment, Launch, install_step, ready_check
|
|
27
|
+
from .computer import app as computer_app
|
|
28
|
+
from .computer.errors import DeviceUnavailable, InvalidArgument
|
|
26
29
|
|
|
27
30
|
logger = logging.getLogger(__name__)
|
|
28
31
|
|
|
29
|
-
ALONE_ENV = "AGENTENV_BROWSER_ALONE"
|
|
30
|
-
OPEN_WEB_ENV = "AGENTENV_BROWSER_OPEN_WEB"
|
|
31
32
|
SITE_PROXY_ENV = "AGENTENV_SITE_PROXY"
|
|
32
33
|
BROWSER = "urn:agentenv:browser/v1"
|
|
33
34
|
#: The flags agentenv sets, by prefix, which a subclass's command may not carry: among them every way to open a debugging
|
|
@@ -51,6 +52,9 @@ HASH_CHUNK_BYTES = 1 << 20
|
|
|
51
52
|
AGENT = "agent"
|
|
52
53
|
#: The targets agentenv's connection follows.
|
|
53
54
|
PAGES = [{"type": "page"}]
|
|
55
|
+
#: The CDP input a computer's page driver may send to the active tab, and how long its screenshot may take.
|
|
56
|
+
PAGE_INPUT = frozenset({"Input.dispatchMouseEvent", "Input.dispatchKeyEvent", "Input.insertText"})
|
|
57
|
+
CAPTURE_TIMEOUT_S, UNNAVIGATED_CAPTURE_TIMEOUT_S = 3.0, 1.0
|
|
54
58
|
#: Alone, loopback goes through the proxy too. In a computer, the apps on loopback beside it stay reachable; ``<-loopback>``
|
|
55
59
|
#: removes Chromium's own rule, so ``*.localhost`` names still don't.
|
|
56
60
|
ALONE_BYPASS = "<-loopback>"
|
|
@@ -187,8 +191,8 @@ class BrowserControl:
|
|
|
187
191
|
``dialogs`` says to: always leaving a page that asks before it's left, so no answer of agentenv's blocks a
|
|
188
192
|
navigation."""
|
|
189
193
|
|
|
190
|
-
def __init__(self, devtools: DevToolsPipe, dialogs: str) -> None:
|
|
191
|
-
self.devtools, self.dialogs = devtools, dialogs
|
|
194
|
+
def __init__(self, devtools: DevToolsPipe, dialogs: str, viewport: Optional[tuple[int, int]] = None) -> None:
|
|
195
|
+
self.devtools, self.dialogs, self.viewport = devtools, dialogs, viewport
|
|
192
196
|
#: When this record starts: a server that restarts starts another.
|
|
193
197
|
self.since = _now()
|
|
194
198
|
self.tabs: dict[str, dict] = {}
|
|
@@ -209,7 +213,8 @@ class BrowserControl:
|
|
|
209
213
|
downloads unattended: into ``~/Downloads``, a name downloaded again kept beside the first."""
|
|
210
214
|
self.ended(STOP)
|
|
211
215
|
await self.devtools.call("Target.setDiscoverTargets", {"discover": True, "filter": PAGES})
|
|
212
|
-
await self.devtools.call("Target.setAutoAttach", {"autoAttach": True,
|
|
216
|
+
await self.devtools.call("Target.setAutoAttach", {"autoAttach": True,
|
|
217
|
+
"waitForDebuggerOnStart": self.viewport is not None,
|
|
213
218
|
"flatten": True, "filter": PAGES})
|
|
214
219
|
await self.devtools.call("Browser.setDownloadBehavior", {"behavior": "default", "eventsEnabled": True})
|
|
215
220
|
|
|
@@ -268,6 +273,36 @@ class BrowserControl:
|
|
|
268
273
|
"closed_tabs": list(self.closed_tabs), "downloads": list(self.downloads.values()),
|
|
269
274
|
"dialogs": list(self.dialog_log)}
|
|
270
275
|
|
|
276
|
+
async def active_session(self) -> str:
|
|
277
|
+
"""The session of the active tab, else of the first open one; DeviceUnavailable with no tab open."""
|
|
278
|
+
active = await self.active_tab() or next(iter(self.tabs), None)
|
|
279
|
+
session = next((session for session, target in self._sessions.items() if target == active), None)
|
|
280
|
+
if session is None:
|
|
281
|
+
raise DeviceUnavailable("the browser has no tab open")
|
|
282
|
+
return session
|
|
283
|
+
|
|
284
|
+
async def capture(self) -> Optional[str]:
|
|
285
|
+
"""The active tab's page as it shows now, a PNG, base64; None for a tab that paints nothing within
|
|
286
|
+
``CAPTURE_TIMEOUT_S``, which a desktop browser shows blank, or sooner for a tab with no URL yet, such as a new
|
|
287
|
+
tab whose navigation the browser blocked, which may never paint."""
|
|
288
|
+
session = await self.active_session()
|
|
289
|
+
navigated = self.tabs.get(self._sessions[session], {}).get("url")
|
|
290
|
+
try:
|
|
291
|
+
shot = await self.devtools.call("Page.captureScreenshot", {"format": "png", "optimizeForSpeed": True},
|
|
292
|
+
session_id=session,
|
|
293
|
+
timeout_s=CAPTURE_TIMEOUT_S if navigated else UNNAVIGATED_CAPTURE_TIMEOUT_S)
|
|
294
|
+
except asyncio.TimeoutError:
|
|
295
|
+
return None
|
|
296
|
+
return shot["data"]
|
|
297
|
+
|
|
298
|
+
async def dispatch(self, events: list[dict]) -> None:
|
|
299
|
+
"""Each of ``events``, a CDP input call, sent to the active tab in order; InvalidArgument for any other call."""
|
|
300
|
+
if refused := [event.get("method") for event in events if event.get("method") not in PAGE_INPUT]:
|
|
301
|
+
raise InvalidArgument(f"page input takes {', '.join(sorted(PAGE_INPUT))}, not {refused}")
|
|
302
|
+
session = await self.active_session()
|
|
303
|
+
for event in events:
|
|
304
|
+
await self.devtools.call(event["method"], event.get("params") or {}, session_id=session)
|
|
305
|
+
|
|
271
306
|
async def _refresh(self, target: Optional[str] = None) -> None:
|
|
272
307
|
"""The URL and title of the tab ``target``, or of every open tab, as the browser has them now: it sends no
|
|
273
308
|
event when a page's title changes."""
|
|
@@ -307,9 +342,19 @@ class BrowserControl:
|
|
|
307
342
|
def _on_Target_attachedToTarget(self, params: dict, session: Optional[str]) -> None:
|
|
308
343
|
if params["targetInfo"].get("type") == "page":
|
|
309
344
|
self._sessions[params["sessionId"]] = params["targetInfo"]["targetId"]
|
|
310
|
-
self._spawn(self.
|
|
311
|
-
|
|
312
|
-
|
|
345
|
+
self._spawn(self._let_run(params["sessionId"]))
|
|
346
|
+
|
|
347
|
+
async def _let_run(self, session: str) -> None:
|
|
348
|
+
"""A page it attached to, enabled and, headless, held to its window's size, then told to run: a page attached as
|
|
349
|
+
it opens, a popup say, or any new page headless, waits for that before it runs."""
|
|
350
|
+
try:
|
|
351
|
+
await self.devtools.call("Page.enable", session_id=session)
|
|
352
|
+
if self.viewport is not None:
|
|
353
|
+
width, height = self.viewport
|
|
354
|
+
await self.devtools.call("Emulation.setDeviceMetricsOverride", {
|
|
355
|
+
"width": width, "height": height, "deviceScaleFactor": 1, "mobile": False}, session_id=session)
|
|
356
|
+
finally:
|
|
357
|
+
await self.devtools.call("Runtime.runIfWaitingForDebugger", session_id=session)
|
|
313
358
|
|
|
314
359
|
def _on_Target_detachedFromTarget(self, params: dict, session: Optional[str]) -> None:
|
|
315
360
|
self._sessions.pop(params["sessionId"], None)
|
|
@@ -433,16 +478,19 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
|
|
|
433
478
|
for folder in (UPLOADS, DOWNLOADS):
|
|
434
479
|
(Path.home() / folder).mkdir(exist_ok=True)
|
|
435
480
|
#: agentenv's control of it over that connection, and the record it keeps.
|
|
436
|
-
self.control = BrowserControl(self.devtools, self.settings.dialogs
|
|
481
|
+
self.control = BrowserControl(self.devtools, self.settings.dialogs,
|
|
482
|
+
(self.settings.window.width, self.settings.window.height) if _alone()
|
|
483
|
+
else None)
|
|
437
484
|
self.devtools.on_close(self._pipe_closed)
|
|
438
485
|
|
|
439
486
|
@classmethod
|
|
440
487
|
def declaration(cls) -> AppDeclaration:
|
|
441
|
-
"""What the class declares; ValueError for a launch whose command carries a flag agentenv
|
|
488
|
+
"""What the class declares, a website browser; ValueError for a launch whose command carries a flag agentenv
|
|
489
|
+
owns."""
|
|
442
490
|
for declared in cls._installs.values():
|
|
443
491
|
if declared.launch is not None:
|
|
444
492
|
_refuse_owned(cls.__name__, declared.launch)
|
|
445
|
-
return super().declaration()
|
|
493
|
+
return super().declaration().model_copy(update={"browser": True})
|
|
446
494
|
|
|
447
495
|
def profile_dir(self) -> Path:
|
|
448
496
|
return Path.home() / self.profile
|
|
@@ -469,7 +517,7 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
|
|
|
469
517
|
"names none")
|
|
470
518
|
if alone:
|
|
471
519
|
flags.append(f"--window-size={settings.window.width},{settings.window.height}")
|
|
472
|
-
if
|
|
520
|
+
if alone:
|
|
473
521
|
flags.append("--headless=new")
|
|
474
522
|
return flags
|
|
475
523
|
|
|
@@ -491,7 +539,7 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
|
|
|
491
539
|
|
|
492
540
|
def _command_line_tabs(self) -> list[str]:
|
|
493
541
|
"""The start tabs its command line opens: headless Chromium takes one, and the rest open over its pipe."""
|
|
494
|
-
return self.settings.start_urls[:1] if
|
|
542
|
+
return self.settings.start_urls[:1] if _alone() else self.settings.start_urls
|
|
495
543
|
|
|
496
544
|
async def _launch_fds(self) -> dict[int, int]:
|
|
497
545
|
"""Before each launch, an empty cache and the deploy's prompt answers in its profile; and a new debugging pipe:
|
|
@@ -568,10 +616,27 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
|
|
|
568
616
|
"A website browser agentenv runs: its build, the clock its pages see and, alone, its window. record returns "
|
|
569
617
|
"what agentenv's own connection to it saw: its open tabs, each with whether it's active, the tabs closed and "
|
|
570
618
|
"who closed them, its downloads with their size and sha256, its JavaScript dialogs and who answered each, and "
|
|
571
|
-
"each launch, stop and exit of it since the computer booted, with its cause."
|
|
619
|
+
"each launch, stop and exit of it since the computer booted, with its cause. page/viewport, page/capture and "
|
|
620
|
+
"page/input are its active tab's page as a computer's page driver sees it: its size in pixels, which headless "
|
|
621
|
+
"is its window's, a PNG of it, and CDP input calls sent to it in order."))
|
|
572
622
|
async def browser_record(self) -> dict:
|
|
573
623
|
return {**await self.control.record(), "launches": self._computer_app.launches()}
|
|
574
624
|
|
|
625
|
+
@extension(BROWSER, op="page/viewport", method="GET")
|
|
626
|
+
async def browser_page_viewport(self) -> dict:
|
|
627
|
+
window = self.settings.window
|
|
628
|
+
return {"width": window.width, "height": window.height}
|
|
629
|
+
|
|
630
|
+
@extension(BROWSER, op="page/capture", method="GET")
|
|
631
|
+
async def browser_page_capture(self) -> dict:
|
|
632
|
+
png = await self.control.capture()
|
|
633
|
+
return {"png": png} if png is not None else {"blank": True}
|
|
634
|
+
|
|
635
|
+
@extension(BROWSER, op="page/input")
|
|
636
|
+
async def browser_page_input(self, events: list[dict]) -> dict:
|
|
637
|
+
await self.control.dispatch(events)
|
|
638
|
+
return {}
|
|
639
|
+
|
|
575
640
|
def extension_params(self, uri: str) -> dict:
|
|
576
641
|
if uri == BROWSER:
|
|
577
642
|
params: dict[str, Any] = {"chromium": self.version, "clock": "real"}
|
|
@@ -586,11 +651,8 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
|
|
|
586
651
|
|
|
587
652
|
|
|
588
653
|
def _alone() -> bool:
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def _open_web() -> bool:
|
|
593
|
-
return os.environ.get(OPEN_WEB_ENV) == "1"
|
|
654
|
+
"""Whether it runs on a computer with no display, a browser computer's, alone and headless."""
|
|
655
|
+
return computer_app.detect().display == "none"
|
|
594
656
|
|
|
595
657
|
|
|
596
658
|
def _shm_bytes(path: str = "/dev/shm") -> int:
|
|
@@ -601,11 +663,6 @@ def _shm_bytes(path: str = "/dev/shm") -> int:
|
|
|
601
663
|
return stats.f_frsize * stats.f_blocks
|
|
602
664
|
|
|
603
665
|
|
|
604
|
-
def _headless() -> bool:
|
|
605
|
-
"""Alone it runs headless, but headful on Xvfb for the open web, whose sites block headless browsers."""
|
|
606
|
-
return _alone() and not _open_web()
|
|
607
|
-
|
|
608
|
-
|
|
609
666
|
def _refuse_owned(name: str, launch: Launch) -> None:
|
|
610
667
|
if owned := [arg for arg in launch.command if arg.startswith(OWNED_FLAGS)]:
|
|
611
668
|
raise ValueError(f"{name}'s launch sets {', '.join(owned)}, which agentenv sets for every website browser")
|
|
@@ -115,6 +115,7 @@ def test_unknown_predicates_are_refused():
|
|
|
115
115
|
({"keys": ("enter",)}, "keys are W3C key values in their W3C case"),
|
|
116
116
|
({"keys": ("Return",)}, "keys: 'Return' is not a W3C key value"),
|
|
117
117
|
({"display_modes": ((10, 10),)}, "display_modes and @set_display come together"),
|
|
118
|
+
({"screen_kind": "tablet"}, "screen_kind is desktop or page, got 'tablet'"),
|
|
118
119
|
])
|
|
119
120
|
def test_features_are_checked_when_the_class_is_defined(features, error):
|
|
120
121
|
with pytest.raises(TypeError, match=error):
|
|
@@ -169,7 +170,7 @@ async def test_the_card_declares_the_contract_the_driver_implements():
|
|
|
169
170
|
computer = extensions["urn:agentenv:computer/v1"]["params"]
|
|
170
171
|
assert computer["buttons"] == ["left", "right", "middle"] and computer["max_click_count"] == 3
|
|
171
172
|
assert computer["keys"] == "all" and computer["platform"] == LINUX.as_dict() and computer["delivery"] == "foreground"
|
|
172
|
-
assert computer["default_data_paths"] == ["Desktop", "Documents", "Downloads"]
|
|
173
|
+
assert computer["default_data_paths"] == ["Desktop", "Documents", "Downloads"] and computer["screen"] == "desktop"
|
|
173
174
|
assert "methods" not in computer and extensions["urn:agentenv:skill/v1"]["params"]["methods"]["get"]["method"] == "GET"
|
|
174
175
|
assert extensions["urn:agentenv:computer-pointer/v1"]["params"]["buttons"] == ["left", "right", "middle"]
|
|
175
176
|
assert set(extensions["urn:agentenv:computer-keyboard/v1"]["params"]["methods"]) == {"get", "set"}
|
|
@@ -294,6 +295,21 @@ async def test_the_drivers_own_ready_checks_count():
|
|
|
294
295
|
async with http_client(Raising()) as http:
|
|
295
296
|
raised = (await http.get("/agentenv/ext/computer-ready/status")).json()
|
|
296
297
|
assert raised["checks"][-1] == {"name": "driver", "ok": False, "detail": "RuntimeError: probe crashed"}
|
|
298
|
+
assert "failed" not in before and "failed" not in raised
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@pytest.mark.asyncio
|
|
302
|
+
async def test_a_final_check_that_fails_has_the_status_say_the_computer_failed_and_why():
|
|
303
|
+
class Ended(FakeDriver):
|
|
304
|
+
async def ready_checks(self):
|
|
305
|
+
return [ReadyCheck("browser", False, "it ended on its own", final=True),
|
|
306
|
+
ReadyCheck("session", True, "logged in", final=True)]
|
|
307
|
+
|
|
308
|
+
async with http_client(Ended()) as http:
|
|
309
|
+
status = (await http.get("/agentenv/ext/computer-ready/status")).json()
|
|
310
|
+
assert status["ready"] is False and status["failed"] == "browser: it ended on its own"
|
|
311
|
+
assert status["checks"][-2:] == [{"name": "browser", "ok": False, "detail": "it ended on its own", "final": True},
|
|
312
|
+
{"name": "session", "ok": True, "detail": "logged in", "final": True}]
|
|
297
313
|
|
|
298
314
|
|
|
299
315
|
@pytest.mark.asyncio
|
|
@@ -19,10 +19,10 @@ import pytest
|
|
|
19
19
|
from agentenv_protocol.computer import APP_SETTINGS_ENV, ComputerAppEnvironment, Launch, Ready, app, linux, session
|
|
20
20
|
from agentenv_protocol import website_browser
|
|
21
21
|
from agentenv_protocol.computer import app_runtime
|
|
22
|
+
from agentenv_protocol.computer.errors import DeviceUnavailable, InvalidArgument
|
|
22
23
|
from agentenv_protocol.computer.platform import Platform
|
|
23
|
-
from agentenv_protocol.website_browser import (ALONE_BYPASS,
|
|
24
|
-
|
|
25
|
-
WebsiteBrowserEnvironment)
|
|
24
|
+
from agentenv_protocol.website_browser import (ALONE_BYPASS, BROWSER, COMPUTER_BYPASS, POLICIES, SITE_PROXY_ENV, SKILL,
|
|
25
|
+
BrowserControl, DevToolsError, WebsiteBrowserEnvironment)
|
|
26
26
|
|
|
27
27
|
PROXY = "http://127.0.0.1:3128"
|
|
28
28
|
OWNED = ["--user-data-dir", "--disk-cache-dir", "--remote-debugging-pipe", "--no-first-run",
|
|
@@ -80,7 +80,7 @@ def browser_home(monkeypatch, tmp_path):
|
|
|
80
80
|
monkeypatch.setenv("HOME", str(tmp_path / "home"))
|
|
81
81
|
(tmp_path / "home").mkdir()
|
|
82
82
|
monkeypatch.setenv("ENVIRONMENT_NAME", "chromium")
|
|
83
|
-
for name in (
|
|
83
|
+
for name in (SITE_PROXY_ENV, APP_SETTINGS_ENV):
|
|
84
84
|
monkeypatch.delenv(name, raising=False)
|
|
85
85
|
return tmp_path / "home"
|
|
86
86
|
|
|
@@ -89,6 +89,11 @@ def _settings(monkeypatch, **values) -> None:
|
|
|
89
89
|
monkeypatch.setenv(APP_SETTINGS_ENV, json.dumps({"chromium": values}))
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
def _alone(monkeypatch) -> None:
|
|
93
|
+
"""On a computer with no display, a browser computer's."""
|
|
94
|
+
monkeypatch.setattr(app, "detect", lambda: Platform(os="linux", display="none"))
|
|
95
|
+
|
|
96
|
+
|
|
92
97
|
def test_in_a_computer_its_flags_follow_its_command_then_its_start_tabs_and_its_window_is_waited_for(browser_home):
|
|
93
98
|
launch = Browser().launch
|
|
94
99
|
|
|
@@ -110,7 +115,7 @@ def test_in_a_computer_with_a_site_proxy_only_loopback_bypasses_it(monkeypatch):
|
|
|
110
115
|
|
|
111
116
|
|
|
112
117
|
def test_alone_it_goes_through_its_proxy_headless_at_its_window_and_its_launch_waits_on_no_window(monkeypatch):
|
|
113
|
-
monkeypatch
|
|
118
|
+
_alone(monkeypatch)
|
|
114
119
|
monkeypatch.setenv(SITE_PROXY_ENV, PROXY)
|
|
115
120
|
_settings(monkeypatch, window={"width": 1024, "height": 768})
|
|
116
121
|
|
|
@@ -130,7 +135,7 @@ def test_alone_its_launch_keeps_the_port_and_command_it_waits_on_and_their_timeo
|
|
|
130
135
|
class Waiting(WebsiteBrowserEnvironment):
|
|
131
136
|
pass
|
|
132
137
|
|
|
133
|
-
monkeypatch
|
|
138
|
+
_alone(monkeypatch)
|
|
134
139
|
monkeypatch.setenv(SITE_PROXY_ENV, PROXY)
|
|
135
140
|
|
|
136
141
|
waiting = Waiting()
|
|
@@ -140,18 +145,8 @@ def test_alone_its_launch_keeps_the_port_and_command_it_waits_on_and_their_timeo
|
|
|
140
145
|
assert waiting._computer_app.timeout_s == 45
|
|
141
146
|
|
|
142
147
|
|
|
143
|
-
def test_alone_on_the_open_web_it_runs_headful(monkeypatch):
|
|
144
|
-
monkeypatch.setenv(ALONE_ENV, "1")
|
|
145
|
-
monkeypatch.setenv(OPEN_WEB_ENV, "1")
|
|
146
|
-
monkeypatch.setenv(SITE_PROXY_ENV, PROXY)
|
|
147
|
-
|
|
148
|
-
command = Browser().launch.command
|
|
149
|
-
|
|
150
|
-
assert "--window-size=1280,800" in command and not any(flag.startswith("--headless") for flag in command)
|
|
151
|
-
|
|
152
|
-
|
|
153
148
|
def test_alone_it_refuses_to_launch_without_a_site_proxy(monkeypatch):
|
|
154
|
-
monkeypatch
|
|
149
|
+
_alone(monkeypatch)
|
|
155
150
|
|
|
156
151
|
with pytest.raises(ValueError, match=SITE_PROXY_ENV):
|
|
157
152
|
Browser()
|
|
@@ -215,7 +210,7 @@ def test_its_settings_give_its_start_tabs_language_and_time_zone_and_no_start_ur
|
|
|
215
210
|
@pytest.mark.asyncio
|
|
216
211
|
async def test_alone_its_server_launches_it_at_once_in_its_environment_with_no_display_wait_or_launcher(
|
|
217
212
|
monkeypatch, tmp_path):
|
|
218
|
-
monkeypatch
|
|
213
|
+
_alone(monkeypatch)
|
|
219
214
|
monkeypatch.setenv(SITE_PROXY_ENV, PROXY)
|
|
220
215
|
monkeypatch.delenv("DISPLAY", raising=False)
|
|
221
216
|
monkeypatch.chdir(tmp_path)
|
|
@@ -314,7 +309,7 @@ async def test_a_launch_while_it_runs_opens_a_window_over_its_pipe_and_starts_no
|
|
|
314
309
|
async def test_headless_it_opens_one_start_tab_from_its_command_line_and_the_rest_over_its_pipe(monkeypatch, tmp_path):
|
|
315
310
|
monkeypatch.chdir(tmp_path)
|
|
316
311
|
monkeypatch.setattr(app_runtime, "LAUNCHED", tmp_path / "launched")
|
|
317
|
-
monkeypatch
|
|
312
|
+
_alone(monkeypatch)
|
|
318
313
|
monkeypatch.setenv(SITE_PROXY_ENV, PROXY)
|
|
319
314
|
_settings(monkeypatch, start_urls=["https://slack.com", "https://linear.app", "https://github.com"])
|
|
320
315
|
browser = FakeBrowser()
|
|
@@ -400,7 +395,7 @@ def test_its_policies_are_agentenvs_with_its_own_over_them_where_its_build_reads
|
|
|
400
395
|
|
|
401
396
|
def test_its_card_declares_browser_v1_with_its_build_and_clock_and_alone_its_window(monkeypatch):
|
|
402
397
|
in_computer = Browser().extension_params(BROWSER)
|
|
403
|
-
monkeypatch
|
|
398
|
+
_alone(monkeypatch)
|
|
404
399
|
monkeypatch.setenv(SITE_PROXY_ENV, PROXY)
|
|
405
400
|
alone = Browser().extension_params(BROWSER)
|
|
406
401
|
|
|
@@ -503,6 +498,111 @@ async def test_each_page_it_attaches_to_is_told_to_run_so_a_popup_it_attached_to
|
|
|
503
498
|
|
|
504
499
|
assert ("Page.enable", {}, "s-popup") in devtools.calls
|
|
505
500
|
assert ("Runtime.runIfWaitingForDebugger", {}, "s-popup") in devtools.calls
|
|
501
|
+
assert not any(method == "Emulation.setDeviceMetricsOverride" for method, _, _ in devtools.calls)
|
|
502
|
+
assert next(params for method, params, _ in devtools.calls if method == "Target.setAutoAttach")[
|
|
503
|
+
"waitForDebuggerOnStart"] is False
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@pytest.mark.asyncio
|
|
507
|
+
async def test_alone_each_page_it_attaches_to_is_held_to_its_window_before_it_runs(tmp_path):
|
|
508
|
+
devtools = StubDevTools()
|
|
509
|
+
control = BrowserControl(devtools, "agent", (1024, 768))
|
|
510
|
+
await control.attach()
|
|
511
|
+
devtools.send("Target.attachedToTarget", {"sessionId": "s-popup", "waitingForDebugger": True, **_page("popup")})
|
|
512
|
+
await asyncio.sleep(0)
|
|
513
|
+
|
|
514
|
+
sent = [method for method, _, session in devtools.calls if session == "s-popup"]
|
|
515
|
+
assert next(params for method, params, _ in devtools.calls if method == "Target.setAutoAttach")[
|
|
516
|
+
"waitForDebuggerOnStart"] is True
|
|
517
|
+
assert sent == ["Page.enable", "Emulation.setDeviceMetricsOverride", "Runtime.runIfWaitingForDebugger"]
|
|
518
|
+
assert ("Emulation.setDeviceMetricsOverride", {"width": 1024, "height": 768, "deviceScaleFactor": 1,
|
|
519
|
+
"mobile": False}, "s-popup") in devtools.calls
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@pytest.mark.asyncio
|
|
523
|
+
async def test_a_page_it_couldnt_prepare_is_still_told_to_run_so_it_never_stays_held():
|
|
524
|
+
def refused(params, session):
|
|
525
|
+
raise DevToolsError("Target closed")
|
|
526
|
+
|
|
527
|
+
devtools = StubDevTools({"Page.enable": refused})
|
|
528
|
+
control = BrowserControl(devtools, "agent", (1024, 768))
|
|
529
|
+
await control.attach()
|
|
530
|
+
devtools.send("Target.attachedToTarget", {"sessionId": "s-popup", "waitingForDebugger": True, **_page("popup")})
|
|
531
|
+
await asyncio.sleep(0)
|
|
532
|
+
|
|
533
|
+
assert [method for method, _, session in devtools.calls if session == "s-popup"] == [
|
|
534
|
+
"Page.enable", "Runtime.runIfWaitingForDebugger"]
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class Painting(StubDevTools):
|
|
538
|
+
"""A browser whose every tab has focus and paints, but those whose sessions are in ``blank``, which never answer a
|
|
539
|
+
capture; it records each capture's timeout."""
|
|
540
|
+
|
|
541
|
+
def __init__(self, blank=()):
|
|
542
|
+
super().__init__({"Runtime.evaluate": lambda params, session: {"result": {"value": ["visible", True]}}})
|
|
543
|
+
self.blank, self.timeouts = set(blank), []
|
|
544
|
+
|
|
545
|
+
async def call(self, method, params=None, *, session_id=None, timeout_s=None):
|
|
546
|
+
if method != "Page.captureScreenshot":
|
|
547
|
+
return await super().call(method, params, session_id=session_id, timeout_s=timeout_s)
|
|
548
|
+
self.timeouts.append(timeout_s)
|
|
549
|
+
if session_id in self.blank:
|
|
550
|
+
raise asyncio.TimeoutError
|
|
551
|
+
return {"data": f"png of {session_id}"}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
async def _one_tab(devtools, url) -> BrowserControl:
|
|
555
|
+
control = BrowserControl(devtools, "agent")
|
|
556
|
+
await control.attach()
|
|
557
|
+
devtools.send("Target.targetCreated", _page("a", url))
|
|
558
|
+
devtools.send("Target.attachedToTarget", {"sessionId": "s-a", **_page("a", url)})
|
|
559
|
+
await asyncio.sleep(0)
|
|
560
|
+
return control
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@pytest.mark.asyncio
|
|
564
|
+
@pytest.mark.parametrize("url, blank, png, timeout", [
|
|
565
|
+
("https://mail.test/", (), "png of s-a", 3.0),
|
|
566
|
+
("https://mail.test/", ("s-a",), None, 3.0),
|
|
567
|
+
("", ("s-a",), None, 1.0),
|
|
568
|
+
])
|
|
569
|
+
async def test_its_capture_is_the_active_tabs_page_or_none_for_one_that_paints_nothing_sooner_with_no_url(
|
|
570
|
+
url, blank, png, timeout):
|
|
571
|
+
devtools = Painting(blank)
|
|
572
|
+
control = await _one_tab(devtools, url)
|
|
573
|
+
|
|
574
|
+
assert await control.capture() == png and devtools.timeouts == [timeout]
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
@pytest.mark.asyncio
|
|
578
|
+
async def test_its_page_input_goes_to_the_active_tab_in_order_and_takes_only_input_calls():
|
|
579
|
+
devtools = Painting()
|
|
580
|
+
control = await _one_tab(devtools, "https://mail.test/")
|
|
581
|
+
events = [{"method": "Input.dispatchMouseEvent", "params": {"type": "mousePressed", "x": 1, "y": 2}},
|
|
582
|
+
{"method": "Input.insertText", "params": {"text": "hi"}}]
|
|
583
|
+
|
|
584
|
+
await control.dispatch(events)
|
|
585
|
+
with pytest.raises(InvalidArgument, match="Page.navigate"):
|
|
586
|
+
await control.dispatch([*events, {"method": "Page.navigate", "params": {"url": "https://elsewhere.test/"}}])
|
|
587
|
+
|
|
588
|
+
sent = [(method, params, session) for method, params, session in devtools.calls if method.startswith("Input.")]
|
|
589
|
+
assert sent == [("Input.dispatchMouseEvent", {"type": "mousePressed", "x": 1, "y": 2}, "s-a"),
|
|
590
|
+
("Input.insertText", {"text": "hi"}, "s-a")]
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@pytest.mark.asyncio
|
|
594
|
+
async def test_with_no_tab_open_its_capture_and_input_are_unavailable():
|
|
595
|
+
control = BrowserControl(Painting(), "agent")
|
|
596
|
+
await control.attach()
|
|
597
|
+
|
|
598
|
+
with pytest.raises(DeviceUnavailable, match="no tab open"):
|
|
599
|
+
await control.capture()
|
|
600
|
+
with pytest.raises(DeviceUnavailable, match="no tab open"):
|
|
601
|
+
await control.dispatch([])
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def test_its_declaration_says_it_is_a_website_browser_and_another_apps_says_nothing():
|
|
605
|
+
assert Browser.declaration().browser is True and Mail.declaration().browser is None
|
|
506
606
|
|
|
507
607
|
|
|
508
608
|
@pytest.mark.asyncio
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/THIRD_PARTY_NOTICES.md
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/examples/basic_agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/computer_fake.py
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/probe_fake.py
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_a2a_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_errors.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_intake.py
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_manifest.py
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.443 → agentenv_framework_protocol-0.1.444}/tests/test_preflight.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|