agentenv-framework-protocol 0.1.434__tar.gz → 0.1.437__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 (117) hide show
  1. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/PKG-INFO +19 -5
  2. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/README.md +18 -4
  3. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/app_data.py +4 -2
  5. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/app_runtime.py +57 -9
  6. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/dataplane.py +19 -10
  7. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/session.py +28 -1
  8. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/website_browser.py +265 -27
  9. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_dataplane.py +43 -0
  10. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_website_browser.py +175 -8
  11. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/.gitignore +0 -0
  12. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/LICENSE +0 -0
  13. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/NOTICE +0 -0
  14. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/THIRD_PARTY_NOTICES.md +0 -0
  15. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/examples/__init__.py +0 -0
  16. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/examples/advanced_agent.py +0 -0
  17. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/examples/basic_agent.py +0 -0
  18. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/examples/custom_extensions_agent.py +0 -0
  19. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/examples/multimodal_agent.py +0 -0
  20. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/examples/reference_model.py +0 -0
  21. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/examples/streaming_agent.py +0 -0
  22. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/__init__.py +0 -0
  23. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  24. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  25. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  26. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  27. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  28. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  29. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  30. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  31. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  32. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  33. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  34. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/agent_env_environment.py +0 -0
  35. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/client.py +0 -0
  36. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/__init__.py +0 -0
  37. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/_declare.py +0 -0
  38. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/_linux.py +0 -0
  39. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/_load.py +0 -0
  40. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  41. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/app.py +0 -0
  42. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/apps.py +0 -0
  43. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/card.py +0 -0
  44. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/checks.py +0 -0
  45. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  46. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  47. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  48. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  49. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  50. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  51. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  52. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/driver.py +0 -0
  53. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/errors.py +0 -0
  54. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/extensions.py +0 -0
  55. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/frames.py +0 -0
  56. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/keys.py +0 -0
  57. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/launch.py +0 -0
  58. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/manifest.py +0 -0
  59. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/operations.py +0 -0
  60. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/platform.py +0 -0
  61. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/queue.py +0 -0
  62. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/recording.py +0 -0
  63. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/results.py +0 -0
  64. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  65. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/runtime.py +0 -0
  66. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/serve.py +0 -0
  67. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/serve_app.py +0 -0
  68. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/settings.py +0 -0
  69. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/skill.py +0 -0
  70. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/spaces.py +0 -0
  71. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/computer/tools.py +0 -0
  72. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/errors.py +0 -0
  73. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/manifest.py +0 -0
  74. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/preflight.py +0 -0
  75. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/tool_access.py +0 -0
  76. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/tool_client.py +0 -0
  77. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/tool_context.py +0 -0
  78. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/transfers.py +0 -0
  79. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/src/agentenv_protocol/types.py +0 -0
  80. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/computer_fake.py +0 -0
  81. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/probe_fake.py +0 -0
  82. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_a2a_agent.py +0 -0
  83. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_a2a_agent_examples.py +0 -0
  84. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_a2a_cdp_config.py +0 -0
  85. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_a2a_object_transfers.py +0 -0
  86. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_a2a_peer_agents.py +0 -0
  87. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_a2a_proxy.py +0 -0
  88. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_a2a_staging.py +0 -0
  89. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_a2a_trajectory_log.py +0 -0
  90. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_agent_env_environment.py +0 -0
  91. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_client.py +0 -0
  92. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_app_data.py +0 -0
  93. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_app_server.py +0 -0
  94. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_conformance.py +0 -0
  95. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_conformance_checks.py +0 -0
  96. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_conformance_recording.py +0 -0
  97. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_declare.py +0 -0
  98. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_driver.py +0 -0
  99. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_extensions.py +0 -0
  100. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_frames.py +0 -0
  101. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_queue.py +0 -0
  102. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_recording.py +0 -0
  103. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_serve.py +0 -0
  104. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_computer_spaces.py +0 -0
  105. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_control_token.py +0 -0
  106. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_data_objects.py +0 -0
  107. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_errors.py +0 -0
  108. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_extension_methods.py +0 -0
  109. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_intake.py +0 -0
  110. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_manifest.py +0 -0
  111. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_preflight.py +0 -0
  112. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_tool_access.py +0 -0
  113. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_tool_annotations.py +0 -0
  114. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_tool_client.py +0 -0
  115. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_tool_context.py +0 -0
  116. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_tool_context_fastmcp.py +0 -0
  117. {agentenv_framework_protocol-0.1.434 → agentenv_framework_protocol-0.1.437}/tests/test_tool_context_injection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentenv-framework-protocol
3
- Version: 0.1.434
3
+ Version: 0.1.437
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
@@ -813,8 +813,11 @@ are the profile's template. The base class adds:
813
813
  - **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
814
814
  tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
815
815
  the first from its command line and the rest over its pipe, since headless Chromium takes one; `window`,
816
- 1280 × 800, alone only; `locale`; `time_zone`, set as `TZ` in its launch's `env`; and `geolocation` and
817
- `notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`.
816
+ 1280 × 800, alone only; `locale`; `time_zone`, set as `TZ` in its launch's `env`; `geolocation` and
817
+ `notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`; and `dialogs`, who
818
+ answers a page's JavaScript dialogs: `agent` (the default), or agentenv, accepting (`accept`, a `prompt()`
819
+ with its default text) or dismissing (`dismiss`) each. Either way agentenv leaves a page that asks before
820
+ it's left (`beforeunload`), so none of its answers blocks a navigation.
818
821
  - **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile, its
819
822
  HTTP cache under `~/.cache` (`--disk-cache-dir`), `--remote-debugging-pipe`, `--no-first-run`,
820
823
  `--no-default-browser-check`, `--lang` and, when `/dev/shm` holds under 1 GiB (Docker gives 64 MiB),
@@ -842,8 +845,19 @@ are the profile's template. The base class adds:
842
845
  launches it again, and a launch while it runs, from its menu launcher say, opens another window over
843
846
  the pipe rather than a second copy, which would hand its window to the first and exit. The ready check
844
847
  `devtools_answers` asks `Browser.getVersion` over the pipe.
845
- - **`browser/v1`** on its card, params only: `chromium` (its `version`), `clock: "real"` and, alone,
846
- `window`.
848
+ - **Its control connection**, `self.control`, a `BrowserControl` over the pipe. At each launch it attaches
849
+ to every page with flattened sessions and keeps a record, `since` its server started: open tabs in
850
+ opened order, the one whose page has focus `active`; `closed_tabs`, each with `closed_at` and
851
+ `closed_by` (`agentenv`, over this connection; `unknown`, anyone else; `exit` or `stop`, when the browser
852
+ ended on its own or its server stopped it); `downloads`, into `~/Downloads`, each with its state
853
+ and, completed, its size and sha256; and `dialogs`, each with its type, message, answer and who gave it.
854
+ `hidden_page()` gives agentenv a page of its own for reading state, in no window and never in the record.
855
+ - **`browser/v1`** on its card: `chromium` (its `version`), `clock: "real"` and, alone, `window`; and
856
+ `record` (GET), framework-only, which returns that record with `launches`, the app server's log of each
857
+ launch (with its cause: `boot`, `server_restart`, `launch`, `data/add` or `data/reset`), each stop it made
858
+ and each exit it didn't, with its exit `code` or the `signal` that ended it, since the computer booted.
859
+ Nothing relaunches a browser that exits on its own: in a computer the agent can, and alone it's an infra
860
+ failure, its ready check false.
847
861
  - **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
848
862
  `~/Downloads`, folders it makes.
849
863
 
@@ -757,8 +757,11 @@ are the profile's template. The base class adds:
757
757
  - **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
758
758
  tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
759
759
  the first from its command line and the rest over its pipe, since headless Chromium takes one; `window`,
760
- 1280 × 800, alone only; `locale`; `time_zone`, set as `TZ` in its launch's `env`; and `geolocation` and
761
- `notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`.
760
+ 1280 × 800, alone only; `locale`; `time_zone`, set as `TZ` in its launch's `env`; `geolocation` and
761
+ `notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`; and `dialogs`, who
762
+ answers a page's JavaScript dialogs: `agent` (the default), or agentenv, accepting (`accept`, a `prompt()`
763
+ with its default text) or dismissing (`dismiss`) each. Either way agentenv leaves a page that asks before
764
+ it's left (`beforeunload`), so none of its answers blocks a navigation.
762
765
  - **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile, its
763
766
  HTTP cache under `~/.cache` (`--disk-cache-dir`), `--remote-debugging-pipe`, `--no-first-run`,
764
767
  `--no-default-browser-check`, `--lang` and, when `/dev/shm` holds under 1 GiB (Docker gives 64 MiB),
@@ -786,8 +789,19 @@ are the profile's template. The base class adds:
786
789
  launches it again, and a launch while it runs, from its menu launcher say, opens another window over
787
790
  the pipe rather than a second copy, which would hand its window to the first and exit. The ready check
788
791
  `devtools_answers` asks `Browser.getVersion` over the pipe.
789
- - **`browser/v1`** on its card, params only: `chromium` (its `version`), `clock: "real"` and, alone,
790
- `window`.
792
+ - **Its control connection**, `self.control`, a `BrowserControl` over the pipe. At each launch it attaches
793
+ to every page with flattened sessions and keeps a record, `since` its server started: open tabs in
794
+ opened order, the one whose page has focus `active`; `closed_tabs`, each with `closed_at` and
795
+ `closed_by` (`agentenv`, over this connection; `unknown`, anyone else; `exit` or `stop`, when the browser
796
+ ended on its own or its server stopped it); `downloads`, into `~/Downloads`, each with its state
797
+ and, completed, its size and sha256; and `dialogs`, each with its type, message, answer and who gave it.
798
+ `hidden_page()` gives agentenv a page of its own for reading state, in no window and never in the record.
799
+ - **`browser/v1`** on its card: `chromium` (its `version`), `clock: "real"` and, alone, `window`; and
800
+ `record` (GET), framework-only, which returns that record with `launches`, the app server's log of each
801
+ launch (with its cause: `boot`, `server_restart`, `launch`, `data/add` or `data/reset`), each stop it made
802
+ and each exit it didn't, with its exit `code` or the `signal` that ended it, since the computer booted.
803
+ Nothing relaunches a browser that exits on its own: in a computer the agent can, and alone it's an infra
804
+ failure, its ready check false.
791
805
  - **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
792
806
  `~/Downloads`, folders it makes.
793
807
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.434"
3
+ version = "0.1.437"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Optional
15
15
 
16
16
  from ..agent_env_environment import OP_ADD_DATA, OP_GET_DATA, OP_RESET_DATA
17
17
  from ..transfers import WriteNamespaceGrant
18
- from ..types import FilePart, uploaded_object_path
18
+ from ..types import METHOD_ADD, METHOD_RESET, FilePart, uploaded_object_path
19
19
  from .dataplane import file_parts, part_bytes
20
20
 
21
21
  if TYPE_CHECKING:
@@ -23,6 +23,8 @@ if TYPE_CHECKING:
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
+ #: The cause a launch after each data call logs.
27
+ _CAUSES = {OP_ADD_DATA: METHOD_ADD, OP_RESET_DATA: METHOD_RESET}
26
28
  #: Where an app's server keeps its paths as installed, in the app's folder.
27
29
  INITIAL = Path(".agentenv/initial")
28
30
 
@@ -80,7 +82,7 @@ class AppDataPlane:
80
82
  raise
81
83
  finally:
82
84
  if running:
83
- await self.runtime.launch_app()
85
+ await self.runtime.launch_app(_CAUSES[op])
84
86
  except Exception as e:
85
87
  if not answer.done():
86
88
  answer.set_exception(e)
@@ -5,10 +5,13 @@ from __future__ import annotations
5
5
 
6
6
  import asyncio
7
7
  import inspect
8
+ import json
8
9
  import os
9
10
  import shlex
11
+ import signal
10
12
  import time
11
13
  from collections.abc import Awaitable, Callable
14
+ from datetime import datetime, timezone
12
15
  from pathlib import Path
13
16
  from typing import TYPE_CHECKING, Any, Optional
14
17
 
@@ -26,6 +29,11 @@ CHECK_TIMEOUT_S = 10.0
26
29
  #: Where an app's server keeps the pid of the app it launched last, in the app's folder, so a server that restarts can
27
30
  #: still stop it.
28
31
  PID = Path(".agentenv/app.pid")
32
+ #: Where an app's server logs each launch, stop and exit of the app, beside its pid; a boot starts the app's folder as
33
+ #: its image has it, so the log is the boot's.
34
+ LAUNCHES = Path(".agentenv/launches")
35
+ #: How long an app that ended on its own may take to be reaped, for its exit code.
36
+ EXIT_WAIT_S = 5.0
29
37
  #: How long a stopped app may take to exit before it's killed.
30
38
  STOP_TIMEOUT_S = 10.0
31
39
 
@@ -59,6 +67,7 @@ class AppRuntime:
59
67
  self.launch_fds = launch_fds
60
68
  self.relaunch_on_start = relaunch_on_start
61
69
  self.launch_running = launch_running
70
+ self.exits: set[asyncio.Future] = set()
62
71
  self.started = time.time()
63
72
  self.launch_failed: Optional[str] = None
64
73
  self.checking: dict[str, asyncio.Future] = {}
@@ -80,14 +89,14 @@ class AppRuntime:
80
89
  try:
81
90
  if relaunch:
82
91
  await self.stop_app()
83
- await self.launch_app()
92
+ await self.launch_app("server_restart" if relaunch else "boot")
84
93
  except Exception as e:
85
94
  self.launch_failed = f"{type(e).__name__}: {e}"
86
95
 
87
- async def launch_app(self) -> int:
96
+ async def launch_app(self, cause: str = "launch") -> int:
88
97
  """Start the app from its folder, marked as launched, with the fds the app gives it, whose copies here are
89
- closed once it has them; its pid. With ``launch_running``, an app this server launched that still runs does
90
- that instead of starting a second copy, as a browser opens another window."""
98
+ closed once it has them, and logged with ``cause``; its pid. With ``launch_running``, an app this server
99
+ launched that still runs does that instead of starting a second copy, as a browser opens another window."""
91
100
  if self.launch_running is not None and (running := _running()) is not None:
92
101
  await self.launch_running()
93
102
  return running
@@ -104,6 +113,7 @@ class AppRuntime:
104
113
  PID.write_text(str(pid))
105
114
  _mark().parent.mkdir(parents=True, exist_ok=True)
106
115
  _mark().touch()
116
+ self.log("launch", pid, cause=cause)
107
117
  return pid
108
118
 
109
119
  async def stop_app(self) -> bool:
@@ -115,9 +125,39 @@ class AppRuntime:
115
125
  PID.unlink()
116
126
  if not session.alive(pid):
117
127
  return False
128
+ self.log("stop", pid)
118
129
  await session.stop(pid, STOP_TIMEOUT_S)
119
130
  return True
120
131
 
132
+ def exited(self) -> bool:
133
+ """Log that the app this server launched last ended on its own, with its exit code or the signal that ended
134
+ it, unless the server stopped it; whether it did."""
135
+ if (pid := _recorded_pid()) is None:
136
+ return False
137
+ noting = asyncio.ensure_future(self._log_exit(pid))
138
+ self.exits.add(noting)
139
+ noting.add_done_callback(self.exits.discard)
140
+ return True
141
+
142
+ async def _log_exit(self, pid: int) -> None:
143
+ code = await session.returncode(pid, EXIT_WAIT_S)
144
+ self.log("exit", pid, **({} if code is None else
145
+ {"signal": signal.Signals(-code).name} if code < 0 else {"code": code}))
146
+
147
+ def log(self, event: str, pid: int, **fields: Any) -> None:
148
+ """Append ``event`` for the app's ``pid`` to its launch log, which outlives its server for the boot."""
149
+ entry = {"at": datetime.now(timezone.utc).isoformat(), "event": event, "pid": pid, **fields}
150
+ _log().parent.mkdir(parents=True, exist_ok=True)
151
+ with _log().open("a", encoding="utf-8") as log:
152
+ log.write(json.dumps(entry) + "\n")
153
+
154
+ def launches(self) -> list[dict]:
155
+ """Every launch, stop and exit of the app since the computer booted, in order."""
156
+ try:
157
+ return [json.loads(line) for line in _log().read_text(encoding="utf-8").splitlines() if line]
158
+ except FileNotFoundError:
159
+ return []
160
+
121
161
  async def readiness(self) -> AppReadiness:
122
162
  ready = self.launch.ready if self.launch is not None else None
123
163
  checks: list[tuple[str, Awaitable[Optional[str]]]] = []
@@ -154,6 +194,17 @@ def _mark() -> Path:
154
194
  return LAUNCHED / Path.cwd().name
155
195
 
156
196
 
197
+ def _log() -> Path:
198
+ return LAUNCHES
199
+
200
+
201
+ def _recorded_pid() -> Optional[int]:
202
+ try:
203
+ return int(PID.read_text())
204
+ except (FileNotFoundError, ValueError):
205
+ return None
206
+
207
+
157
208
  def _launched_at() -> Optional[float]:
158
209
  """When the app was last launched since the computer booted, by its mark."""
159
210
  try:
@@ -164,11 +215,8 @@ def _launched_at() -> Optional[float]:
164
215
 
165
216
  def _running() -> Optional[int]:
166
217
  """The pid of the app this server launched last, while it runs."""
167
- try:
168
- pid = int(PID.read_text())
169
- except (FileNotFoundError, ValueError):
170
- return None
171
- return pid if session.alive(pid) else None
218
+ pid = _recorded_pid()
219
+ return pid if pid is not None and session.alive(pid) else None
172
220
 
173
221
 
174
222
  async def _bounded(name: str, check: Awaitable[Optional[str]]) -> Optional[str]:
@@ -15,7 +15,7 @@ import httpx
15
15
  from pydantic import TypeAdapter, ValidationError
16
16
 
17
17
  from ..agent_env_environment import add_data, get_data
18
- from ..transfers import NamespaceUploader, ReadObject, RelativePath, WriteNamespaceGrant, download
18
+ from ..transfers import NamespaceUploader, ReadObject, RelativePath, TransferError, WriteNamespaceGrant, download
19
19
  from ..types import DATA_OBJECTS_EXTENSION_URI, FilePart, FileWithBytes, FileWithUri, uploaded_file_part, uploaded_object_path
20
20
 
21
21
  if TYPE_CHECKING:
@@ -64,22 +64,31 @@ class DataPlane:
64
64
  async def file_parts(home: Path, paths: list[str], write_namespace: Optional[WriteNamespaceGrant],
65
65
  leaving_out: list[str] = ()) -> list:
66
66
  """A part for each file under ``paths`` of ``home`` but under none of ``leaving_out``, naming its path relative to
67
- ``home``: uploaded under ``write_namespace`` when there is one, else inline."""
67
+ ``home``: uploaded under ``write_namespace`` when there is one, else inline. A file a running app removes before it's
68
+ read is left out."""
68
69
  files = [(relative, source) for relative, source in _data_files(home, paths)
69
70
  if not any(PurePosixPath(relative).is_relative_to(owned) for owned in leaving_out)]
71
+ parts = []
70
72
  if write_namespace is not None:
71
73
  uploader = NamespaceUploader(write_namespace)
72
- parts = []
73
74
  for relative, source in files:
74
- await uploader.upload(relative, source)
75
+ try:
76
+ await uploader.upload(relative, source)
77
+ except (TransferError, OSError):
78
+ if source.exists():
79
+ raise
80
+ continue
75
81
  parts.append(uploaded_file_part(relative, name=source.name, mime_type=_mime(source)))
76
82
  return parts
77
- return [
78
- FilePart(file=FileWithBytes(bytes=base64.b64encode(source.read_bytes()).decode("ascii"),
79
- name=source.name, mimeType=_mime(source)),
80
- metadata={DATA_OBJECTS_EXTENSION_URI: {"path": relative}})
81
- for relative, source in files
82
- ]
83
+ for relative, source in files:
84
+ try:
85
+ data = source.read_bytes()
86
+ except FileNotFoundError:
87
+ continue
88
+ parts.append(FilePart(file=FileWithBytes(bytes=base64.b64encode(data).decode("ascii"), name=source.name,
89
+ mimeType=_mime(source)),
90
+ metadata={DATA_OBJECTS_EXTENSION_URI: {"path": relative}}))
91
+ return parts
83
92
 
84
93
 
85
94
  async def part_bytes(part: FilePart) -> bytes:
@@ -17,6 +17,10 @@ POLL_S = 0.2
17
17
  WITH_FDS = Path(__file__).with_name("_with_fds.py")
18
18
  #: The processes launched into the session, kept until they exit so each is reaped.
19
19
  _running: set[asyncio.Task] = set()
20
+ #: Each launched process's reaping, by pid, until its exit is asked for: its return code, negative for a signal.
21
+ _exits: dict[int, asyncio.Future] = {}
22
+ #: How many exits no one has asked for are kept, the latest.
23
+ KEPT_EXITS = 16
20
24
 
21
25
 
22
26
  async def wait_until_up() -> None:
@@ -38,12 +42,35 @@ async def launch(argv: list[str], cwd: Path, env: dict[str, str], fds: Optional[
38
42
  reaped = asyncio.ensure_future(process.wait())
39
43
  _running.add(reaped)
40
44
  reaped.add_done_callback(_running.discard)
45
+ _exits[process.pid] = reaped
46
+ reaped.add_done_callback(lambda _: _forget_old_exits())
41
47
  return process.pid
42
48
 
43
49
 
50
+ def _forget_old_exits() -> None:
51
+ """Drop all but the latest ``KEPT_EXITS`` exits no one has asked for."""
52
+ for pid in [pid for pid, reaped in _exits.items() if reaped.done()][:-KEPT_EXITS]:
53
+ del _exits[pid]
54
+
55
+
56
+ async def returncode(pid: int, timeout_s: float) -> Optional[int]:
57
+ """The return code the process ``launch`` started as ``pid`` exited with, negative for the signal that ended it,
58
+ once it's reaped within ``timeout_s``; None for one ``launch`` didn't start or that hasn't exited."""
59
+ reaped = _exits.get(pid)
60
+ if reaped is None:
61
+ return None
62
+ try:
63
+ code = await asyncio.wait_for(asyncio.shield(reaped), timeout_s)
64
+ except TimeoutError:
65
+ return None
66
+ _exits.pop(pid, None)
67
+ return code
68
+
69
+
44
70
  async def stop(pid: int, timeout_s: float) -> None:
45
71
  """End the session ``launch`` started as ``pid``, every process in it: asked to exit, then killed after
46
- ``timeout_s``."""
72
+ ``timeout_s``. No one asks how it exited once it's stopped."""
73
+ _exits.pop(pid, None)
47
74
  try:
48
75
  os.killpg(pid, signal.SIGTERM)
49
76
  except ProcessLookupError: