agentenv-framework-protocol 0.1.456__tar.gz → 0.1.459__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.456 → agentenv_framework_protocol-0.1.459}/PKG-INFO +32 -1
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/README.md +31 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/recording.py +23 -3
- agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/__init__.py +41 -0
- agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/clock.py +142 -0
- agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/conformance.py +358 -0
- agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/environment.py +431 -0
- agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/errors.py +39 -0
- agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/readiness.py +81 -0
- agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/routes.py +397 -0
- agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/session.py +55 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_recording.py +21 -0
- agentenv_framework_protocol-0.1.459/tests/test_website_api.py +453 -0
- agentenv_framework_protocol-0.1.459/tests/test_website_conformance.py +331 -0
- agentenv_framework_protocol-0.1.459/tests/test_website_environment.py +720 -0
- agentenv_framework_protocol-0.1.459/tests/test_website_readiness.py +212 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/_with_fds.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/app.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/app_data.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/app_runtime.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/apps.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/driver.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/seed.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/session.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/svc.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/website_browser.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_cdp_config.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_app_server.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_conformance_page.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_page.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_seed.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_svc_forward.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_context_injection.py +0 -0
- {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_website_browser.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.459
|
|
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
|
|
@@ -490,6 +490,37 @@ async with ToolSession(mcp_url=mcp_url, role="alice@example.com") as session:
|
|
|
490
490
|
- **Tools:** `list_tools()` returns the tools the role may use as `EnvironmentTool`, the type a card's
|
|
491
491
|
`capabilities.tools` holds. Each `inputSchema` is the JSON Schema a model API takes as the tool's parameters.
|
|
492
492
|
|
|
493
|
+
## Website readiness
|
|
494
|
+
|
|
495
|
+
`agentenv_protocol.website` is the one definition of whether a website env is healthy. A site's card advertises
|
|
496
|
+
`urn:agentenv:website/v1`'s `ready` (`WEBSITE_READY_OP`), a GET answering a `WebsiteReadiness`: `ready`, a boolean,
|
|
497
|
+
`problems`, what keeps it from being ready, and optionally `waited_s` since its server started and the `timeout_s` it
|
|
498
|
+
may take (`SITE_READY_TIMEOUT_S` when it says nothing), which is how long a deploy waits for it. A
|
|
499
|
+
`WebsiteEnvironment` answers from its `@ready_check` methods, run at once, each within `CHECK_TIMEOUT_S` and a plain
|
|
500
|
+
`def` in a worker thread, and from `@website(ready_timeout_s=...)`; it serves no separate health path. The model is
|
|
501
|
+
strict about `ready` and `problems` (`"yes"` is no `ready`) and ignores fields it doesn't know, so a reader keeps
|
|
502
|
+
reading a newer site. A server whose card has no `ready` is still read at its health path (`DEFAULT_HEALTH_PATH`
|
|
503
|
+
unless it names one), which is deprecated. Everything that judges a site reads it through these helpers: agent-env's deploy,
|
|
504
|
+
`DeployedEnv.readiness()`, and the conformance checker's `health`, which fails an answer that isn't a
|
|
505
|
+
`WebsiteReadiness`.
|
|
506
|
+
|
|
507
|
+
```python
|
|
508
|
+
from agentenv_protocol.website import env_readiness, website_readiness
|
|
509
|
+
|
|
510
|
+
site = await website_readiness(base_url, card) # WebsiteReadiness, or None for a card without ready
|
|
511
|
+
env = await env_readiness(gateway_url, env_card) # WebsiteReadiness of every site of the env
|
|
512
|
+
print(env.ready, env.problems) # False ['shop: seeded']
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
- **One site:** `website_readiness(base_url, card, *, timeout=READY_TIMEOUT_S, headers=None)` asks the site as its card
|
|
516
|
+
advertises `ready`. It raises httpx's error when the site can't be reached, `ExtensionCallError` for a non-2xx
|
|
517
|
+
answer and `ValueError` (pydantic's `ValidationError`) for an answer that isn't a `WebsiteReadiness`.
|
|
518
|
+
- **An env:** `env_readiness(base_url, card, ...)` asks, at once, the card itself when it advertises `ready` (a single
|
|
519
|
+
site's card), and every child under a composed card's `children_environments` that does, its endpoints rooted at
|
|
520
|
+
`base_url` as a gateway's are. A child's problems are prefixed with its name (`shop: seeded`); one that can't be
|
|
521
|
+
asked, or answers no `WebsiteReadiness`, is a problem itself. A card or child without `ready`, an MCP server's or an
|
|
522
|
+
older site's, isn't counted, so an env with no site serving it is ready with no problems.
|
|
523
|
+
|
|
493
524
|
## Computer drivers
|
|
494
525
|
|
|
495
526
|
A computer driver is what runs inside a computer an agent uses through screenshots and input. Write
|
|
@@ -431,6 +431,37 @@ async with ToolSession(mcp_url=mcp_url, role="alice@example.com") as session:
|
|
|
431
431
|
- **Tools:** `list_tools()` returns the tools the role may use as `EnvironmentTool`, the type a card's
|
|
432
432
|
`capabilities.tools` holds. Each `inputSchema` is the JSON Schema a model API takes as the tool's parameters.
|
|
433
433
|
|
|
434
|
+
## Website readiness
|
|
435
|
+
|
|
436
|
+
`agentenv_protocol.website` is the one definition of whether a website env is healthy. A site's card advertises
|
|
437
|
+
`urn:agentenv:website/v1`'s `ready` (`WEBSITE_READY_OP`), a GET answering a `WebsiteReadiness`: `ready`, a boolean,
|
|
438
|
+
`problems`, what keeps it from being ready, and optionally `waited_s` since its server started and the `timeout_s` it
|
|
439
|
+
may take (`SITE_READY_TIMEOUT_S` when it says nothing), which is how long a deploy waits for it. A
|
|
440
|
+
`WebsiteEnvironment` answers from its `@ready_check` methods, run at once, each within `CHECK_TIMEOUT_S` and a plain
|
|
441
|
+
`def` in a worker thread, and from `@website(ready_timeout_s=...)`; it serves no separate health path. The model is
|
|
442
|
+
strict about `ready` and `problems` (`"yes"` is no `ready`) and ignores fields it doesn't know, so a reader keeps
|
|
443
|
+
reading a newer site. A server whose card has no `ready` is still read at its health path (`DEFAULT_HEALTH_PATH`
|
|
444
|
+
unless it names one), which is deprecated. Everything that judges a site reads it through these helpers: agent-env's deploy,
|
|
445
|
+
`DeployedEnv.readiness()`, and the conformance checker's `health`, which fails an answer that isn't a
|
|
446
|
+
`WebsiteReadiness`.
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
from agentenv_protocol.website import env_readiness, website_readiness
|
|
450
|
+
|
|
451
|
+
site = await website_readiness(base_url, card) # WebsiteReadiness, or None for a card without ready
|
|
452
|
+
env = await env_readiness(gateway_url, env_card) # WebsiteReadiness of every site of the env
|
|
453
|
+
print(env.ready, env.problems) # False ['shop: seeded']
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
- **One site:** `website_readiness(base_url, card, *, timeout=READY_TIMEOUT_S, headers=None)` asks the site as its card
|
|
457
|
+
advertises `ready`. It raises httpx's error when the site can't be reached, `ExtensionCallError` for a non-2xx
|
|
458
|
+
answer and `ValueError` (pydantic's `ValidationError`) for an answer that isn't a `WebsiteReadiness`.
|
|
459
|
+
- **An env:** `env_readiness(base_url, card, ...)` asks, at once, the card itself when it advertises `ready` (a single
|
|
460
|
+
site's card), and every child under a composed card's `children_environments` that does, its endpoints rooted at
|
|
461
|
+
`base_url` as a gateway's are. A child's problems are prefixed with its name (`shop: seeded`); one that can't be
|
|
462
|
+
asked, or answers no `WebsiteReadiness`, is a problem itself. A card or child without `ready`, an MCP server's or an
|
|
463
|
+
older site's, isn't counted, so an env with no site serving it is ready with no problems.
|
|
464
|
+
|
|
434
465
|
## Computer drivers
|
|
435
466
|
|
|
436
467
|
A computer driver is what runs inside a computer an agent uses through screenshots and input. Write
|
|
@@ -52,6 +52,8 @@ FAILING_S = 60.0
|
|
|
52
52
|
GAP_S = 2.0
|
|
53
53
|
#: How long a stream that stopped or failed waits before it is started again.
|
|
54
54
|
RETRY_S = 1.0
|
|
55
|
+
#: How long a recording's start waits for its first frame, so a recording a start answered for is recording.
|
|
56
|
+
FIRST_FRAME_S = 5.0
|
|
55
57
|
#: How often free disk is checked while frames arrive.
|
|
56
58
|
DISK_CHECK_S = 5.0
|
|
57
59
|
KEEP_ENDED = 64
|
|
@@ -127,8 +129,9 @@ def recording_params(recording: Recording, streams: bool) -> dict:
|
|
|
127
129
|
|
|
128
130
|
|
|
129
131
|
class Recordings:
|
|
130
|
-
"""The recordings of one driver: started by request id,
|
|
131
|
-
ones kept with their manifests so a repeated ``end`` answers the
|
|
132
|
+
"""The recordings of one driver: started by request id, each start answering once its recording is recording, open
|
|
133
|
+
ones counted against ``max_concurrent``, ended ones kept with their manifests so a repeated ``end`` answers the
|
|
134
|
+
same."""
|
|
132
135
|
|
|
133
136
|
def __init__(self, runtime: ComputerRuntime) -> None:
|
|
134
137
|
self.runtime = runtime
|
|
@@ -144,7 +147,9 @@ class Recordings:
|
|
|
144
147
|
raise InvalidArgument("request_id is a non-empty string")
|
|
145
148
|
async with self._lock:
|
|
146
149
|
if request_id in self.started:
|
|
147
|
-
|
|
150
|
+
answer = self.started[request_id]
|
|
151
|
+
await self._live(self.running.get(answer["recording_id"]))
|
|
152
|
+
return answer
|
|
148
153
|
hints, grant = self._hints(features, scope, window, fps, max_edge, cursor, upload)
|
|
149
154
|
stopped = [rid for rid, r in self.running.items() if r.done]
|
|
150
155
|
for rid in stopped[:max(0, len(stopped) - KEEP_ENDED)]: # stopped by itself and never ended
|
|
@@ -164,8 +169,16 @@ class Recordings:
|
|
|
164
169
|
self.started[request_id] = answer
|
|
165
170
|
while len(self.started) > KEEP_STARTED:
|
|
166
171
|
self.started.popitem(last=False)
|
|
172
|
+
await self._live(recording)
|
|
167
173
|
return answer
|
|
168
174
|
|
|
175
|
+
@staticmethod
|
|
176
|
+
async def _live(recording: Optional[_Recording]) -> None:
|
|
177
|
+
"""Wait up to FIRST_FRAME_S for ``recording``, one still running, to have its first frame."""
|
|
178
|
+
if recording is not None:
|
|
179
|
+
with contextlib.suppress(TimeoutError):
|
|
180
|
+
await asyncio.wait_for(recording.live.wait(), FIRST_FRAME_S)
|
|
181
|
+
|
|
169
182
|
async def end(self, recording_id: str, upload: Optional[dict], reason: str) -> dict:
|
|
170
183
|
if reason not in REASONS:
|
|
171
184
|
raise InvalidArgument(f"reason is one of {list(REASONS)}, got {reason!r}")
|
|
@@ -219,6 +232,7 @@ class _Recording:
|
|
|
219
232
|
self.ended_by: Optional[str] = None
|
|
220
233
|
self.error: Optional[str] = None
|
|
221
234
|
self.done = False
|
|
235
|
+
self.live = asyncio.Event()
|
|
222
236
|
self.task: Optional[asyncio.Task] = None
|
|
223
237
|
self.timer: Optional[asyncio.TimerHandle] = None
|
|
224
238
|
self.released: Optional[asyncio.Future] = None
|
|
@@ -228,7 +242,11 @@ class _Recording:
|
|
|
228
242
|
self._last_disk_check = 0.0
|
|
229
243
|
|
|
230
244
|
async def run(self) -> None:
|
|
245
|
+
"""Frames or segments from the source, recorded until the recording ends; ``live`` once it's recording: a
|
|
246
|
+
stream from its start, the fallback's polling from its first frame, which a busy computer is slow to take."""
|
|
231
247
|
failing_since: Optional[float] = None
|
|
248
|
+
if self.streams:
|
|
249
|
+
self.live.set()
|
|
232
250
|
try:
|
|
233
251
|
while True:
|
|
234
252
|
try:
|
|
@@ -255,6 +273,7 @@ class _Recording:
|
|
|
255
273
|
await self._close()
|
|
256
274
|
finally:
|
|
257
275
|
self.done = True
|
|
276
|
+
self.live.set()
|
|
258
277
|
self.released = asyncio.ensure_future(self._release())
|
|
259
278
|
|
|
260
279
|
def expire(self) -> None:
|
|
@@ -315,6 +334,7 @@ class _Recording:
|
|
|
315
334
|
path = self.hints.directory / f"{len(self.segments):04d}.mp4"
|
|
316
335
|
self.encoder = await self._in_thread(_Encoder, path, frame.width, frame.height, self.hints.fps, at)
|
|
317
336
|
await self._in_thread(self.encoder.add, frame, at)
|
|
337
|
+
self.live.set()
|
|
318
338
|
self._check_disk()
|
|
319
339
|
|
|
320
340
|
async def _add_segment(self, item: RecordedSegment) -> None:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""The website SDK: a site's server, meeting ``urn:agentenv:website/v1``. Subclass WebsiteEnvironment, declare the
|
|
2
|
+
site with ``@website(domains=[...], static="dist/")``, mark its API with ``@api.get/post/put/patch/delete(path)``, and
|
|
3
|
+
``serve()`` it; it needs only the plain package, and uvicorn to serve. The class is optional: a server in any language
|
|
4
|
+
that meets the contract works, which ``python -m agentenv_protocol.website.conformance <url>`` checks. Whether a site
|
|
5
|
+
is ready is its ``WebsiteReadiness`` answer to ``ready``, which ``website_readiness`` reads of one site and
|
|
6
|
+
``env_readiness`` of every site of an env. What a task left a site with is its data plane's ``data/get``."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .clock import CLOCK_EXTENSION_URI, UNIVERSE_NOW_HEADER, ClockUnavailable, UniverseClock
|
|
11
|
+
from .environment import (
|
|
12
|
+
SESSION_PATH,
|
|
13
|
+
WebsiteDeclaration,
|
|
14
|
+
WebsiteEnvironment,
|
|
15
|
+
host_problem,
|
|
16
|
+
ready_check,
|
|
17
|
+
website,
|
|
18
|
+
)
|
|
19
|
+
from .errors import ApiError, BadRequest, Conflict, Forbidden, NotFound, Unauthorized
|
|
20
|
+
from .readiness import (
|
|
21
|
+
DEFAULT_HEALTH_PATH,
|
|
22
|
+
READY_TIMEOUT_S,
|
|
23
|
+
SITE_READY_TIMEOUT_S,
|
|
24
|
+
WEBSITE_EXTENSION_URI,
|
|
25
|
+
WEBSITE_READY_OP,
|
|
26
|
+
WebsiteReadiness,
|
|
27
|
+
env_readiness,
|
|
28
|
+
website_readiness,
|
|
29
|
+
)
|
|
30
|
+
from .routes import ROLE_KEY_ENV, ROLE_KEY_HEADER, ApiContext, ApiRoute, api, reserved_path
|
|
31
|
+
from .session import SESSION_COOKIE, SESSION_SECRET_ENV, SessionSigner
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"WebsiteEnvironment", "WebsiteDeclaration", "website", "api", "ApiContext", "ApiRoute", "ready_check", "host_problem",
|
|
35
|
+
"ApiError", "BadRequest", "Unauthorized", "Forbidden", "NotFound", "Conflict",
|
|
36
|
+
"UniverseClock", "ClockUnavailable", "SessionSigner", "reserved_path",
|
|
37
|
+
"WEBSITE_EXTENSION_URI", "CLOCK_EXTENSION_URI", "UNIVERSE_NOW_HEADER", "DEFAULT_HEALTH_PATH", "SESSION_PATH",
|
|
38
|
+
"SESSION_COOKIE", "SESSION_SECRET_ENV", "ROLE_KEY_ENV", "ROLE_KEY_HEADER",
|
|
39
|
+
"WEBSITE_READY_OP", "READY_TIMEOUT_S", "SITE_READY_TIMEOUT_S", "WebsiteReadiness",
|
|
40
|
+
"website_readiness", "env_readiness",
|
|
41
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""The universe's clock as a site reads it, the way MCP servers read it: ``clock/v1``'s ``sync_time`` hands the server
|
|
2
|
+
the gateway's ``/clock/time`` URL, and each request then reads that clock's ``{"virtual_time": <RFC3339>}`` once,
|
|
3
|
+
the one instant everything the request does reads (``now()``) and its answer carries as ``X-Universe-Now``, RFC3339 in
|
|
4
|
+
UTC. Requests that arrive while a read is in flight share it, over one pooled connection, so a burst costs the gateway
|
|
5
|
+
one read. An unsynced site reads wall time, and so does a synced one whose gateway clock isn't armed (404); one whose clock
|
|
6
|
+
can't be read raises ``ClockUnavailable``. ``sync_time`` with an empty URL goes back to wall time. ``sync_time`` may
|
|
7
|
+
also name the universe's time zone, an IANA name, which the clock keeps for the site to show local times in; the time
|
|
8
|
+
itself stays UTC."""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import contextvars
|
|
13
|
+
import datetime
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
CLOCK_EXTENSION_URI = "urn:agentenv:clock/v1"
|
|
20
|
+
UNIVERSE_NOW_HEADER = "X-Universe-Now"
|
|
21
|
+
READ_TIMEOUT = httpx.Timeout(connect=0.5, read=1.0, write=1.0, pool=0.5)
|
|
22
|
+
READ_ATTEMPTS = 2
|
|
23
|
+
|
|
24
|
+
_FRACTION = re.compile(r"(\.\d+)(?=[+-]\d\d:?\d\d$|Z$|$)")
|
|
25
|
+
_request_now: contextvars.ContextVar[datetime.datetime | None] = contextvars.ContextVar("universe_now", default=None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ClockUnavailable(Exception):
|
|
29
|
+
"""The synced clock couldn't be read: unreachable, a 5xx after a retry, or an answer that isn't a time."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def iso(moment: datetime.datetime) -> str:
|
|
33
|
+
return moment.astimezone(datetime.timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_time(value: str) -> datetime.datetime:
|
|
37
|
+
"""An RFC3339 time, its fraction of a second of any length (Python 3.10 parses only 3 or 6 digits)."""
|
|
38
|
+
value = _FRACTION.sub(lambda m: m.group(1)[:7].ljust(7, "0"), value.replace("Z", "+00:00"))
|
|
39
|
+
moment = datetime.datetime.fromisoformat(value)
|
|
40
|
+
return moment if moment.tzinfo else moment.replace(tzinfo=datetime.timezone.utc)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _wall() -> datetime.datetime:
|
|
44
|
+
return datetime.datetime.now(datetime.timezone.utc)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UniverseClock:
|
|
48
|
+
"""The time a site answers with: wall time until ``sync`` names a gateway clock, that clock's, read per request,
|
|
49
|
+
after."""
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.url: str | None = None
|
|
53
|
+
self.timezone: str | None = None
|
|
54
|
+
self._last: datetime.datetime | None = None
|
|
55
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
56
|
+
self._client: httpx.AsyncClient | None = None
|
|
57
|
+
self._reading: tuple[str, asyncio.Future] | None = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def synced(self) -> bool:
|
|
61
|
+
return self.url is not None
|
|
62
|
+
|
|
63
|
+
def now(self) -> datetime.datetime:
|
|
64
|
+
"""The instant the request being answered read, else the last one read, else wall time."""
|
|
65
|
+
return _request_now.get() or self._last or _wall()
|
|
66
|
+
|
|
67
|
+
async def read(self) -> datetime.datetime:
|
|
68
|
+
"""The universe's time now: the synced clock's, wall time when none is synced or it isn't armed; raises
|
|
69
|
+
``ClockUnavailable`` when it can't be read."""
|
|
70
|
+
if self.url is None:
|
|
71
|
+
return _wall()
|
|
72
|
+
self._last = await self._shared_read(self.url) or _wall()
|
|
73
|
+
return self._last
|
|
74
|
+
|
|
75
|
+
def answering_at(self, moment: datetime.datetime) -> contextvars.Token:
|
|
76
|
+
"""Make ``moment`` what ``now()`` reads while the current request is answered."""
|
|
77
|
+
return _request_now.set(moment)
|
|
78
|
+
|
|
79
|
+
def answered(self, token: contextvars.Token) -> None:
|
|
80
|
+
_request_now.reset(token)
|
|
81
|
+
|
|
82
|
+
async def sync(self, url: str, timezone: str | None = None) -> datetime.datetime:
|
|
83
|
+
"""Follow the clock at ``url`` from now on, in ``timezone`` (none when the sync names none), or wall time again
|
|
84
|
+
for an empty ``url``; raises ``ClockUnavailable`` when the clock can't be read, keeping the clock and zone it
|
|
85
|
+
followed."""
|
|
86
|
+
if not url:
|
|
87
|
+
self.url, self.timezone, self._last, self._reading = None, None, None, None
|
|
88
|
+
return _wall()
|
|
89
|
+
virtual = await _read(self._pooled(), url) or _wall()
|
|
90
|
+
self.url, self.timezone, self._last, self._reading = url, timezone, virtual, None
|
|
91
|
+
return virtual
|
|
92
|
+
|
|
93
|
+
async def _shared_read(self, url: str) -> datetime.datetime | None:
|
|
94
|
+
"""The clock at ``url``'s time, from the read already in flight for it when there is one, else a new one."""
|
|
95
|
+
client = self._pooled()
|
|
96
|
+
if self._reading is None or self._reading[0] != url or self._reading[1].done():
|
|
97
|
+
reading = asyncio.ensure_future(_read(client, url))
|
|
98
|
+
reading.add_done_callback(lambda done: done.cancelled() or done.exception())
|
|
99
|
+
self._reading = (url, reading)
|
|
100
|
+
return await asyncio.shield(self._reading[1])
|
|
101
|
+
|
|
102
|
+
def _pooled(self) -> httpx.AsyncClient:
|
|
103
|
+
"""The client this event loop reads the clock with, kept so its connections are reused."""
|
|
104
|
+
loop = asyncio.get_running_loop()
|
|
105
|
+
if self._loop is not loop or self._client is None:
|
|
106
|
+
self._loop, self._client, self._reading = loop, httpx.AsyncClient(timeout=READ_TIMEOUT), None
|
|
107
|
+
return self._client
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _read(client: httpx.AsyncClient, url: str) -> datetime.datetime | None:
|
|
111
|
+
"""The clock at ``url``'s time, None when it isn't armed (404); a transport error or a 5xx is tried again once."""
|
|
112
|
+
failure: Exception | None = None
|
|
113
|
+
for _attempt in range(READ_ATTEMPTS):
|
|
114
|
+
try:
|
|
115
|
+
response = await client.get(url)
|
|
116
|
+
except httpx.TransportError as e:
|
|
117
|
+
failure = e
|
|
118
|
+
continue
|
|
119
|
+
if response.status_code == 404:
|
|
120
|
+
return None
|
|
121
|
+
if response.status_code >= 500:
|
|
122
|
+
failure = httpx.HTTPStatusError(f"{response.status_code}", request=response.request, response=response)
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
response.raise_for_status()
|
|
126
|
+
return parse_time(response.json()["virtual_time"])
|
|
127
|
+
except (httpx.HTTPStatusError, ValueError, KeyError, TypeError) as e:
|
|
128
|
+
raise ClockUnavailable(f"the universe's clock at {url} answered {response.status_code}: {e}") from e
|
|
129
|
+
raise ClockUnavailable(f"the universe's clock at {url} can't be read: {type(failure).__name__}: {failure}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def clock_patch_script(now: datetime.datetime) -> bytes:
|
|
133
|
+
"""An inline script that shifts the page's ``Date`` to the universe's time ``now``, and follows the
|
|
134
|
+
``X-Universe-Now`` header of every ``fetch`` the page makes after. The ``Date`` it puts in place answers called,
|
|
135
|
+
with the time's string, as constructed, and keeps ``Date.parse``, ``Date.UTC`` and ``instanceof Date``."""
|
|
136
|
+
return ("<script>(function(){var D=Date,o=%s-D.now();function n(){return D.now()+o}"
|
|
137
|
+
"function U(){if(!new.target)return new D(n()).toString();"
|
|
138
|
+
"return Reflect.construct(D,arguments.length?[].slice.call(arguments):[n()],new.target)}"
|
|
139
|
+
"Object.setPrototypeOf(U,D);U.prototype=D.prototype;D.prototype.constructor=U;U.now=n;U.parse=D.parse;"
|
|
140
|
+
"U.UTC=D.UTC;window.Date=U;var f=window.fetch;if(f)window.fetch=function(){return f.apply(this,arguments)"
|
|
141
|
+
".then(function(r){var t=D.parse(r.headers.get(%s)||'');if(!isNaN(t))o=t-D.now();return r})}})();"
|
|
142
|
+
"</script>" % (int(now.timestamp() * 1000), json.dumps(UNIVERSE_NOW_HEADER))).encode()
|