agentenv-framework-protocol 0.1.470__tar.gz → 0.1.472__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.470 → agentenv_framework_protocol-0.1.472}/PKG-INFO +37 -3
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/README.md +33 -2
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/pyproject.toml +6 -1
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/browser_cdp.py +5 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/__init__.py +4 -1
- agentenv_framework_protocol-0.1.472/src/agentenv_protocol/computer/_x11.py +395 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/app.py +25 -1
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/app_data.py +46 -14
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/app_runtime.py +11 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/apps.py +14 -1
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/dataplane.py +2 -2
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/driver.py +12 -2
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/session.py +20 -3
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/svc.py +8 -4
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website_browser/control.py +39 -0
- agentenv_framework_protocol-0.1.472/src/agentenv_protocol/website_browser/data.py +125 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website_browser/environment.py +107 -4
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website_browser/shapes.py +34 -0
- agentenv_framework_protocol-0.1.472/tests/test_browser_data.py +313 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_app_data.py +137 -5
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_app_server.py +26 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_conformance.py +17 -9
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_dataplane.py +3 -3
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_svc_forward.py +33 -2
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/Dockerfile +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/Dockerfile.dockerignore +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/README.md +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/data/seed.json +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/env.toml +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/site.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/web/index.html +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/web/package-lock.json +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/web/package.json +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/web/src/main.js +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/web/src/style.css +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/examples/website/notes/web/vite.config.js +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/_macos.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/_with_fds.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/seed.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website/clock.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website/conformance.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website/environment.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website/errors.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website/readiness.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website/routes.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website/session.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website_browser/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/src/agentenv_protocol/website_browser/devtools.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_a2a_cdp_config.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_browser_state.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_conformance_page.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_macos.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_page.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_seed.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_tool_context_injection.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_website_api.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_website_browser.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_website_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_website_environment.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_website_mcp.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_website_notes_example.py +0 -0
- {agentenv_framework_protocol-0.1.470 → agentenv_framework_protocol-0.1.472}/tests/test_website_readiness.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.472
|
|
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
|
|
@@ -58,6 +58,9 @@ Requires-Dist: websockets>=13; extra == 'dev'
|
|
|
58
58
|
Provides-Extra: macos
|
|
59
59
|
Requires-Dist: pyobjc-framework-quartz>=11; (sys_platform == 'darwin') and extra == 'macos'
|
|
60
60
|
Requires-Dist: pyobjc-framework-screencapturekit>=11; (sys_platform == 'darwin') and extra == 'macos'
|
|
61
|
+
Provides-Extra: x11
|
|
62
|
+
Requires-Dist: mss<11,>=9; extra == 'x11'
|
|
63
|
+
Requires-Dist: python-xlib<0.34,>=0.33; extra == 'x11'
|
|
61
64
|
Description-Content-Type: text/markdown
|
|
62
65
|
|
|
63
66
|
# agentenv-framework-protocol
|
|
@@ -891,13 +894,19 @@ in the session (`agentenv_protocol.computer.session`) is Linux's.
|
|
|
891
894
|
|
|
892
895
|
The server serves the app's data plane and its skill. With `paths` alone, `data/add` writes the files
|
|
893
896
|
it's given under them and refuses any other path, or one a link leads out of them; `data/get`
|
|
894
|
-
returns the files there, a file tree
|
|
897
|
+
returns the files there, a file tree, copied with every process of the app paused (SIGSTOP) so they
|
|
898
|
+
are of one moment, and sent from the copy once it runs again; and `data/reset` puts them back as the server found them when
|
|
895
899
|
it first started on this boot, which is the app as installed. Hooks serve what they mark instead.
|
|
896
900
|
A hook's `data/add` parts come as they were sent, inline or as a read grant, and
|
|
897
901
|
`part_bytes(part)` returns a part's bytes either way. Either way the base class takes data calls and
|
|
898
902
|
launches one at a time, runs a load and a reset with the app stopped and relaunches it after,
|
|
899
903
|
follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
|
|
900
|
-
hook runs in a thread, which the reset and relaunch after it wait for.
|
|
904
|
+
hook runs in a thread, which the reset and relaunch after it wait for. An app whose hooks serve an
|
|
905
|
+
operation on the running app names it in `data_live` (a website browser's load sets cookies over its
|
|
906
|
+
pipe), and the base class doesn't stop it for that one; a reset after such a load that failed still
|
|
907
|
+
stops and relaunches it. `_resets_failed_add()` says whether a failed load is followed by a reset at
|
|
908
|
+
all, always by default; and `data_keeps_paths` has the server keep the app's paths as installed for a
|
|
909
|
+
reset hook of its own, which `app_data.reset(paths)` puts back, as the default reset does. The driver fronts its apps:
|
|
901
910
|
its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
|
|
902
911
|
requests to the app's server, taking the control token:
|
|
903
912
|
|
|
@@ -915,6 +924,11 @@ agents reach this way without the control token: the driver reads them from the
|
|
|
915
924
|
(`computer-app/v1`'s `agent_routes`) and lets a call under one through, while the app checks it
|
|
916
925
|
itself. A path with a `.` or `..` segment, escaped or not, never counts as under one.
|
|
917
926
|
|
|
927
|
+
Once an agent acts in the computer, by any tool call on the driver's `/mcp` or a call on a route an
|
|
928
|
+
app opens to agents, the driver writes `.agentenv/agent-acted` in each app's folder, and
|
|
929
|
+
`agent_acted()` reads it from the app's own: a fresh computer starts without it, and an app that resets
|
|
930
|
+
removes its own with `forget_agent_acted()`. agentenv's own calls, with the control token, don't count.
|
|
931
|
+
|
|
918
932
|
`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
|
|
919
933
|
env stores (`browser: true` for a website browser), and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
920
934
|
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
|
|
@@ -1016,6 +1030,26 @@ are the profile's template. The base class adds:
|
|
|
1016
1030
|
under the `write_namespace` grant when there is one, else inline, as `data/get` returns files. Inline, the whole
|
|
1017
1031
|
file is read into memory and sent base64 in one answer, so a large one belongs under a grant.
|
|
1018
1032
|
|
|
1033
|
+
The browser's data plane is a file tree, every file by its path under the session user's home:
|
|
1034
|
+
- `data/get` returns its state as `browser-state.json` (`BrowserState`: `cookies` and `local_storage`
|
|
1035
|
+
as above, `tabs`, `closed_tabs`, `downloads` and `clock`, `real` with its `now`); its profile's files,
|
|
1036
|
+
each database with its journal or log, copied in one pass with the browser paused once its state is
|
|
1037
|
+
read, so the files are of one moment, without caches (`Cache`, `Code Cache`,
|
|
1038
|
+
`GPUCache`, the shader and Dawn caches, `CacheStorage`, `ScriptCache`, `Crashpad`) or locks
|
|
1039
|
+
(`Singleton*`); and its finished downloads.
|
|
1040
|
+
- `data/add` applies such a state while the browser runs, from `browser-state.json` or a data part of
|
|
1041
|
+
its shape (`StateToLoad`: `cookies`, `local_storage` and `tabs`; what it records of the past is left),
|
|
1042
|
+
after files written by their path: profile files restore the profile to exactly them, with the
|
|
1043
|
+
browser stopped and relaunched first, and files under `~/Downloads` and `~/Uploads` land there; any
|
|
1044
|
+
other path is refused before anything is applied. A load's `tabs` open in place of the open ones,
|
|
1045
|
+
the active one in front; with none, a load that changes cookies or local storage sends the start tabs
|
|
1046
|
+
back to their `start_urls`, so a page loaded before it shows it. Once an agent has acted, a load
|
|
1047
|
+
can't change the profile or the tabs, the start tabs stay as they are, and a load that fails isn't
|
|
1048
|
+
reset: its error says what it had applied.
|
|
1049
|
+
- `data/reset` ends every CDP address, puts the profile back as installed, empties `~/Downloads` and
|
|
1050
|
+
`~/Uploads`, starts its record over and forgets that an agent acted; its data plane relaunches it at
|
|
1051
|
+
its start tabs.
|
|
1052
|
+
|
|
1019
1053
|
The record's entries are the models in `agentenv_protocol.website_browser` (`OpenTab`, with the tab that opened
|
|
1020
1054
|
it as `opener`; `ClosedTab`; `Download`; `Dialog`; `LaunchEntry`; `BrowserRecord`), every field present and null
|
|
1021
1055
|
until it's known.
|
|
@@ -829,13 +829,19 @@ in the session (`agentenv_protocol.computer.session`) is Linux's.
|
|
|
829
829
|
|
|
830
830
|
The server serves the app's data plane and its skill. With `paths` alone, `data/add` writes the files
|
|
831
831
|
it's given under them and refuses any other path, or one a link leads out of them; `data/get`
|
|
832
|
-
returns the files there, a file tree
|
|
832
|
+
returns the files there, a file tree, copied with every process of the app paused (SIGSTOP) so they
|
|
833
|
+
are of one moment, and sent from the copy once it runs again; and `data/reset` puts them back as the server found them when
|
|
833
834
|
it first started on this boot, which is the app as installed. Hooks serve what they mark instead.
|
|
834
835
|
A hook's `data/add` parts come as they were sent, inline or as a read grant, and
|
|
835
836
|
`part_bytes(part)` returns a part's bytes either way. Either way the base class takes data calls and
|
|
836
837
|
launches one at a time, runs a load and a reset with the app stopped and relaunches it after,
|
|
837
838
|
follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
|
|
838
|
-
hook runs in a thread, which the reset and relaunch after it wait for.
|
|
839
|
+
hook runs in a thread, which the reset and relaunch after it wait for. An app whose hooks serve an
|
|
840
|
+
operation on the running app names it in `data_live` (a website browser's load sets cookies over its
|
|
841
|
+
pipe), and the base class doesn't stop it for that one; a reset after such a load that failed still
|
|
842
|
+
stops and relaunches it. `_resets_failed_add()` says whether a failed load is followed by a reset at
|
|
843
|
+
all, always by default; and `data_keeps_paths` has the server keep the app's paths as installed for a
|
|
844
|
+
reset hook of its own, which `app_data.reset(paths)` puts back, as the default reset does. The driver fronts its apps:
|
|
839
845
|
its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
|
|
840
846
|
requests to the app's server, taking the control token:
|
|
841
847
|
|
|
@@ -853,6 +859,11 @@ agents reach this way without the control token: the driver reads them from the
|
|
|
853
859
|
(`computer-app/v1`'s `agent_routes`) and lets a call under one through, while the app checks it
|
|
854
860
|
itself. A path with a `.` or `..` segment, escaped or not, never counts as under one.
|
|
855
861
|
|
|
862
|
+
Once an agent acts in the computer, by any tool call on the driver's `/mcp` or a call on a route an
|
|
863
|
+
app opens to agents, the driver writes `.agentenv/agent-acted` in each app's folder, and
|
|
864
|
+
`agent_acted()` reads it from the app's own: a fresh computer starts without it, and an app that resets
|
|
865
|
+
removes its own with `forget_agent_acted()`. agentenv's own calls, with the control token, don't count.
|
|
866
|
+
|
|
856
867
|
`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
|
|
857
868
|
env stores (`browser: true` for a website browser), and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
858
869
|
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
|
|
@@ -954,6 +965,26 @@ are the profile's template. The base class adds:
|
|
|
954
965
|
under the `write_namespace` grant when there is one, else inline, as `data/get` returns files. Inline, the whole
|
|
955
966
|
file is read into memory and sent base64 in one answer, so a large one belongs under a grant.
|
|
956
967
|
|
|
968
|
+
The browser's data plane is a file tree, every file by its path under the session user's home:
|
|
969
|
+
- `data/get` returns its state as `browser-state.json` (`BrowserState`: `cookies` and `local_storage`
|
|
970
|
+
as above, `tabs`, `closed_tabs`, `downloads` and `clock`, `real` with its `now`); its profile's files,
|
|
971
|
+
each database with its journal or log, copied in one pass with the browser paused once its state is
|
|
972
|
+
read, so the files are of one moment, without caches (`Cache`, `Code Cache`,
|
|
973
|
+
`GPUCache`, the shader and Dawn caches, `CacheStorage`, `ScriptCache`, `Crashpad`) or locks
|
|
974
|
+
(`Singleton*`); and its finished downloads.
|
|
975
|
+
- `data/add` applies such a state while the browser runs, from `browser-state.json` or a data part of
|
|
976
|
+
its shape (`StateToLoad`: `cookies`, `local_storage` and `tabs`; what it records of the past is left),
|
|
977
|
+
after files written by their path: profile files restore the profile to exactly them, with the
|
|
978
|
+
browser stopped and relaunched first, and files under `~/Downloads` and `~/Uploads` land there; any
|
|
979
|
+
other path is refused before anything is applied. A load's `tabs` open in place of the open ones,
|
|
980
|
+
the active one in front; with none, a load that changes cookies or local storage sends the start tabs
|
|
981
|
+
back to their `start_urls`, so a page loaded before it shows it. Once an agent has acted, a load
|
|
982
|
+
can't change the profile or the tabs, the start tabs stay as they are, and a load that fails isn't
|
|
983
|
+
reset: its error says what it had applied.
|
|
984
|
+
- `data/reset` ends every CDP address, puts the profile back as installed, empties `~/Downloads` and
|
|
985
|
+
`~/Uploads`, starts its record over and forgets that an agent acted; its data plane relaunches it at
|
|
986
|
+
its start tabs.
|
|
987
|
+
|
|
957
988
|
The record's entries are the models in `agentenv_protocol.website_browser` (`OpenTab`, with the tab that opened
|
|
958
989
|
it as `opener`; `ClosedTab`; `Download`; `Dialog`; `LaunchEntry`; `BrowserRecord`), every field present and null
|
|
959
990
|
until it's known.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentenv-framework-protocol"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.472"
|
|
4
4
|
description = "Open data-plane protocol and server SDK for agent environments."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -56,6 +56,11 @@ macos = [
|
|
|
56
56
|
"pyobjc-framework-Quartz>=11; sys_platform == 'darwin'",
|
|
57
57
|
"pyobjc-framework-ScreenCaptureKit>=11; sys_platform == 'darwin'",
|
|
58
58
|
]
|
|
59
|
+
# A Linux driver on X11Driver: mss captures the X display and XTEST sends its input, through python-xlib.
|
|
60
|
+
x11 = [
|
|
61
|
+
"mss>=9,<11",
|
|
62
|
+
"python-xlib>=0.33,<0.34",
|
|
63
|
+
]
|
|
59
64
|
dev = [
|
|
60
65
|
"a2a-sdk[http-server]>=0.3.26,<0.4",
|
|
61
66
|
"python-multipart>=0.0.13",
|
|
@@ -111,6 +111,11 @@ class Tokens:
|
|
|
111
111
|
self._save()
|
|
112
112
|
return self._minted[user]
|
|
113
113
|
|
|
114
|
+
def revoke_all(self) -> None:
|
|
115
|
+
"""Every address minted so far ended, as a reset ends them."""
|
|
116
|
+
self._minted, self._known = {}, {}
|
|
117
|
+
self._save()
|
|
118
|
+
|
|
114
119
|
def address(self, token: str) -> Optional[Address]:
|
|
115
120
|
"""The address ``token`` was minted for, or None for one that wasn't."""
|
|
116
121
|
known = self._known.get(_hash(token))
|
|
@@ -98,16 +98,18 @@ if TYPE_CHECKING:
|
|
|
98
98
|
)
|
|
99
99
|
from .driver import DEFAULT_IDENTITY, ComputerDriverEnvironment, DriverDeclaration, computer_driver
|
|
100
100
|
from ._macos import MacOSDriver
|
|
101
|
+
from ._x11 import X11Driver
|
|
101
102
|
from .recording import RecordedFrame, RecordedSegment, RecordHints, Recording, frames_in_thread
|
|
102
103
|
|
|
103
104
|
# The names whose modules need the computer extra (mcp, av and Pillow), by module; MacOSDriver's, the macos extra too,
|
|
104
|
-
# on first use of the Mac.
|
|
105
|
+
# on first use of the Mac, and X11Driver's, the x11 extra.
|
|
105
106
|
_ON_USE = {
|
|
106
107
|
"driver": ("ComputerDriverEnvironment", "computer_driver", "DEFAULT_IDENTITY", "DriverDeclaration"),
|
|
107
108
|
"card": ("COMPUTER", "COMPUTER_READY", "COMPUTER_DISPLAY", "COMPUTER_SPACES", "COMPUTER_POINTER",
|
|
108
109
|
"COMPUTER_KEYBOARD", "COMPUTER_PAGE", "COMPUTER_EXEC", "COMPUTER_RECORDING"),
|
|
109
110
|
"recording": ("Recording", "RecordHints", "RecordedFrame", "RecordedSegment", "frames_in_thread"),
|
|
110
111
|
"_macos": ("MacOSDriver",),
|
|
112
|
+
"_x11": ("X11Driver",),
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
|
|
@@ -120,6 +122,7 @@ def __getattr__(name: str) -> Any:
|
|
|
120
122
|
|
|
121
123
|
__all__ = [
|
|
122
124
|
"ComputerDriverEnvironment", "computer_driver", "DEFAULT_IDENTITY", "DriverDeclaration", "MacOSDriver",
|
|
125
|
+
"X11Driver",
|
|
123
126
|
"ComputerAppEnvironment", "computer_app", "install", "linux", "install_step", "ready_check", "Linux", "Service",
|
|
124
127
|
"Launch", "Ready", "Packages", "AptSource", "Download", "RunScript",
|
|
125
128
|
"AppDeclaration", "AppOs",
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""computer/v1 on an X11 desktop on Linux, for the reference driver and for a driver whose software serves tools of its
|
|
2
|
+
own, as agent-desktop's does: mss captures the screen and XTEST, through python-xlib, sends the input. X calls run off
|
|
3
|
+
the event loop, one at a time, on a connection checked before each call and reopened when it drops, so the driver
|
|
4
|
+
outlives a restarted X server, and input that lets go of a button returns once the server has let go of it. It records
|
|
5
|
+
the display through FFmpeg's ``x11grab``, by PyAV. A driver subclasses ``X11Driver``, declares itself with
|
|
6
|
+
``computer_driver``, and serves its own tools beside these. Needs the ``x11`` extra: mss and python-xlib."""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from collections import OrderedDict
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any, AsyncIterator, Callable, Iterator
|
|
18
|
+
|
|
19
|
+
import av
|
|
20
|
+
import mss
|
|
21
|
+
from Xlib import X, XK
|
|
22
|
+
from Xlib import display as xdisplay
|
|
23
|
+
from Xlib.error import ConnectionClosedError, DisplayConnectionError
|
|
24
|
+
from Xlib.ext import xtest
|
|
25
|
+
|
|
26
|
+
from .driver import ComputerDriverEnvironment
|
|
27
|
+
from .errors import DeliveryFailed, DeviceUnavailable, Unsupported
|
|
28
|
+
from .operations import (Display, Image, ReadyCheck, capture, click, display, drag, key_down, key_up, pointer_down,
|
|
29
|
+
pointer_move, pointer_position, pointer_up, press, record, scroll, type_text)
|
|
30
|
+
from .platform import Platform
|
|
31
|
+
from .recording import RecordedFrame, RecordHints, Recording, frames_in_thread
|
|
32
|
+
from .settings import ComputerSettings
|
|
33
|
+
|
|
34
|
+
BUTTONS = {"left": 1, "middle": 2, "right": 3, "back": 8, "forward": 9}
|
|
35
|
+
#: The pointer state's bit for each button that has one.
|
|
36
|
+
BUTTON_MASKS = {"left": X.Button1Mask, "middle": X.Button2Mask, "right": X.Button3Mask}
|
|
37
|
+
SCROLL = {"up": 4, "down": 5, "left": 6, "right": 7}
|
|
38
|
+
#: W3C key values and their X keysym names; any other single character is typed as its own keysym.
|
|
39
|
+
KEYSYMS = {
|
|
40
|
+
"Enter": "Return", "Tab": "Tab", "Escape": "Escape", "Backspace": "BackSpace", "Delete": "Delete",
|
|
41
|
+
"ArrowUp": "Up", "ArrowDown": "Down", "ArrowLeft": "Left", "ArrowRight": "Right", "Home": "Home", "End": "End",
|
|
42
|
+
"PageUp": "Prior", "PageDown": "Next", "Insert": "Insert", "Control": "Control_L", "Shift": "Shift_L",
|
|
43
|
+
"Alt": "Alt_L", "Meta": "Super_L", "Super": "Super_L", "CapsLock": "Caps_Lock", "ContextMenu": "Menu",
|
|
44
|
+
"PrintScreen": "Print", "Pause": "Pause", " ": "space", **{f"F{n}": f"F{n}" for n in range(1, 25)},
|
|
45
|
+
}
|
|
46
|
+
TYPED = {"\n": "Return", "\t": "Tab"}
|
|
47
|
+
#: The pause between the presses of a double or triple click, in seconds.
|
|
48
|
+
CLICK_GAP_S = 0.04
|
|
49
|
+
#: How long a button's release may take to reach the app, and how often the driver looks.
|
|
50
|
+
RELEASE_WAIT_S = 5.0
|
|
51
|
+
RELEASE_POLL_S = 0.005
|
|
52
|
+
#: How far apart a drag's pointer moves are, in pixels.
|
|
53
|
+
DRAG_STEP_PX = 20
|
|
54
|
+
DROPPED = (ConnectionClosedError, DisplayConnectionError, OSError)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class X11Driver(ComputerDriverEnvironment):
|
|
58
|
+
"""The computer/v1 operations, the pointer and keyboard groups among them, on an X11 desktop."""
|
|
59
|
+
|
|
60
|
+
buttons = ("left", "right", "middle", "back", "forward")
|
|
61
|
+
max_click_count = 3
|
|
62
|
+
max_drag_points = 50
|
|
63
|
+
recording = Recording(max_fps=30)
|
|
64
|
+
|
|
65
|
+
def __init__(self, settings: ComputerSettings | None = None, platform: Platform | None = None) -> None:
|
|
66
|
+
super().__init__(settings, platform)
|
|
67
|
+
self._x: _Connection | None = None
|
|
68
|
+
self._x_lock = threading.Lock()
|
|
69
|
+
#: The keycodes each key held by @key_down pressed, which @key_up releases.
|
|
70
|
+
self._held: dict[str, list[int]] = {}
|
|
71
|
+
|
|
72
|
+
@display
|
|
73
|
+
async def screen(self) -> Display:
|
|
74
|
+
def read(c):
|
|
75
|
+
s = c.d.screen()
|
|
76
|
+
return Display(s.width_in_pixels, s.height_in_pixels, 1.0)
|
|
77
|
+
return await self._read(read)
|
|
78
|
+
|
|
79
|
+
@capture
|
|
80
|
+
async def grab(self) -> Image:
|
|
81
|
+
return await asyncio.to_thread(_grab)
|
|
82
|
+
|
|
83
|
+
@record
|
|
84
|
+
async def stream(self, hints: RecordHints) -> AsyncIterator[RecordedFrame]:
|
|
85
|
+
"""The display through FFmpeg's ``x11grab``. Its frames carry the wall clock, so each one's time is when it
|
|
86
|
+
was captured."""
|
|
87
|
+
size = await self.screen()
|
|
88
|
+
options = {"framerate": str(hints.fps), "video_size": f"{size.width}x{size.height}",
|
|
89
|
+
"draw_mouse": "1" if hints.cursor else "0"}
|
|
90
|
+
|
|
91
|
+
def frames() -> Iterator[RecordedFrame]:
|
|
92
|
+
with av.open(os.environ.get("DISPLAY", ":0"), format="x11grab", options=options) as source:
|
|
93
|
+
for frame in source.decode(video=0):
|
|
94
|
+
yield RecordedFrame(frame, datetime.fromtimestamp(frame.time, timezone.utc))
|
|
95
|
+
|
|
96
|
+
async for frame in frames_in_thread(frames):
|
|
97
|
+
yield frame
|
|
98
|
+
|
|
99
|
+
async def ready_checks(self) -> list[ReadyCheck]:
|
|
100
|
+
try:
|
|
101
|
+
has = await self._read(lambda c: c.d.has_extension("XTEST"))
|
|
102
|
+
except DeviceUnavailable as e:
|
|
103
|
+
return [ReadyCheck("xtest", False, e.message)]
|
|
104
|
+
return [ReadyCheck("xtest", bool(has), "XTEST available" if has else "XTEST missing")]
|
|
105
|
+
|
|
106
|
+
@click
|
|
107
|
+
async def click_at(self, x, y, button, count, modifiers) -> None:
|
|
108
|
+
syms = [_keysym(k) for k in modifiers]
|
|
109
|
+
|
|
110
|
+
def act(c):
|
|
111
|
+
held = c.hold(syms)
|
|
112
|
+
c.move(x, y)
|
|
113
|
+
for i in range(count):
|
|
114
|
+
c.button(button, True)
|
|
115
|
+
c.button(button, False)
|
|
116
|
+
if i + 1 < count:
|
|
117
|
+
c.d.sync()
|
|
118
|
+
time.sleep(CLICK_GAP_S)
|
|
119
|
+
c.released(button)
|
|
120
|
+
c.release(reversed(held))
|
|
121
|
+
await self._send(act)
|
|
122
|
+
|
|
123
|
+
@drag
|
|
124
|
+
async def drag_along(self, path, button, duration_ms, modifiers) -> None:
|
|
125
|
+
syms = [_keysym(k) for k in modifiers]
|
|
126
|
+
steps = _steps(path)
|
|
127
|
+
pause = duration_ms / 1000 / len(steps)
|
|
128
|
+
held: list[int] = []
|
|
129
|
+
|
|
130
|
+
def start(c):
|
|
131
|
+
held.extend(c.hold(syms))
|
|
132
|
+
c.move(*path[0])
|
|
133
|
+
c.button(button, True)
|
|
134
|
+
|
|
135
|
+
def finish(c):
|
|
136
|
+
c.button(button, False)
|
|
137
|
+
c.released(button)
|
|
138
|
+
c.release(reversed(held))
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
await self._send(start)
|
|
142
|
+
for px, py in steps:
|
|
143
|
+
await self._send(lambda c, px=px, py=py: c.move(px, py))
|
|
144
|
+
await asyncio.sleep(pause)
|
|
145
|
+
except BaseException:
|
|
146
|
+
with contextlib.suppress(Exception):
|
|
147
|
+
await self._send(finish)
|
|
148
|
+
raise
|
|
149
|
+
await self._send(finish)
|
|
150
|
+
|
|
151
|
+
@scroll
|
|
152
|
+
async def scroll_at(self, x, y, direction, amount, modifiers) -> None:
|
|
153
|
+
syms = [_keysym(k) for k in modifiers]
|
|
154
|
+
|
|
155
|
+
def act(c):
|
|
156
|
+
held = c.hold(syms)
|
|
157
|
+
c.move(x, y)
|
|
158
|
+
for _ in range(amount):
|
|
159
|
+
xtest.fake_input(c.d, X.ButtonPress, SCROLL[direction])
|
|
160
|
+
xtest.fake_input(c.d, X.ButtonRelease, SCROLL[direction])
|
|
161
|
+
c.release(reversed(held))
|
|
162
|
+
await self._send(act)
|
|
163
|
+
|
|
164
|
+
@pointer_move
|
|
165
|
+
async def move(self, x, y) -> None:
|
|
166
|
+
await self._send(lambda c: c.move(x, y))
|
|
167
|
+
|
|
168
|
+
@pointer_down
|
|
169
|
+
async def down(self, button) -> None:
|
|
170
|
+
await self._send(lambda c: c.button(button, True))
|
|
171
|
+
|
|
172
|
+
@pointer_up
|
|
173
|
+
async def up(self, button) -> None:
|
|
174
|
+
def act(c):
|
|
175
|
+
c.button(button, False)
|
|
176
|
+
c.released(button)
|
|
177
|
+
await self._send(act)
|
|
178
|
+
|
|
179
|
+
@pointer_position
|
|
180
|
+
async def where(self) -> tuple[int, int]:
|
|
181
|
+
def read(c):
|
|
182
|
+
p = c.d.screen().root.query_pointer()
|
|
183
|
+
return p.root_x, p.root_y
|
|
184
|
+
return await self._read(read)
|
|
185
|
+
|
|
186
|
+
@type_text
|
|
187
|
+
async def type_in(self, text) -> None:
|
|
188
|
+
syms = [XK.string_to_keysym(TYPED[ch]) if ch in TYPED else _keysym(ch) for ch in text]
|
|
189
|
+
await self._read(lambda c: c.check(syms))
|
|
190
|
+
for sym in syms:
|
|
191
|
+
hold = self.settings.tap_hold_ms / 1000
|
|
192
|
+
await self._send(lambda c, sym=sym: c.tap(sym, hold))
|
|
193
|
+
await asyncio.sleep(self.settings.type_delay_ms / 1000)
|
|
194
|
+
|
|
195
|
+
@press
|
|
196
|
+
async def press_keys(self, keys) -> None:
|
|
197
|
+
syms = [_keysym(k) for k in keys]
|
|
198
|
+
hold = self.settings.tap_hold_ms / 1000
|
|
199
|
+
|
|
200
|
+
def act(c):
|
|
201
|
+
pressed = c.hold(syms)
|
|
202
|
+
c.d.sync()
|
|
203
|
+
time.sleep(hold)
|
|
204
|
+
c.release(reversed(pressed))
|
|
205
|
+
await self._send(act)
|
|
206
|
+
|
|
207
|
+
@key_down
|
|
208
|
+
async def keys_down(self, keys) -> None:
|
|
209
|
+
syms = [_keysym(k) for k in keys]
|
|
210
|
+
|
|
211
|
+
def act(c):
|
|
212
|
+
for key, pressed in zip(keys, c.hold_each(syms)):
|
|
213
|
+
self._held.setdefault(key, []).extend(pressed)
|
|
214
|
+
await self._send(act)
|
|
215
|
+
|
|
216
|
+
@key_up
|
|
217
|
+
async def keys_up(self, keys) -> None:
|
|
218
|
+
def act(c):
|
|
219
|
+
for key in keys:
|
|
220
|
+
c.release(reversed(self._held.pop(key, None) or c.codes(_keysym(key))[-1:]))
|
|
221
|
+
await self._send(act)
|
|
222
|
+
|
|
223
|
+
async def _read(self, fn: Callable[[_Connection], Any]) -> Any:
|
|
224
|
+
return await asyncio.to_thread(self._x_call, fn, False)
|
|
225
|
+
|
|
226
|
+
async def _send(self, fn: Callable[[_Connection], Any]) -> Any:
|
|
227
|
+
return await asyncio.to_thread(self._x_call, fn, True)
|
|
228
|
+
|
|
229
|
+
def _x_call(self, fn: Callable[[_Connection], Any], sends_input: bool) -> Any:
|
|
230
|
+
"""``fn`` on a live connection. When the connection drops, a read is retried once on a new one; input is not
|
|
231
|
+
sent again, since some of it may have arrived."""
|
|
232
|
+
with self._x_lock:
|
|
233
|
+
for attempt in (1, 2):
|
|
234
|
+
c = self._connected()
|
|
235
|
+
try:
|
|
236
|
+
result = fn(c)
|
|
237
|
+
c.d.sync()
|
|
238
|
+
return result
|
|
239
|
+
except DROPPED as e:
|
|
240
|
+
self._x = None
|
|
241
|
+
if sends_input:
|
|
242
|
+
raise DeliveryFailed(f"the X connection dropped while sending input: {type(e).__name__}") from None
|
|
243
|
+
if attempt == 2:
|
|
244
|
+
raise DeviceUnavailable(f"the X server is unavailable: {type(e).__name__}") from None
|
|
245
|
+
|
|
246
|
+
def _connected(self) -> _Connection:
|
|
247
|
+
"""The connection, opened or reopened as needed and checked with a round trip before anything is sent."""
|
|
248
|
+
for attempt in (1, 2):
|
|
249
|
+
try:
|
|
250
|
+
if self._x is None:
|
|
251
|
+
self._x = _Connection()
|
|
252
|
+
self._held.clear()
|
|
253
|
+
self._x.d.sync()
|
|
254
|
+
return self._x
|
|
255
|
+
except DROPPED as e:
|
|
256
|
+
self._x = None
|
|
257
|
+
if attempt == 2:
|
|
258
|
+
raise DeviceUnavailable(f"the X server is unavailable: {type(e).__name__}") from None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class _Connection:
|
|
262
|
+
"""One connection to the X server, with XTEST input on it. A character the keymap lacks is typed through a spare
|
|
263
|
+
keycode mapped to it, kept for that character and reused; once all are taken, the least recently used one that
|
|
264
|
+
no key holds down is mapped again."""
|
|
265
|
+
|
|
266
|
+
def __init__(self) -> None:
|
|
267
|
+
self.d = xdisplay.Display()
|
|
268
|
+
first = self.d.display.info.min_keycode
|
|
269
|
+
mapping = self.d.get_keyboard_mapping(first, self.d.display.info.max_keycode - first + 1)
|
|
270
|
+
self.width = len(mapping[0])
|
|
271
|
+
self.spare = [first + i for i, syms in enumerate(mapping) if not any(syms)]
|
|
272
|
+
self.mapped: OrderedDict[int, int] = OrderedDict()
|
|
273
|
+
self.shift = self.d.keysym_to_keycode(XK.string_to_keysym("Shift_L"))
|
|
274
|
+
|
|
275
|
+
def move(self, x, y) -> None:
|
|
276
|
+
xtest.fake_input(self.d, X.MotionNotify, x=int(x), y=int(y))
|
|
277
|
+
|
|
278
|
+
def button(self, button: str, down: bool) -> None:
|
|
279
|
+
xtest.fake_input(self.d, X.ButtonPress if down else X.ButtonRelease, BUTTONS[button])
|
|
280
|
+
|
|
281
|
+
def released(self, button: str) -> None:
|
|
282
|
+
"""Wait, up to RELEASE_WAIT_S, until the server has let go of ``button``. A window manager's passive grab holds
|
|
283
|
+
a click on a window until it replays it to the app, while keys reach the app at once, so a key sent before then
|
|
284
|
+
overtakes the click, and so do the next click's moves."""
|
|
285
|
+
deadline = time.monotonic() + RELEASE_WAIT_S
|
|
286
|
+
while self.d.screen().root.query_pointer().mask & BUTTON_MASKS.get(button, 0) and time.monotonic() < deadline:
|
|
287
|
+
time.sleep(RELEASE_POLL_S)
|
|
288
|
+
|
|
289
|
+
def hold(self, keysyms: list[int]) -> list[int]:
|
|
290
|
+
"""``hold_each``'s keycodes, in the order they were pressed."""
|
|
291
|
+
return [code for pressed in self.hold_each(keysyms) for code in pressed]
|
|
292
|
+
|
|
293
|
+
def hold_each(self, keysyms: list[int]) -> list[list[int]]:
|
|
294
|
+
"""Press what the keysyms need that isn't down already, Shift first where a character needs it; for each
|
|
295
|
+
keysym, the keycodes this pressed, so the caller releases only those. Every keycode is found before the first
|
|
296
|
+
press, so keysyms that can't all be held at once are refused with nothing sent."""
|
|
297
|
+
down = self._down()
|
|
298
|
+
planned: list[list[int]] = []
|
|
299
|
+
for keysym in keysyms:
|
|
300
|
+
planned.append(self.codes(keysym, busy=down | {code for codes in planned for code in codes}))
|
|
301
|
+
pressed = []
|
|
302
|
+
for codes in planned:
|
|
303
|
+
pressed.append([code for code in codes if code not in down])
|
|
304
|
+
for code in pressed[-1]:
|
|
305
|
+
xtest.fake_input(self.d, X.KeyPress, code)
|
|
306
|
+
down.add(code)
|
|
307
|
+
return pressed
|
|
308
|
+
|
|
309
|
+
def release(self, codes) -> None:
|
|
310
|
+
for code in codes:
|
|
311
|
+
xtest.fake_input(self.d, X.KeyRelease, code)
|
|
312
|
+
|
|
313
|
+
def tap(self, keysym: int, hold_s: float) -> None:
|
|
314
|
+
"""Type one character: its key pressed and released, with Shift around it when it needs Shift and nothing
|
|
315
|
+
holds Shift already."""
|
|
316
|
+
down = self._down()
|
|
317
|
+
*modifiers, code = self.codes(keysym, busy=down)
|
|
318
|
+
pressed = [m for m in modifiers if m not in down]
|
|
319
|
+
for m in pressed:
|
|
320
|
+
xtest.fake_input(self.d, X.KeyPress, m)
|
|
321
|
+
xtest.fake_input(self.d, X.KeyPress, code)
|
|
322
|
+
self.d.sync()
|
|
323
|
+
time.sleep(hold_s)
|
|
324
|
+
xtest.fake_input(self.d, X.KeyRelease, code)
|
|
325
|
+
self.release(reversed(pressed))
|
|
326
|
+
|
|
327
|
+
def codes(self, keysym: int, busy: set[int] = frozenset()) -> list[int]:
|
|
328
|
+
"""The keycodes that make ``keysym``: Shift and the key when it is the key's second level, else one key; a
|
|
329
|
+
keysym off the keymap is mapped to a spare keycode not in ``busy``."""
|
|
330
|
+
codes = self._on_keymap(keysym)
|
|
331
|
+
if codes is not None:
|
|
332
|
+
return codes
|
|
333
|
+
code = self._free(busy)
|
|
334
|
+
self.d.change_keyboard_mapping(code, [(keysym,) * self.width])
|
|
335
|
+
self.d.sync()
|
|
336
|
+
self.mapped[keysym] = code
|
|
337
|
+
return [code]
|
|
338
|
+
|
|
339
|
+
def check(self, keysyms: list[int]) -> None:
|
|
340
|
+
"""Refuse, before anything is sent, text with a character off the keymap when no spare keycode is free."""
|
|
341
|
+
if any(self._on_keymap(k) is None for k in keysyms):
|
|
342
|
+
self._free(self._down(), take=False)
|
|
343
|
+
|
|
344
|
+
def _free(self, busy: set[int], take: bool = True) -> int:
|
|
345
|
+
"""A spare keycode not in ``busy``, the least recently used mapped one when none is unmapped."""
|
|
346
|
+
if self.spare:
|
|
347
|
+
return self.spare.pop() if take else self.spare[-1]
|
|
348
|
+
for keysym, code in self.mapped.items():
|
|
349
|
+
if code not in busy:
|
|
350
|
+
if take:
|
|
351
|
+
del self.mapped[keysym]
|
|
352
|
+
return code
|
|
353
|
+
raise Unsupported("no spare keycode is free for a character off the X keymap: the server has none, or keys "
|
|
354
|
+
"hold them all down")
|
|
355
|
+
|
|
356
|
+
def _on_keymap(self, keysym: int) -> list[int] | None:
|
|
357
|
+
if keysym in self.mapped:
|
|
358
|
+
self.mapped.move_to_end(keysym)
|
|
359
|
+
return [self.mapped[keysym]]
|
|
360
|
+
code = self.d.keysym_to_keycode(keysym)
|
|
361
|
+
if code and self.d.keycode_to_keysym(code, 0) == keysym:
|
|
362
|
+
return [code]
|
|
363
|
+
if code and self.d.keycode_to_keysym(code, 1) == keysym:
|
|
364
|
+
return [self.shift, code]
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
def _down(self) -> set[int]:
|
|
368
|
+
return {8 * i + bit for i, byte in enumerate(self.d.query_keymap()) for bit in range(8) if byte >> bit & 1}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _grab() -> Image:
|
|
372
|
+
try:
|
|
373
|
+
with mss.MSS() as m:
|
|
374
|
+
shot = m.grab(m.monitors[0])
|
|
375
|
+
return Image(shot.width, shot.height, bytes(shot.bgra), "BGRA")
|
|
376
|
+
except Exception as e:
|
|
377
|
+
raise DeviceUnavailable(f"capture failed: {type(e).__name__}: {e}") from None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _steps(path: list) -> list[tuple[float, float]]:
|
|
381
|
+
"""The points a drag moves through after its start, about every DRAG_STEP_PX pixels."""
|
|
382
|
+
steps = []
|
|
383
|
+
for (x0, y0), (x1, y1) in zip(path, path[1:]):
|
|
384
|
+
n = max(2, int(math.hypot(x1 - x0, y1 - y0) / DRAG_STEP_PX))
|
|
385
|
+
steps += [(x0 + (x1 - x0) * k / n, y0 + (y1 - y0) * k / n) for k in range(1, n + 1)]
|
|
386
|
+
return steps
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _keysym(key: str) -> int:
|
|
390
|
+
if key in KEYSYMS:
|
|
391
|
+
return XK.string_to_keysym(KEYSYMS[key])
|
|
392
|
+
if len(key) == 1:
|
|
393
|
+
code = ord(key)
|
|
394
|
+
return code if code < 0x100 else 0x01000000 | code
|
|
395
|
+
raise Unsupported(f"this driver has no X keysym for {key!r}")
|
|
@@ -43,6 +43,9 @@ WINDOW_KEYS = frozenset({"class", "instance", "title"})
|
|
|
43
43
|
COMPUTER_APP = "urn:agentenv:computer-app/v1"
|
|
44
44
|
#: Where an app's own server listens, in the app's folder, and where it says why it last failed to start.
|
|
45
45
|
SOCKET = Path(".agentenv/server.sock")
|
|
46
|
+
#: Written in an app's folder by its computer's driver once an agent acts there, by a tool call or on a route the app
|
|
47
|
+
#: opens to agents: a fresh computer starts without it, and an app that resets removes its own.
|
|
48
|
+
AGENT_ACTED = Path(".agentenv/agent-acted")
|
|
46
49
|
FAILURE = Path(".agentenv/server.failure")
|
|
47
50
|
#: The values of an app's build settings its env fixes in its computer's image, in the app's folder.
|
|
48
51
|
BUILD_SETTINGS = Path(".agentenv/build_settings.json")
|
|
@@ -359,6 +362,17 @@ def _named(os_types: Union[str, list[str], None]) -> Optional[list[str]]:
|
|
|
359
362
|
return None if os_types is None else [os_types] if isinstance(os_types, str) else list(os_types)
|
|
360
363
|
|
|
361
364
|
|
|
365
|
+
def agent_acted() -> bool:
|
|
366
|
+
"""Whether an agent has acted in this app's computer since the computer started or the app last reset, as its
|
|
367
|
+
driver notes; read from the app's own folder."""
|
|
368
|
+
return AGENT_ACTED.exists()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def forget_agent_acted() -> None:
|
|
372
|
+
"""Start over, as an app that has reset: no agent has acted since."""
|
|
373
|
+
AGENT_ACTED.unlink(missing_ok=True)
|
|
374
|
+
|
|
375
|
+
|
|
362
376
|
class ComputerAppEnvironment(AgentEnvEnvironment):
|
|
363
377
|
"""Subclass it, declare it with ``@computer_app`` and an OS decorator for each OS (``@linux``), and set ``skill`` as
|
|
364
378
|
a class attribute; a deploy's settings arrive as ``self.settings``, those its env fixes at build as
|
|
@@ -383,6 +397,11 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
|
|
|
383
397
|
skill: ClassVar[Optional[str]] = None
|
|
384
398
|
#: How long one of its data calls may take, a hook or its paths' default, before it fails.
|
|
385
399
|
data_timeout_s: ClassVar[float] = 300.0
|
|
400
|
+
#: The data operations its hooks serve on the running app, which its data plane neither stops it for nor
|
|
401
|
+
#: relaunches it after; and whether its server keeps its paths as installed for a reset hook of its own to put
|
|
402
|
+
#: back (``app_data.reset``), as it does for the default reset.
|
|
403
|
+
data_live: ClassVar[frozenset[str]] = frozenset()
|
|
404
|
+
data_keeps_paths: ClassVar[bool] = False
|
|
386
405
|
#: Path prefixes of its own server's routes, each ending in ``/``, that agents reach through the driver without the
|
|
387
406
|
#: computer's control token; the app checks a call to one itself.
|
|
388
407
|
agent_routes: ClassVar[tuple[str, ...]] = ()
|
|
@@ -426,7 +445,8 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
|
|
|
426
445
|
launch_running=self._launch_running())
|
|
427
446
|
self._computer_app_data = AppDataPlane(
|
|
428
447
|
self._computer_app, {op: getattr(self, name) for op, name in cls._data_hooks.items()}, self.paths,
|
|
429
|
-
cls.data_timeout_s
|
|
448
|
+
cls.data_timeout_s, live=cls.data_live, keeps_paths=cls.data_keeps_paths,
|
|
449
|
+
resets_failed_add=self._resets_failed_add)
|
|
430
450
|
|
|
431
451
|
def _launch_for(self, launch: Launch) -> Launch:
|
|
432
452
|
"""The launch its server uses, from the one declared for its OS, whose ``ready`` timeout it keeps: as
|
|
@@ -449,6 +469,10 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
|
|
|
449
469
|
"""What a launch does while it runs instead of starting a second copy: nothing, so another copy starts."""
|
|
450
470
|
return None
|
|
451
471
|
|
|
472
|
+
def _resets_failed_add(self) -> bool:
|
|
473
|
+
"""Whether a load that fails is followed by a reset, so the app isn't left half loaded: always."""
|
|
474
|
+
return True
|
|
475
|
+
|
|
452
476
|
@classmethod
|
|
453
477
|
def settings_on(cls, os_type: Optional[str]) -> type[BaseModel]:
|
|
454
478
|
"""Its ``Settings`` on ``os_type``, with the fields its OS adds."""
|