b3-prosaic-runtime 0.9.0__tar.gz → 0.9.2__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 (163) hide show
  1. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/CHANGELOG.md +18 -1
  2. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/PKG-INFO +43 -1
  3. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/README.md +42 -0
  4. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/pyproject.toml +1 -1
  5. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/scripts/conversation_wheel_smoke.py +1 -1
  6. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/scripts/production_wheel_smoke.py +1 -1
  7. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/scripts/release_artifacts.py +10 -2
  8. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/b3_prosaic_runtime.egg-info/PKG-INFO +43 -1
  9. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/b3_prosaic_runtime.egg-info/SOURCES.txt +4 -0
  10. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/__init__.py +7 -1
  11. b3_prosaic_runtime-0.9.2/src/prosaic_runtime/adapter.py +200 -0
  12. b3_prosaic_runtime-0.9.2/src/prosaic_runtime/execution_budget.py +108 -0
  13. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/operation_context.py +23 -1
  14. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/runtime.py +33 -9
  15. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/tools.py +2 -2
  16. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_conformance.py +1 -1
  17. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_distribution_metadata.py +1 -1
  18. b3_prosaic_runtime-0.9.2/tests/test_execution_budget.py +128 -0
  19. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_package_compatibility.py +2 -2
  20. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_release_artifacts.py +20 -0
  21. b3_prosaic_runtime-0.9.2/tests/test_run_adapter.py +126 -0
  22. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/LICENSE +0 -0
  23. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/LICENSE-MIT +0 -0
  24. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/MANIFEST.in +0 -0
  25. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/NOTICE.md +0 -0
  26. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/accounting.md +0 -0
  27. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/adr-cli-sandbox.md +0 -0
  28. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/cli-tools.md +0 -0
  29. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/conformance.md +0 -0
  30. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/conversations.md +0 -0
  31. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/package-index.md +0 -0
  32. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/production-acceptance.md +0 -0
  33. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/release-0.7.1.md +0 -0
  34. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/security.md +0 -0
  35. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/structured-tools.md +0 -0
  36. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/telemetry.md +0 -0
  37. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/tool-effects.md +0 -0
  38. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/docs/workflow-factory-runtime-implementation.md +0 -0
  39. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/acquire-pilot.md +0 -0
  40. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/acquired-reviewer.md +0 -0
  41. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/catalog-reader.md +0 -0
  42. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/cli-spec-reviewer.md +0 -0
  43. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/launch-analyst.md +0 -0
  44. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/launch-briefer.md +0 -0
  45. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/launch-decision.md +0 -0
  46. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/launch-skeptic.md +0 -0
  47. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/native-summarizer.md +0 -0
  48. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/preloaded-reviewer.md +0 -0
  49. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/reviewer.md +0 -0
  50. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/subagents/summarizer.md +0 -0
  51. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/.prosaic/tools/analyze-spec.yml +0 -0
  52. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/README.md +0 -0
  53. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/anthropic.yml +0 -0
  54. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/catalog.json +0 -0
  55. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/catalog_tools.py +0 -0
  56. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/cli-tool/prosaic_example_analyzer.py +0 -0
  57. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/cli-tool/pyproject.toml +0 -0
  58. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/cli-tools-sandboxed.yml +0 -0
  59. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/cli-tools.yml +0 -0
  60. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/custom-tools.yml +0 -0
  61. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/evidence/launch/brief.md +0 -0
  62. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/evidence/launch/field-notes.md +0 -0
  63. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/evidence/launch/metrics.md +0 -0
  64. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/evidence/pilot.md +0 -0
  65. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/evidence/requirements.md +0 -0
  66. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/prosaic-runtime.yaml +0 -0
  67. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/run_acquisition.py +0 -0
  68. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/run_cli_tool.py +0 -0
  69. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/run_custom_tool.py +0 -0
  70. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/run_examples.py +0 -0
  71. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/run_sandbox_probe.py +0 -0
  72. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/run_tiers.py +0 -0
  73. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/tokenproxy.yml +0 -0
  74. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/understanding/.prosaic/subagents/reviewer.md +0 -0
  75. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/understanding/.prosaic/tools/understanding.yml +0 -0
  76. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/understanding/runtime.yml +0 -0
  77. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/examples/with-tools.yml +0 -0
  78. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/scripts/wheel_smoke.py +0 -0
  79. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/setup.cfg +0 -0
  80. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/b3_prosaic_runtime.egg-info/dependency_links.txt +0 -0
  81. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/b3_prosaic_runtime.egg-info/entry_points.txt +0 -0
  82. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/b3_prosaic_runtime.egg-info/requires.txt +0 -0
  83. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/b3_prosaic_runtime.egg-info/top_level.txt +0 -0
  84. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/accounting.py +0 -0
  85. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/accounting_capture.py +0 -0
  86. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/admission.py +0 -0
  87. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/anthropic.py +0 -0
  88. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/anthropic_stream.py +0 -0
  89. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/anthropic_usage.py +0 -0
  90. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/artifacts.py +0 -0
  91. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/budgets.py +0 -0
  92. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/cli.py +0 -0
  93. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/cli_tools.py +0 -0
  94. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/config.py +0 -0
  95. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/conformance.py +0 -0
  96. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/console.py +0 -0
  97. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/conversation.py +0 -0
  98. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/conversation_preparation.py +0 -0
  99. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/diagnostics.py +0 -0
  100. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/events.py +0 -0
  101. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/execution.py +0 -0
  102. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/http_bounds.py +0 -0
  103. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/openai_compatible.py +0 -0
  104. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/openai_compatible_compaction.py +0 -0
  105. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/openai_compatible_filters.py +0 -0
  106. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/openai_compatible_progress.py +0 -0
  107. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/openai_compatible_transcript.py +0 -0
  108. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/policy.py +0 -0
  109. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/provider_types.py +0 -0
  110. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/sandbox.py +0 -0
  111. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/smoke_prose/pilot.md +0 -0
  112. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/smoke_prose/subagents/native-summarizer.md +0 -0
  113. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/smoke_prose/subagents/reviewer.md +0 -0
  114. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/smoke_prose/subagents/summarizer.md +0 -0
  115. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/structured_tools.py +0 -0
  116. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/telemetry.py +0 -0
  117. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/tool_effects.py +0 -0
  118. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/tool_registry.py +0 -0
  119. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/turn_capture.py +0 -0
  120. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/src/prosaic_runtime/types.py +0 -0
  121. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/support/conversation_server.py +0 -0
  122. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/support/tool_journal.py +0 -0
  123. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_accounting.py +0 -0
  124. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_acquisition.py +0 -0
  125. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_anthropic.py +0 -0
  126. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_anthropic_accounting.py +0 -0
  127. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_anthropic_package.py +0 -0
  128. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_anthropic_stream.py +0 -0
  129. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_artifact_admission.py +0 -0
  130. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_cli_sandbox.py +0 -0
  131. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_cli_tool_example.py +0 -0
  132. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_cli_tools.py +0 -0
  133. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_cli_tools_boundaries.py +0 -0
  134. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_config.py +0 -0
  135. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_conversation_bounds.py +0 -0
  136. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_conversation_contracts.py +0 -0
  137. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_conversation_distribution.py +0 -0
  138. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_conversation_failure_evidence.py +0 -0
  139. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_conversation_review_regressions.py +0 -0
  140. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_conversation_tool_batches.py +0 -0
  141. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_conversation_turns.py +0 -0
  142. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_custom_tool_example.py +0 -0
  143. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_custom_tool_transport.py +0 -0
  144. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_custom_tools.py +0 -0
  145. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_features.py +0 -0
  146. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_http_boundaries.py +0 -0
  147. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_input_boundary.py +0 -0
  148. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_invocation_budgets.py +0 -0
  149. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_production_smoke.py +0 -0
  150. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_prompt_injection.py +0 -0
  151. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_provider_execution.py +0 -0
  152. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_read_provenance.py +0 -0
  153. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_redirect_safety.py +0 -0
  154. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_runtime.py +0 -0
  155. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_sample_program.py +0 -0
  156. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_sandbox_probe_example.py +0 -0
  157. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_structured_tools.py +0 -0
  158. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_telemetry.py +0 -0
  159. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_tier_examples.py +0 -0
  160. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_tool_effect_workers.py +0 -0
  161. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_tool_effects.py +0 -0
  162. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_transport.py +0 -0
  163. {b3_prosaic_runtime-0.9.0 → b3_prosaic_runtime-0.9.2}/tests/test_usage_integrity.py +0 -0
@@ -1,6 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.9.2 — 2026-10-13
4
+
5
+ - Add a host-owned ExecutionBudget that shares deadlines, provider/tool dispatch
6
+ allowances and known/unknown reported-token evidence across nested calls/retries.
7
+ - Preserve trusted parent accounting context in tool callbacks and expose bounded
8
+ child lineage and remaining-time helpers without placing attribution in prompts.
9
+ - Qualify SDK and existing PostgreSQL recorder compatibility; publish only the SDK.
10
+
11
+ ## 0.9.1 — 2026-10-13
12
+
13
+ - Add versioned `RuntimeAdapter` for trusted host projections and normalization
14
+ around an unmodified Runtime. Execution callbacks can only reduce authority.
15
+ - Share deadline, request/tool allowances and reported-token evidence across all
16
+ adapter attempts; preserve known partial and unknown usage and callback failures.
17
+ - Ordinary execution, conversation APIs, accounting and PostgreSQL remain unchanged.
18
+ - Publish only the SDK distribution; retain adapter compatibility qualification.
19
+
20
+ ## Unreleased historical candidates
4
21
 
5
22
  - Prepare Runtime 0.8.1 and the paired PostgreSQL recorder 0.2.1 patch candidates.
6
23
  - Recheck cancellation and absolute deadlines after observer/accounting preparation,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3-prosaic-runtime
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Bounded execution of neutral Prosaic agents on OpenAI-compatible and native Anthropic endpoints
5
5
  Author: B3 Cognition
6
6
  License-Expression: Apache-2.0
@@ -653,3 +653,45 @@ For endpoints where the host deliberately chooses JSON tool requests, use the
653
653
  shared [`StructuredToolLoop`](docs/structured-tools.md). It reuses registered
654
654
  `CustomTool` contracts and bounded execution, with application-owned final-result
655
655
  admission. Harness can drive each turn without embedding product rules in Runtime.
656
+
657
+ ### Shared budgets for nested host execution (Runtime 0.9.2)
658
+
659
+ Use `ExecutionBudget` around a complete host operation to share one absolute
660
+ monotonic deadline, provider/tool dispatch caps and an optional reported-token cap
661
+ across `run()`, `turn()`, RuntimeAdapter retries and synchronous nested callbacks:
662
+
663
+ ```python
664
+ from prosaic_runtime import ExecutionBudget
665
+
666
+ budget = ExecutionBudget(timeout_s=30, max_provider_requests=50,
667
+ max_tool_calls=1, max_reported_tokens=40960,
668
+ cancelled=request_cancelled)
669
+ with budget.activate():
670
+ reply = conversation.send(...)
671
+ ```
672
+
673
+ Reactivation never replenishes counters or the deadline. Nested budget scopes
674
+ must satisfy every enclosing allowance. Provider/tool attempts are reserved
675
+ atomically; explicit worker threads must activate the same budget object because
676
+ context variables are not automatically inherited by threads. This cooperative
677
+ host contract does not sandbox or preempt trusted callbacks. Callback networking
678
+ must narrow its own timeout to the supplied deadline and check cancellation.
679
+
680
+ `ToolExecutionContext.accounting_context` holds the actual frozen resolved parent
681
+ attribution when accounting is enabled. `child_context(invocation_id=..., run_id=...,
682
+ step_id=...)` derives nested lineage without taking identities from model arguments.
683
+ `remaining_timeout(maximum)` narrows a callback timeout to the original deadline.
684
+ Attribution is private host context and must not be copied into prompts/results.
685
+
686
+ Known reported tokens remain available through `budget.reported_tokens`; unknown
687
+ usage remains unknown through `budget.usage_complete`. A configured token cap
688
+ stops subsequent dispatch on unknown usage. A response can itself exceed a
689
+ reported-token cap; its paid evidence is retained and the operation fails rather
690
+ than pretending the provider call was prevented. This is not a currency cap.
691
+
692
+ ALWAYS persist host operation identity and conservative remaining reservations
693
+ before dispatch when crash recovery matters. NEVER treat this process-local
694
+ budget as a durable ledger or reset uncertain allowances after a restart.
695
+ PostgreSQL accounting remains the authoritative provider-call evidence. Saved
696
+ host outcomes require a fenced ToolJournal or equivalent durable application
697
+ repository; interrupted effects must not be repeated automatically.
@@ -631,3 +631,45 @@ For endpoints where the host deliberately chooses JSON tool requests, use the
631
631
  shared [`StructuredToolLoop`](docs/structured-tools.md). It reuses registered
632
632
  `CustomTool` contracts and bounded execution, with application-owned final-result
633
633
  admission. Harness can drive each turn without embedding product rules in Runtime.
634
+
635
+ ### Shared budgets for nested host execution (Runtime 0.9.2)
636
+
637
+ Use `ExecutionBudget` around a complete host operation to share one absolute
638
+ monotonic deadline, provider/tool dispatch caps and an optional reported-token cap
639
+ across `run()`, `turn()`, RuntimeAdapter retries and synchronous nested callbacks:
640
+
641
+ ```python
642
+ from prosaic_runtime import ExecutionBudget
643
+
644
+ budget = ExecutionBudget(timeout_s=30, max_provider_requests=50,
645
+ max_tool_calls=1, max_reported_tokens=40960,
646
+ cancelled=request_cancelled)
647
+ with budget.activate():
648
+ reply = conversation.send(...)
649
+ ```
650
+
651
+ Reactivation never replenishes counters or the deadline. Nested budget scopes
652
+ must satisfy every enclosing allowance. Provider/tool attempts are reserved
653
+ atomically; explicit worker threads must activate the same budget object because
654
+ context variables are not automatically inherited by threads. This cooperative
655
+ host contract does not sandbox or preempt trusted callbacks. Callback networking
656
+ must narrow its own timeout to the supplied deadline and check cancellation.
657
+
658
+ `ToolExecutionContext.accounting_context` holds the actual frozen resolved parent
659
+ attribution when accounting is enabled. `child_context(invocation_id=..., run_id=...,
660
+ step_id=...)` derives nested lineage without taking identities from model arguments.
661
+ `remaining_timeout(maximum)` narrows a callback timeout to the original deadline.
662
+ Attribution is private host context and must not be copied into prompts/results.
663
+
664
+ Known reported tokens remain available through `budget.reported_tokens`; unknown
665
+ usage remains unknown through `budget.usage_complete`. A configured token cap
666
+ stops subsequent dispatch on unknown usage. A response can itself exceed a
667
+ reported-token cap; its paid evidence is retained and the operation fails rather
668
+ than pretending the provider call was prevented. This is not a currency cap.
669
+
670
+ ALWAYS persist host operation identity and conservative remaining reservations
671
+ before dispatch when crash recovery matters. NEVER treat this process-local
672
+ budget as a durable ledger or reset uncertain allowances after a restart.
673
+ PostgreSQL accounting remains the authoritative provider-call evidence. Saved
674
+ host outcomes require a fenced ToolJournal or equivalent durable application
675
+ repository; interrupted effects must not be repeated automatically.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "b3-prosaic-runtime"
7
- version = "0.9.0"
7
+ version = "0.9.2"
8
8
  description = "Bounded execution of neutral Prosaic agents on OpenAI-compatible and native Anthropic endpoints"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -22,7 +22,7 @@ def ownership():
22
22
  import prosaic_runtime as api
23
23
  import prosaic_runtime_postgres as recorder
24
24
  for module, distribution, version in ((prosaic, 'b3-prosaic', '0.4.1'),
25
- (api, 'b3-prosaic-runtime', '0.9.0'),
25
+ (api, 'b3-prosaic-runtime', '0.9.2'),
26
26
  (recorder, 'b3-prosaic-runtime-postgres', '0.3.0')):
27
27
  owner = metadata.distribution(distribution)
28
28
  assert module.__version__ == owner.version == version
@@ -49,7 +49,7 @@ def installed_origins():
49
49
  owners = {}
50
50
  origins = {}
51
51
  for package, name, version in ((prosaic, "b3-prosaic", "0.4.1"),
52
- (runtime, "b3-prosaic-runtime", "0.9.0"),
52
+ (runtime, "b3-prosaic-runtime", "0.9.2"),
53
53
  (postgres, "b3-prosaic-runtime-postgres", "0.3.0")):
54
54
  owner = metadata.distribution(name)
55
55
  assert owner.version == package.__version__ == version
@@ -443,10 +443,16 @@ def fetch_public(url):
443
443
  raise
444
444
 
445
445
 
446
- def check_index(directory, commit, *, allow_missing=False, fetch=fetch_public):
446
+ def check_index(directory, commit, *, allow_missing=False, fetch=fetch_public, distributions=None):
447
447
  receipt = verify_receipt(directory, commit)
448
+ available = {item['name'] for item in receipt['distributions']}
449
+ selected = available if distributions is None else set(distributions)
450
+ if not selected or not selected.issubset(available):
451
+ raise ReleaseError('invalid index distribution selection')
448
452
  missing = False
449
453
  for distribution in receipt['distributions']:
454
+ if distribution['name'] not in selected:
455
+ continue
450
456
  name, version = distribution['name'], distribution['version']
451
457
  if not re.fullmatch('[A-Za-z0-9_.-]+', name) or not re.fullmatch('[A-Za-z0-9_.+-]+', version):
452
458
  raise ReleaseError('invalid index package identity')
@@ -561,6 +567,7 @@ def main():
561
567
  command.add_argument('--require', action='append', required=True)
562
568
  elif action == 'index-check':
563
569
  command.add_argument('--allow-missing', action='store_true')
570
+ command.add_argument('--distribution', action='append')
564
571
  args = parser.parse_args()
565
572
  if args.action == 'build':
566
573
  expected = dict(value.split('=', 1) for value in args.expect)
@@ -574,7 +581,8 @@ def main():
574
581
  command = args.command[1:] if args.command[:1] == ['--'] else args.command
575
582
  result = qualify(args.directory, args.commit, args.label, command, junit=args.junit)
576
583
  elif args.action == 'index-check':
577
- result = check_index(args.directory, args.commit, allow_missing=args.allow_missing)
584
+ result = check_index(args.directory, args.commit, allow_missing=args.allow_missing,
585
+ distributions=args.distribution)
578
586
  else:
579
587
  result = require_qualification(args.directory, args.commit, args.require)
580
588
  print(json.dumps(result, sort_keys=True))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3-prosaic-runtime
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Bounded execution of neutral Prosaic agents on OpenAI-compatible and native Anthropic endpoints
5
5
  Author: B3 Cognition
6
6
  License-Expression: Apache-2.0
@@ -653,3 +653,45 @@ For endpoints where the host deliberately chooses JSON tool requests, use the
653
653
  shared [`StructuredToolLoop`](docs/structured-tools.md). It reuses registered
654
654
  `CustomTool` contracts and bounded execution, with application-owned final-result
655
655
  admission. Harness can drive each turn without embedding product rules in Runtime.
656
+
657
+ ### Shared budgets for nested host execution (Runtime 0.9.2)
658
+
659
+ Use `ExecutionBudget` around a complete host operation to share one absolute
660
+ monotonic deadline, provider/tool dispatch caps and an optional reported-token cap
661
+ across `run()`, `turn()`, RuntimeAdapter retries and synchronous nested callbacks:
662
+
663
+ ```python
664
+ from prosaic_runtime import ExecutionBudget
665
+
666
+ budget = ExecutionBudget(timeout_s=30, max_provider_requests=50,
667
+ max_tool_calls=1, max_reported_tokens=40960,
668
+ cancelled=request_cancelled)
669
+ with budget.activate():
670
+ reply = conversation.send(...)
671
+ ```
672
+
673
+ Reactivation never replenishes counters or the deadline. Nested budget scopes
674
+ must satisfy every enclosing allowance. Provider/tool attempts are reserved
675
+ atomically; explicit worker threads must activate the same budget object because
676
+ context variables are not automatically inherited by threads. This cooperative
677
+ host contract does not sandbox or preempt trusted callbacks. Callback networking
678
+ must narrow its own timeout to the supplied deadline and check cancellation.
679
+
680
+ `ToolExecutionContext.accounting_context` holds the actual frozen resolved parent
681
+ attribution when accounting is enabled. `child_context(invocation_id=..., run_id=...,
682
+ step_id=...)` derives nested lineage without taking identities from model arguments.
683
+ `remaining_timeout(maximum)` narrows a callback timeout to the original deadline.
684
+ Attribution is private host context and must not be copied into prompts/results.
685
+
686
+ Known reported tokens remain available through `budget.reported_tokens`; unknown
687
+ usage remains unknown through `budget.usage_complete`. A configured token cap
688
+ stops subsequent dispatch on unknown usage. A response can itself exceed a
689
+ reported-token cap; its paid evidence is retained and the operation fails rather
690
+ than pretending the provider call was prevented. This is not a currency cap.
691
+
692
+ ALWAYS persist host operation identity and conservative remaining reservations
693
+ before dispatch when crash recovery matters. NEVER treat this process-local
694
+ budget as a durable ledger or reset uncertain allowances after a restart.
695
+ PostgreSQL accounting remains the authoritative provider-call evidence. Saved
696
+ host outcomes require a fenced ToolJournal or equivalent durable application
697
+ repository; interrupted effects must not be repeated automatically.
@@ -70,6 +70,7 @@ src/b3_prosaic_runtime.egg-info/top_level.txt
70
70
  src/prosaic_runtime/__init__.py
71
71
  src/prosaic_runtime/accounting.py
72
72
  src/prosaic_runtime/accounting_capture.py
73
+ src/prosaic_runtime/adapter.py
73
74
  src/prosaic_runtime/admission.py
74
75
  src/prosaic_runtime/anthropic.py
75
76
  src/prosaic_runtime/anthropic_stream.py
@@ -86,6 +87,7 @@ src/prosaic_runtime/conversation_preparation.py
86
87
  src/prosaic_runtime/diagnostics.py
87
88
  src/prosaic_runtime/events.py
88
89
  src/prosaic_runtime/execution.py
90
+ src/prosaic_runtime/execution_budget.py
89
91
  src/prosaic_runtime/http_bounds.py
90
92
  src/prosaic_runtime/openai_compatible.py
91
93
  src/prosaic_runtime/openai_compatible_compaction.py
@@ -132,6 +134,7 @@ tests/test_custom_tool_example.py
132
134
  tests/test_custom_tool_transport.py
133
135
  tests/test_custom_tools.py
134
136
  tests/test_distribution_metadata.py
137
+ tests/test_execution_budget.py
135
138
  tests/test_features.py
136
139
  tests/test_http_boundaries.py
137
140
  tests/test_input_boundary.py
@@ -143,6 +146,7 @@ tests/test_provider_execution.py
143
146
  tests/test_read_provenance.py
144
147
  tests/test_redirect_safety.py
145
148
  tests/test_release_artifacts.py
149
+ tests/test_run_adapter.py
146
150
  tests/test_runtime.py
147
151
  tests/test_sample_program.py
148
152
  tests/test_sandbox_probe_example.py
@@ -1,5 +1,5 @@
1
1
  """Execute neutral Prosaic artifacts with explicitly granted authority."""
2
- __version__ = '0.9.0'
2
+ __version__ = '0.9.2'
3
3
 
4
4
  from .artifacts import ProsaicArtifact
5
5
  from .config import EndpointConfig, RuntimeConfig, RunLimits, CliSandboxConfig
@@ -30,3 +30,9 @@ from .conversation import (ConversationMessage, CompletedTurn, TurnEvidence,
30
30
  TurnResult, TurnLimits, PreparedTurn, TurnAdmissionError)
31
31
  __all__ += ["ConversationMessage", "CompletedTurn", "TurnEvidence", "TurnResult",
32
32
  "TurnLimits", "PreparedTurn", "TurnAdmissionError"]
33
+
34
+ from .adapter import RuntimeAdapter
35
+ __all__ += ["RuntimeAdapter"]
36
+
37
+ from .execution_budget import ExecutionBudget
38
+ __all__ += ["ExecutionBudget"]
@@ -0,0 +1,200 @@
1
+ """Trusted host adaptation around an unmodified Runtime and one shared allowance.
2
+
3
+ ALWAYS dispatch through execute and retain every attempt's evidence.
4
+ NEVER broaden host policy or manufacture successful execution from a failed call.
5
+ """
6
+ from copy import deepcopy
7
+ from dataclasses import dataclass, replace
8
+ import hashlib
9
+ import math
10
+ import re
11
+ import time
12
+
13
+ from .runtime import ProsaicRuntime
14
+ from .policy import RunPolicy
15
+ from .types import Result
16
+ from .budgets import InvocationBudgetExceeded
17
+
18
+
19
+ class AdapterBoundary(Exception):
20
+ def __init__(self, reason):
21
+ self.reason = reason
22
+ super().__init__(reason)
23
+
24
+
25
+ class _Attempts:
26
+ def __init__(self, policy, maximum, deadline, cancelled):
27
+ self.policy, self.maximum, self.deadline, self.cancelled = policy, maximum, deadline, cancelled
28
+ self.records = []
29
+ self.requests = self.tools = self.tokens = 0
30
+ self.complete = True
31
+ self.cost = 0.0
32
+ self.last = None
33
+
34
+ def remaining(self, timeout_s=None, allowed_tools=None, max_tool_rounds=None, max_input_bytes=None):
35
+ if self.cancelled():
36
+ raise AdapterBoundary('adapter_cancelled')
37
+ remaining = self.deadline - time.monotonic()
38
+ if remaining <= 0:
39
+ raise AdapterBoundary('adapter_timeout')
40
+ if len(self.records) >= self.maximum:
41
+ raise AdapterBoundary('adapter_attempt_limit')
42
+ policy = self.policy
43
+ values = {}
44
+ for name, consumed in (('max_provider_requests', self.requests), ('max_tool_calls', self.tools), ('max_reported_tokens', self.tokens)):
45
+ cap = getattr(policy, name)
46
+ if cap is not None:
47
+ if name == 'max_reported_tokens' and not self.complete:
48
+ raise InvocationBudgetExceeded('usage_unknown')
49
+ if consumed >= cap and name != 'max_tool_calls':
50
+ raise InvocationBudgetExceeded('token_limit' if name == 'max_reported_tokens' else 'provider_request_limit')
51
+ values[name] = max(0, cap - consumed)
52
+ if allowed_tools is not None:
53
+ grant = frozenset(allowed_tools)
54
+ if grant - policy.allowed_tools:
55
+ raise ValueError('adapter cannot broaden tool grants')
56
+ values['allowed_tools'] = grant
57
+ for name, value in (('max_tool_rounds', max_tool_rounds), ('max_input_bytes', max_input_bytes)):
58
+ if value is not None:
59
+ if type(value) is not int or value <= 0:
60
+ raise ValueError('adapter bounds must be positive integers')
61
+ values[name] = min(getattr(policy, name), value)
62
+ if timeout_s is not None:
63
+ if not isinstance(timeout_s, (float, int)) or isinstance(timeout_s, bool) or not math.isfinite(timeout_s) or timeout_s <= 0:
64
+ raise ValueError('adapter timeout must be finite and positive')
65
+ remaining = min(remaining, timeout_s)
66
+ return replace(policy, timeout_s=remaining, **values)
67
+
68
+ def add(self, result, arguments):
69
+ if type(result) is not Result:
70
+ raise ValueError('adapter delegate must return Result')
71
+ owned = deepcopy(result)
72
+ info = owned.metadata.get('invocation_budgets_v1', {})
73
+ requests = info.get('provider_requests', 1)
74
+ tools = info.get('tool_calls', 0)
75
+ if any(type(value) is not int or value < 0 for value in (requests, tools)):
76
+ raise ValueError('invalid delegate allowance evidence')
77
+ usage = owned.token_usage
78
+ if usage is not None and (type(usage) is not int or usage < 0):
79
+ raise ValueError('invalid delegate usage evidence')
80
+ complete = info.get('usage_complete', usage is not None or requests == 0)
81
+ if type(complete) is not bool:
82
+ raise ValueError('invalid delegate usage completeness')
83
+ self.complete &= complete and (usage is not None or requests == 0)
84
+ reported = info.get('reported_tokens', usage or 0)
85
+ if type(reported) is not int or reported < 0:
86
+ raise ValueError('invalid partial usage evidence')
87
+ known = max(reported, usage or 0)
88
+ self.tokens += known
89
+ self.requests += requests
90
+ self.tools += tools
91
+ if not isinstance(owned.cost_usd, (int, float)) or not math.isfinite(owned.cost_usd) or owned.cost_usd < 0:
92
+ raise ValueError('invalid delegate cost evidence')
93
+ self.cost += owned.cost_usd
94
+ self.last = owned
95
+ self.records.append({'index': len(self.records) + 1, 'exit_code': owned.exit_code,
96
+ 'timed_out': owned.timed_out, 'token_usage': usage, 'known_tokens': known, 'provider_requests': requests,
97
+ 'tool_calls': tools, 'arguments_sha256': hashlib.sha256(arguments.encode()).hexdigest()})
98
+
99
+ def finish(self, result, descriptor):
100
+ if type(result) is not Result:
101
+ result = Result(1, '', 'adapter returned an invalid result', metadata={'failure_reason': 'adapter_result_mismatch'})
102
+ if result.exit_code == 0 and (self.last is None or self.last.exit_code != 0):
103
+ result = Result(1, '', 'adapter cannot admit a failed execution', metadata={'failure_reason': 'adapter_result_mismatch'})
104
+ reason = None
105
+ for name, value, code in (('max_provider_requests', self.requests, 'provider_request_limit'),
106
+ ('max_tool_calls', self.tools, 'tool_call_limit'),
107
+ ('max_reported_tokens', self.tokens, 'token_limit')):
108
+ cap = getattr(self.policy, name)
109
+ if cap is not None and value > cap:
110
+ reason = code
111
+ if self.policy.max_reported_tokens is not None and not self.complete:
112
+ reason = 'usage_unknown'
113
+ if result.exit_code == 0 and self.cancelled():
114
+ result = Result(130, '', 'invocation cancelled', metadata={'failure_reason': 'cancelled'})
115
+ elif result.exit_code == 0 and time.monotonic() >= self.deadline:
116
+ result = Result(1, '', 'invocation deadline exceeded', timed_out=True)
117
+ if reason:
118
+ result = Result(1, '', reason, metadata={'failure_reason': reason})
119
+ metadata = deepcopy(self.last.metadata if self.last is not None else {})
120
+ for key, value in result.metadata.items():
121
+ if key not in metadata or key == 'failure_reason':
122
+ metadata[key] = deepcopy(value)
123
+ result = replace(result, stdout=result.stdout if result.exit_code == 0 else '', metadata=metadata,
124
+ token_usage=self.tokens if self.complete and self.records else None, cost_usd=self.cost)
125
+ result.metadata['invocation_budgets_v1'] = {'provider_requests': self.requests,
126
+ 'tool_calls': self.tools, 'reported_tokens': self.tokens, 'usage_complete': self.complete}
127
+ result.metadata['runtime_adapter_v1'] = {**descriptor, 'attempts': deepcopy(self.records)}
128
+ return result
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class RuntimeAdapter:
133
+ """Versioned trusted invocation hook; execute accepts only reduced authority.
134
+
135
+ invoke(artifact, arguments, *, execute, policy, on_event, cancelled, **options)
136
+ must return Result. execute(arguments, **reductions) performs a real Runtime
137
+ invocation. Hooks are cooperative trusted host code, not a sandbox.
138
+ """
139
+ runtime: ProsaicRuntime
140
+ version: str
141
+ invoke: object
142
+ max_attempts: int = 3
143
+
144
+ def __post_init__(self):
145
+ if type(self.version) is not str or not re.fullmatch(r'[A-Za-z0-9_.-]{1,128}', self.version):
146
+ raise ValueError('adapter needs a bounded version')
147
+ if not callable(self.invoke) or type(self.max_attempts) is not int or not 1 <= self.max_attempts <= 8:
148
+ raise ValueError('adapter needs a callback and bounded attempts')
149
+ self.validate_binding()
150
+
151
+ def validate_binding(self):
152
+ if (type(self.runtime) is not ProsaicRuntime or
153
+ getattr(self.runtime.run, '__func__', None) is not ProsaicRuntime.run):
154
+ raise ValueError('adapter requires an unmodified concrete ProsaicRuntime')
155
+
156
+ @property
157
+ def descriptor(self):
158
+ self.validate_binding()
159
+ return {'version': self.version, 'contract': 'runtime_adapter_v1', 'max_attempts': self.max_attempts}
160
+
161
+ @property
162
+ def config(self):
163
+ return self.runtime.config
164
+
165
+ @property
166
+ def capabilities(self):
167
+ return self.runtime.capabilities | frozenset({'runtime_adapter_v1'})
168
+
169
+ @property
170
+ def tool_descriptors(self):
171
+ return self.runtime.tool_descriptors
172
+
173
+ @property
174
+ def cli_tool_names(self):
175
+ return self.runtime.cli_tool_names
176
+
177
+ def run(self, artifact, arguments='', *, policy=None, on_event=None, cancelled=None, **options):
178
+ self.validate_binding()
179
+ policy = policy or RunPolicy()
180
+ cancelled = cancelled or (lambda: False)
181
+ ledger = _Attempts(policy, self.max_attempts, time.monotonic() + policy.timeout_s, cancelled)
182
+ descriptor = self.descriptor
183
+ def execute(projected, **reductions):
184
+ self.validate_binding()
185
+ effective = ledger.remaining(**reductions)
186
+ if type(projected) is not str:
187
+ raise ValueError('adapter arguments must be text')
188
+ result = self.runtime.run(artifact, projected, policy=effective, on_event=on_event,
189
+ cancelled=cancelled, **options)
190
+ ledger.add(result, projected)
191
+ return result
192
+ try:
193
+ result = self.invoke(artifact, arguments, execute=execute, policy=policy,
194
+ on_event=on_event, cancelled=cancelled, **options)
195
+ except (InvocationBudgetExceeded, AdapterBoundary) as exc:
196
+ result = Result(130 if exc.reason == 'adapter_cancelled' else 1, '', exc.reason,
197
+ timed_out=exc.reason == 'adapter_timeout', metadata={'failure_reason': exc.reason})
198
+ except Exception:
199
+ result = Result(1, '', 'trusted adapter callback failed', metadata={'failure_reason': 'adapter_callback_failed'})
200
+ return ledger.finish(result, descriptor)
@@ -0,0 +1,108 @@
1
+ """Host-owned, process-local allowance across nested synchronous Runtime calls."""
2
+ from contextlib import contextmanager
3
+ from contextvars import ContextVar
4
+ import math
5
+ from threading import RLock
6
+ import time
7
+
8
+ from .budgets import InvocationBudgetExceeded
9
+ from .events import Cancelled
10
+ from .tools import ToolDeadlineExceeded
11
+
12
+ _active = ContextVar('prosaic_execution_budgets', default=())
13
+
14
+
15
+ class ExecutionBudget:
16
+ """A fixed deadline and dispatch caps; activation never replenishes allowance.
17
+
18
+ This is admission, not a durable ledger or a sandbox for trusted callbacks.
19
+ Activate the same object explicitly in worker threads to share its counters.
20
+ """
21
+ def __init__(self, *, timeout_s, max_provider_requests, max_tool_calls, max_reported_tokens=None, cancelled=None):
22
+ if type(timeout_s) not in (int,float) or not math.isfinite(timeout_s) or timeout_s<=0:
23
+ raise ValueError('invalid execution timeout')
24
+ for cap in (max_provider_requests,max_tool_calls):
25
+ if type(cap) is not int or cap<0: raise ValueError('invalid execution allowance')
26
+ if max_reported_tokens is not None and (type(max_reported_tokens) is not int or max_reported_tokens<0):
27
+ raise ValueError('invalid reported token allowance')
28
+ if cancelled is not None and not callable(cancelled): raise ValueError('invalid cancellation callback')
29
+ self._deadline=time.monotonic()+timeout_s
30
+ self._provider_cap,self._tool_cap=max_provider_requests,max_tool_calls
31
+ self._provider_requests=self._tool_calls=self._reported_tokens=0
32
+ self._token_cap=max_reported_tokens
33
+ self._usage_complete=True
34
+ self._cancelled=cancelled
35
+ self._lock=RLock()
36
+
37
+ @property
38
+ def deadline(self): return self._deadline
39
+ @property
40
+ def provider_requests(self):
41
+ with self._lock: return self._provider_requests
42
+ @property
43
+ def tool_calls(self):
44
+ with self._lock: return self._tool_calls
45
+
46
+ @property
47
+ def reported_tokens(self):
48
+ with self._lock: return self._reported_tokens
49
+ @property
50
+ def usage_complete(self):
51
+ with self._lock: return self._usage_complete
52
+
53
+ def _check_tokens(self, *, dispatch=False):
54
+ if self._token_cap is not None:
55
+ if not self._usage_complete: raise InvocationBudgetExceeded('usage_unknown')
56
+ if self._reported_tokens>self._token_cap or (dispatch and self._reported_tokens>=self._token_cap):
57
+ raise InvocationBudgetExceeded('token_limit')
58
+
59
+ def check(self):
60
+ if self._cancelled is not None and self._cancelled(): raise Cancelled()
61
+ if time.monotonic()>=self._deadline: raise ToolDeadlineExceeded('execution deadline exceeded')
62
+
63
+ @contextmanager
64
+ def activate(self):
65
+ stack=_active.get()
66
+ token=_active.set(stack if self in stack else (*stack,self))
67
+ try: yield self
68
+ finally: _active.reset(token)
69
+
70
+
71
+ def active_budgets(): return _active.get()
72
+
73
+
74
+ def check_budgets(budgets):
75
+ for budget in budgets: budget.check()
76
+
77
+
78
+ def reserve_dispatch(budgets, *, tool=False, reserve=True):
79
+ """All enclosing allowances are checked under ordered locks before any debit."""
80
+ check_budgets(budgets)
81
+ ordered=sorted(budgets,key=id)
82
+ for budget in ordered: budget._lock.acquire()
83
+ try:
84
+ for budget in budgets:
85
+ budget._check_tokens(dispatch=True)
86
+ count=budget._tool_calls if tool else budget._provider_requests
87
+ cap=budget._tool_cap if tool else budget._provider_cap
88
+ if count>=cap:
89
+ raise InvocationBudgetExceeded('tool_call_limit' if tool else 'provider_request_limit')
90
+ if reserve:
91
+ for budget in budgets:
92
+ if tool: budget._tool_calls+=1
93
+ else: budget._provider_requests+=1
94
+ finally:
95
+ for budget in reversed(ordered): budget._lock.release()
96
+
97
+
98
+ def record_shared_usage(budgets, usage):
99
+ # Retain all enclosing paid evidence before reporting any violated cap.
100
+ ordered=sorted(budgets,key=id)
101
+ for budget in ordered: budget._lock.acquire()
102
+ try:
103
+ for budget in budgets:
104
+ if type(usage) is not int or usage<0: budget._usage_complete=False
105
+ else: budget._reported_tokens+=usage
106
+ for budget in budgets: budget._check_tokens()
107
+ finally:
108
+ for budget in reversed(ordered): budget._lock.release()
@@ -1,5 +1,5 @@
1
1
  """Opaque host correlation, independent of billing attribution."""
2
- from dataclasses import dataclass, fields
2
+ from dataclasses import dataclass, fields, field
3
3
  from collections.abc import Callable
4
4
  import unicodedata
5
5
 
@@ -37,3 +37,25 @@ class ToolExecutionContext:
37
37
  deadline: float
38
38
  cancelled: Callable
39
39
  operation_key: str | None = None
40
+
41
+ accounting_context: object = field(default=None, repr=False)
42
+
43
+ def child_context(self, *, invocation_id, run_id=None, step_id=None):
44
+ """Derive nested attribution from the actual resolved parent, never tool arguments."""
45
+ from .accounting import ResolvedContext
46
+ if type(self.accounting_context) is not ResolvedContext:
47
+ raise ValueError('accounted parent context required')
48
+ return self.accounting_context.child(invocation_id=invocation_id,run_id=run_id,step_id=step_id)
49
+
50
+ def remaining_timeout(self, maximum):
51
+ """Narrow a child's timeout to this tool's original absolute deadline."""
52
+ import math
53
+ import time
54
+ from .events import Cancelled
55
+ from .tools import ToolDeadlineExceeded
56
+ if type(maximum) not in (int,float) or not math.isfinite(maximum) or maximum<=0:
57
+ raise ValueError('invalid timeout')
58
+ if self.cancelled(): raise Cancelled()
59
+ remaining=self.deadline-time.monotonic()
60
+ if remaining<=0: raise ToolDeadlineExceeded('tool deadline exceeded')
61
+ return min(maximum,remaining)