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.
Files changed (138) hide show
  1. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/PKG-INFO +32 -1
  2. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/README.md +31 -0
  3. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/recording.py +23 -3
  5. agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/__init__.py +41 -0
  6. agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/clock.py +142 -0
  7. agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/conformance.py +358 -0
  8. agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/environment.py +431 -0
  9. agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/errors.py +39 -0
  10. agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/readiness.py +81 -0
  11. agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/routes.py +397 -0
  12. agentenv_framework_protocol-0.1.459/src/agentenv_protocol/website/session.py +55 -0
  13. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_recording.py +21 -0
  14. agentenv_framework_protocol-0.1.459/tests/test_website_api.py +453 -0
  15. agentenv_framework_protocol-0.1.459/tests/test_website_conformance.py +331 -0
  16. agentenv_framework_protocol-0.1.459/tests/test_website_environment.py +720 -0
  17. agentenv_framework_protocol-0.1.459/tests/test_website_readiness.py +212 -0
  18. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/.gitignore +0 -0
  19. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/LICENSE +0 -0
  20. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/NOTICE +0 -0
  21. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/THIRD_PARTY_NOTICES.md +0 -0
  22. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/__init__.py +0 -0
  23. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/advanced_agent.py +0 -0
  24. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/basic_agent.py +0 -0
  25. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/custom_extensions_agent.py +0 -0
  26. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/multimodal_agent.py +0 -0
  27. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/reference_model.py +0 -0
  28. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/examples/streaming_agent.py +0 -0
  29. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/__init__.py +0 -0
  30. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  31. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  32. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  33. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  34. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  35. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  36. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  37. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  38. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  39. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  40. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  41. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/agent_env_environment.py +0 -0
  42. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/browser_cdp.py +0 -0
  43. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/client.py +0 -0
  44. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/__init__.py +0 -0
  45. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/_declare.py +0 -0
  46. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/_linux.py +0 -0
  47. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/_load.py +0 -0
  48. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  49. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/app.py +0 -0
  50. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/app_data.py +0 -0
  51. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/app_runtime.py +0 -0
  52. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/apps.py +0 -0
  53. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/card.py +0 -0
  54. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/checks.py +0 -0
  55. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  56. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  57. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  58. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  59. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  60. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  61. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
  62. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  63. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/dataplane.py +0 -0
  64. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/driver.py +0 -0
  65. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/errors.py +0 -0
  66. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/extensions.py +0 -0
  67. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/frames.py +0 -0
  68. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/keys.py +0 -0
  69. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/launch.py +0 -0
  70. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/manifest.py +0 -0
  71. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/operations.py +0 -0
  72. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/platform.py +0 -0
  73. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/queue.py +0 -0
  74. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/results.py +0 -0
  75. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  76. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/runtime.py +0 -0
  77. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/seed.py +0 -0
  78. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/serve.py +0 -0
  79. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/serve_app.py +0 -0
  80. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/session.py +0 -0
  81. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/settings.py +0 -0
  82. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/skill.py +0 -0
  83. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/spaces.py +0 -0
  84. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/svc.py +0 -0
  85. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/computer/tools.py +0 -0
  86. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/errors.py +0 -0
  87. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/manifest.py +0 -0
  88. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/preflight.py +0 -0
  89. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/tool_access.py +0 -0
  90. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/tool_client.py +0 -0
  91. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/tool_context.py +0 -0
  92. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/transfers.py +0 -0
  93. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/types.py +0 -0
  94. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/src/agentenv_protocol/website_browser.py +0 -0
  95. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/computer_fake.py +0 -0
  96. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/probe_fake.py +0 -0
  97. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_agent.py +0 -0
  98. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_agent_examples.py +0 -0
  99. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_cdp_config.py +0 -0
  100. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_object_transfers.py +0 -0
  101. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_peer_agents.py +0 -0
  102. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_proxy.py +0 -0
  103. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_staging.py +0 -0
  104. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_a2a_trajectory_log.py +0 -0
  105. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_agent_env_environment.py +0 -0
  106. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_browser_cdp.py +0 -0
  107. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_client.py +0 -0
  108. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_app_data.py +0 -0
  109. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_app_server.py +0 -0
  110. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_conformance.py +0 -0
  111. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_conformance_checks.py +0 -0
  112. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_conformance_page.py +0 -0
  113. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_conformance_recording.py +0 -0
  114. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_dataplane.py +0 -0
  115. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_declare.py +0 -0
  116. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_driver.py +0 -0
  117. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_extensions.py +0 -0
  118. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_frames.py +0 -0
  119. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_page.py +0 -0
  120. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_queue.py +0 -0
  121. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_seed.py +0 -0
  122. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_serve.py +0 -0
  123. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_spaces.py +0 -0
  124. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_computer_svc_forward.py +0 -0
  125. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_control_token.py +0 -0
  126. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_data_objects.py +0 -0
  127. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_errors.py +0 -0
  128. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_extension_methods.py +0 -0
  129. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_intake.py +0 -0
  130. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_manifest.py +0 -0
  131. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_preflight.py +0 -0
  132. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_access.py +0 -0
  133. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_annotations.py +0 -0
  134. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_client.py +0 -0
  135. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_context.py +0 -0
  136. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_context_fastmcp.py +0 -0
  137. {agentenv_framework_protocol-0.1.456 → agentenv_framework_protocol-0.1.459}/tests/test_tool_context_injection.py +0 -0
  138. {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.456
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.456"
3
+ version = "0.1.459"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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, open ones counted against ``max_concurrent``, ended
131
- ones kept with their manifests so a repeated ``end`` answers the same."""
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
- return self.started[request_id]
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()