b3-prosaic-runtime 0.9.4__tar.gz → 0.9.6__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 (178) hide show
  1. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/CHANGELOG.md +7 -0
  2. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/PKG-INFO +39 -1
  3. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/README.md +38 -0
  4. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/tool-effects.md +9 -1
  5. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/pyproject.toml +1 -1
  6. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/scripts/conversation_wheel_smoke.py +1 -1
  7. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/scripts/production_wheel_smoke.py +1 -1
  8. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/b3_prosaic_runtime.egg-info/PKG-INFO +39 -1
  9. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/b3_prosaic_runtime.egg-info/SOURCES.txt +6 -0
  10. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/__init__.py +7 -2
  11. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/accounting_capture.py +2 -0
  12. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/adapter.py +6 -0
  13. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/anthropic.py +2 -1
  14. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/execution.py +28 -1
  15. b3_prosaic_runtime-0.9.6/src/prosaic_runtime/lifecycle.py +85 -0
  16. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/openai_compatible.py +17 -0
  17. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/runtime.py +12 -2
  18. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/tool_effects.py +27 -1
  19. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/tools.py +4 -4
  20. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_conformance.py +1 -1
  21. b3_prosaic_runtime-0.9.6/tests/test_controlled_tool_failure.py +89 -0
  22. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_distribution_metadata.py +1 -1
  23. b3_prosaic_runtime-0.9.6/tests/test_finalization.py +191 -0
  24. b3_prosaic_runtime-0.9.6/tests/test_http_correlation.py +80 -0
  25. b3_prosaic_runtime-0.9.6/tests/test_lifecycle.py +70 -0
  26. b3_prosaic_runtime-0.9.6/tests/test_omlx_tool_envelope.py +60 -0
  27. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_package_compatibility.py +1 -1
  28. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_tool_effects.py +1 -1
  29. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/LICENSE +0 -0
  30. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/LICENSE-MIT +0 -0
  31. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/MANIFEST.in +0 -0
  32. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/NOTICE.md +0 -0
  33. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/accounting.md +0 -0
  34. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/adr-cli-sandbox.md +0 -0
  35. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/cli-tools.md +0 -0
  36. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/conformance.md +0 -0
  37. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/conversations.md +0 -0
  38. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/package-index.md +0 -0
  39. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/production-acceptance.md +0 -0
  40. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/release-0.7.1.md +0 -0
  41. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/security.md +0 -0
  42. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/structured-tools.md +0 -0
  43. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/telemetry.md +0 -0
  44. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/docs/workflow-factory-runtime-implementation.md +0 -0
  45. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/acquire-pilot.md +0 -0
  46. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/acquired-reviewer.md +0 -0
  47. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/catalog-reader.md +0 -0
  48. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/cli-spec-reviewer.md +0 -0
  49. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/launch-analyst.md +0 -0
  50. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/launch-briefer.md +0 -0
  51. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/launch-decision.md +0 -0
  52. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/launch-skeptic.md +0 -0
  53. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/native-summarizer.md +0 -0
  54. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/preloaded-reviewer.md +0 -0
  55. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/reviewer.md +0 -0
  56. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/subagents/summarizer.md +0 -0
  57. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/.prosaic/tools/analyze-spec.yml +0 -0
  58. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/README.md +0 -0
  59. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/anthropic.yml +0 -0
  60. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/catalog.json +0 -0
  61. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/catalog_tools.py +0 -0
  62. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/cli-tool/prosaic_example_analyzer.py +0 -0
  63. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/cli-tool/pyproject.toml +0 -0
  64. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/cli-tools-sandboxed.yml +0 -0
  65. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/cli-tools.yml +0 -0
  66. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/custom-tools.yml +0 -0
  67. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/evidence/launch/brief.md +0 -0
  68. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/evidence/launch/field-notes.md +0 -0
  69. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/evidence/launch/metrics.md +0 -0
  70. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/evidence/pilot.md +0 -0
  71. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/evidence/requirements.md +0 -0
  72. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/prosaic-runtime.yaml +0 -0
  73. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/run_acquisition.py +0 -0
  74. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/run_cli_tool.py +0 -0
  75. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/run_custom_tool.py +0 -0
  76. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/run_examples.py +0 -0
  77. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/run_sandbox_probe.py +0 -0
  78. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/run_tiers.py +0 -0
  79. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/tokenproxy.yml +0 -0
  80. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/understanding/.prosaic/subagents/reviewer.md +0 -0
  81. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/understanding/.prosaic/tools/understanding.yml +0 -0
  82. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/understanding/runtime.yml +0 -0
  83. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/examples/with-tools.yml +0 -0
  84. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/scripts/release_artifacts.py +0 -0
  85. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/scripts/wheel_smoke.py +0 -0
  86. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/setup.cfg +0 -0
  87. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/b3_prosaic_runtime.egg-info/dependency_links.txt +0 -0
  88. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/b3_prosaic_runtime.egg-info/entry_points.txt +0 -0
  89. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/b3_prosaic_runtime.egg-info/requires.txt +0 -0
  90. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/b3_prosaic_runtime.egg-info/top_level.txt +0 -0
  91. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/accounting.py +0 -0
  92. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/admission.py +0 -0
  93. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/anthropic_stream.py +0 -0
  94. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/anthropic_usage.py +0 -0
  95. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/artifacts.py +0 -0
  96. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/budgets.py +0 -0
  97. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/cli.py +0 -0
  98. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/cli_tools.py +0 -0
  99. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/config.py +0 -0
  100. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/conformance.py +0 -0
  101. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/console.py +0 -0
  102. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/context_assembly.py +0 -0
  103. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/context_contracts.py +0 -0
  104. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/context_counters.py +0 -0
  105. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/context_execution.py +0 -0
  106. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/context_work.py +0 -0
  107. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/conversation.py +0 -0
  108. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/conversation_preparation.py +0 -0
  109. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/diagnostics.py +0 -0
  110. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/events.py +0 -0
  111. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/execution_budget.py +0 -0
  112. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/http_bounds.py +0 -0
  113. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/openai_compatible_compaction.py +0 -0
  114. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/openai_compatible_filters.py +0 -0
  115. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/openai_compatible_progress.py +0 -0
  116. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/openai_compatible_transcript.py +0 -0
  117. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/operation_context.py +0 -0
  118. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/policy.py +0 -0
  119. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/provider_types.py +0 -0
  120. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/sandbox.py +0 -0
  121. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/smoke_prose/pilot.md +0 -0
  122. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/smoke_prose/subagents/native-summarizer.md +0 -0
  123. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/smoke_prose/subagents/reviewer.md +0 -0
  124. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/smoke_prose/subagents/summarizer.md +0 -0
  125. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/structured_tools.py +0 -0
  126. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/telemetry.py +0 -0
  127. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/tool_registry.py +0 -0
  128. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/turn_capture.py +0 -0
  129. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/src/prosaic_runtime/types.py +0 -0
  130. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/support/context_counter.py +0 -0
  131. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/support/conversation_server.py +0 -0
  132. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/support/tool_journal.py +0 -0
  133. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_accounting.py +0 -0
  134. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_acquisition.py +0 -0
  135. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_anthropic.py +0 -0
  136. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_anthropic_accounting.py +0 -0
  137. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_anthropic_package.py +0 -0
  138. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_anthropic_stream.py +0 -0
  139. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_artifact_admission.py +0 -0
  140. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_cli_sandbox.py +0 -0
  141. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_cli_tool_example.py +0 -0
  142. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_cli_tools.py +0 -0
  143. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_cli_tools_boundaries.py +0 -0
  144. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_config.py +0 -0
  145. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_context_assembly.py +0 -0
  146. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_context_contracts.py +0 -0
  147. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_context_execution.py +0 -0
  148. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_conversation_bounds.py +0 -0
  149. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_conversation_contracts.py +0 -0
  150. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_conversation_distribution.py +0 -0
  151. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_conversation_failure_evidence.py +0 -0
  152. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_conversation_review_regressions.py +0 -0
  153. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_conversation_tool_batches.py +0 -0
  154. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_conversation_turns.py +0 -0
  155. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_custom_tool_example.py +0 -0
  156. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_custom_tool_transport.py +0 -0
  157. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_custom_tools.py +0 -0
  158. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_execution_budget.py +0 -0
  159. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_features.py +0 -0
  160. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_http_boundaries.py +0 -0
  161. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_input_boundary.py +0 -0
  162. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_invocation_budgets.py +0 -0
  163. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_production_smoke.py +0 -0
  164. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_prompt_injection.py +0 -0
  165. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_provider_execution.py +0 -0
  166. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_read_provenance.py +0 -0
  167. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_redirect_safety.py +0 -0
  168. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_release_artifacts.py +0 -0
  169. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_run_adapter.py +0 -0
  170. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_runtime.py +0 -0
  171. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_sample_program.py +0 -0
  172. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_sandbox_probe_example.py +0 -0
  173. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_structured_tools.py +0 -0
  174. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_telemetry.py +0 -0
  175. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_tier_examples.py +0 -0
  176. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_tool_effect_workers.py +0 -0
  177. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_transport.py +0 -0
  178. {b3_prosaic_runtime-0.9.4 → b3_prosaic_runtime-0.9.6}/tests/test_usage_integrity.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.6 — 2026-10-17
4
+
5
+ - Add opt-in host-controlled final-answer phases after a bounded number of tool rounds.
6
+ - Add optional oMLX native tool-call envelope constraints, removed for final answers.
7
+ - Validate envelope configuration before dispatch and reject further tool calls after finalization.
8
+ - Retain native grant, budget and argument checks; defaults remain unchanged.
9
+
3
10
  ## 0.9.3 — 2026-10-14
4
11
 
5
12
  - Normalize negative provider timeout exit statuses to 124 in conversation turn
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3-prosaic-runtime
3
- Version: 0.9.4
3
+ Version: 0.9.6
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
@@ -28,6 +28,42 @@ and the `prosaic-runtime` CLI remain unchanged. The separately installed recorde
28
28
  is `b3-prosaic-runtime-postgres` **0.3.0**, selecting Runtime 0.9.x.
29
29
  Candidates are not public index releases until the release qualification gate passes.
30
30
 
31
+ ### Opt-in final-answer phase
32
+
33
+ The local `0.9.3+finalize.4` candidate adds an operator-controlled endpoint
34
+ feature for bounded lookup workflows:
35
+
36
+ ```yaml
37
+ features:
38
+ finalize_after_tool_rounds: 1
39
+ ```
40
+
41
+ After that many tool rounds, the next OpenAI-compatible or native Anthropic
42
+ request contains no tool declarations and receives trusted final-answer
43
+ guidance. A provider that still requests a tool fails with
44
+ `tool_finalization_not_honored` before another handler runs. Existing provider,
45
+ tool, token and time budgets still apply; allow capacity for the final request.
46
+ Cancellation or exhausted budgets can stop before that request. Missing usage
47
+ remains unknown. This setting does not assert that the evidence is sufficient
48
+ or that the final answer is correct.
49
+
50
+ The value must be an integer from one through the invocation's effective
51
+ `max_tool_rounds`. Omit it to retain existing multi-round and no-progress
52
+ behavior. Agent prose and model output cannot set this host configuration.
53
+ This candidate is for pinned-wheel qualification, not a published release.
54
+
55
+ For an oMLX endpoint serving Qwen JSON tool envelopes, the host can also set
56
+ `omlx_tool_call_envelope: true`. This requires an explicit
57
+ `finalize_after_tool_rounds` and requests oMLX's `structured_outputs.regex`
58
+ constraint while tools are offered. It forces a tool envelope rather than an
59
+ ordinary answer during that phase; the final request removes this constraint.
60
+ `json_mode` applies to the final answer and is suppressed during the tool phase.
61
+ Native Anthropic rejects this vendor-specific setting before dispatch. Other
62
+ servers may reject or ignore it; this is not a portable OpenAI protocol option.
63
+ The constraint covers the envelope, not argument correctness or task quality.
64
+ Runtime still validates native tool names, arguments, permissions and budgets.
65
+ Assistant text is never converted to a tool call by this setting.
66
+
31
67
  The optional [stateless conversation API](docs/conversations.md) uses complete,
32
68
  bounded portable history through `prepare_turn()` and `turn()`. It requires no
33
69
  workspace for ordinary callbacks or tool-free turns. Durable sessions and human
@@ -695,3 +731,5 @@ budget as a durable ledger or reset uncertain allowances after a restart.
695
731
  PostgreSQL accounting remains the authoritative provider-call evidence. Saved
696
732
  host outcomes require a fenced ToolJournal or equivalent durable application
697
733
  repository; interrupted effects must not be repeated automatically.
734
+
735
+ See [execution correlation and graceful drain](docs/execution-lifecycle.md) for the host shutdown contract.
@@ -6,6 +6,42 @@ and the `prosaic-runtime` CLI remain unchanged. The separately installed recorde
6
6
  is `b3-prosaic-runtime-postgres` **0.3.0**, selecting Runtime 0.9.x.
7
7
  Candidates are not public index releases until the release qualification gate passes.
8
8
 
9
+ ### Opt-in final-answer phase
10
+
11
+ The local `0.9.3+finalize.4` candidate adds an operator-controlled endpoint
12
+ feature for bounded lookup workflows:
13
+
14
+ ```yaml
15
+ features:
16
+ finalize_after_tool_rounds: 1
17
+ ```
18
+
19
+ After that many tool rounds, the next OpenAI-compatible or native Anthropic
20
+ request contains no tool declarations and receives trusted final-answer
21
+ guidance. A provider that still requests a tool fails with
22
+ `tool_finalization_not_honored` before another handler runs. Existing provider,
23
+ tool, token and time budgets still apply; allow capacity for the final request.
24
+ Cancellation or exhausted budgets can stop before that request. Missing usage
25
+ remains unknown. This setting does not assert that the evidence is sufficient
26
+ or that the final answer is correct.
27
+
28
+ The value must be an integer from one through the invocation's effective
29
+ `max_tool_rounds`. Omit it to retain existing multi-round and no-progress
30
+ behavior. Agent prose and model output cannot set this host configuration.
31
+ This candidate is for pinned-wheel qualification, not a published release.
32
+
33
+ For an oMLX endpoint serving Qwen JSON tool envelopes, the host can also set
34
+ `omlx_tool_call_envelope: true`. This requires an explicit
35
+ `finalize_after_tool_rounds` and requests oMLX's `structured_outputs.regex`
36
+ constraint while tools are offered. It forces a tool envelope rather than an
37
+ ordinary answer during that phase; the final request removes this constraint.
38
+ `json_mode` applies to the final answer and is suppressed during the tool phase.
39
+ Native Anthropic rejects this vendor-specific setting before dispatch. Other
40
+ servers may reject or ignore it; this is not a portable OpenAI protocol option.
41
+ The constraint covers the envelope, not argument correctness or task quality.
42
+ Runtime still validates native tool names, arguments, permissions and budgets.
43
+ Assistant text is never converted to a tool call by this setting.
44
+
9
45
  The optional [stateless conversation API](docs/conversations.md) uses complete,
10
46
  bounded portable history through `prepare_turn()` and `turn()`. It requires no
11
47
  workspace for ordinary callbacks or tool-free turns. Durable sessions and human
@@ -673,3 +709,5 @@ budget as a durable ledger or reset uncertain allowances after a restart.
673
709
  PostgreSQL accounting remains the authoritative provider-call evidence. Saved
674
710
  host outcomes require a fenced ToolJournal or equivalent durable application
675
711
  repository; interrupted effects must not be repeated automatically.
712
+
713
+ See [execution correlation and graceful drain](docs/execution-lifecycle.md) for the host shutdown contract.
@@ -72,7 +72,7 @@ matching lowercase 64-hex signature and one of these closed states:
72
72
  | State | Permitted fields | Runtime action |
73
73
  | --- | --- | --- |
74
74
  | `new` | Bounded nonempty `claim_token`; no outcome | Dispatch the effect once. |
75
- | `replay` | No token; finite bounded `{'status': 'ok', 'result': ...}` outcome | Copy and return the recorded outcome. |
75
+ | `replay` | No token; finite bounded `{'status': 'ok', 'result': ...}` or `{'status': 'error', 'error': code}` outcome | Copy and return the recorded outcome. |
76
76
  | `uncertain` | No token or outcome | Stop pending explicit host resolution. |
77
77
 
78
78
  The journal must atomically claim the namespace/key address and commit using a
@@ -112,3 +112,11 @@ or domain idempotency/fencing.
112
112
  The SQLite journal under `tests/support/` is test support only. Production business
113
113
  journals belong to host applications; usage recorders and Harness checkpoints are
114
114
  separate stores and responsibilities.
115
+
116
+ A callback may return the public `ToolFailure("read_unavailable")` only for a
117
+ proven completed failure. Its bounded machine code is journaled before cancellation
118
+ or deadline checks; completion evidence is `completed` or `replayed`, never an
119
+ assertion that the requested business operation succeeded. NEVER return this type
120
+ for an unknown effect. ALWAYS leave generic exceptions and lost acknowledgements
121
+ on the uncertain-effect path. Ordinary returned dictionaries remain successful
122
+ JSON data and are not interpreted as this explicit failure contract.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "b3-prosaic-runtime"
7
- version = "0.9.4"
7
+ version = "0.9.6"
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.4'),
25
+ (api, 'b3-prosaic-runtime', '0.9.6'),
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.4"),
52
+ (runtime, "b3-prosaic-runtime", "0.9.6"),
53
53
  (postgres, "b3-prosaic-runtime-postgres", "0.3.0")):
54
54
  owner = metadata.distribution(name)
55
55
  assert owner.version == package.__version__ == version
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3-prosaic-runtime
3
- Version: 0.9.4
3
+ Version: 0.9.6
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
@@ -28,6 +28,42 @@ and the `prosaic-runtime` CLI remain unchanged. The separately installed recorde
28
28
  is `b3-prosaic-runtime-postgres` **0.3.0**, selecting Runtime 0.9.x.
29
29
  Candidates are not public index releases until the release qualification gate passes.
30
30
 
31
+ ### Opt-in final-answer phase
32
+
33
+ The local `0.9.3+finalize.4` candidate adds an operator-controlled endpoint
34
+ feature for bounded lookup workflows:
35
+
36
+ ```yaml
37
+ features:
38
+ finalize_after_tool_rounds: 1
39
+ ```
40
+
41
+ After that many tool rounds, the next OpenAI-compatible or native Anthropic
42
+ request contains no tool declarations and receives trusted final-answer
43
+ guidance. A provider that still requests a tool fails with
44
+ `tool_finalization_not_honored` before another handler runs. Existing provider,
45
+ tool, token and time budgets still apply; allow capacity for the final request.
46
+ Cancellation or exhausted budgets can stop before that request. Missing usage
47
+ remains unknown. This setting does not assert that the evidence is sufficient
48
+ or that the final answer is correct.
49
+
50
+ The value must be an integer from one through the invocation's effective
51
+ `max_tool_rounds`. Omit it to retain existing multi-round and no-progress
52
+ behavior. Agent prose and model output cannot set this host configuration.
53
+ This candidate is for pinned-wheel qualification, not a published release.
54
+
55
+ For an oMLX endpoint serving Qwen JSON tool envelopes, the host can also set
56
+ `omlx_tool_call_envelope: true`. This requires an explicit
57
+ `finalize_after_tool_rounds` and requests oMLX's `structured_outputs.regex`
58
+ constraint while tools are offered. It forces a tool envelope rather than an
59
+ ordinary answer during that phase; the final request removes this constraint.
60
+ `json_mode` applies to the final answer and is suppressed during the tool phase.
61
+ Native Anthropic rejects this vendor-specific setting before dispatch. Other
62
+ servers may reject or ignore it; this is not a portable OpenAI protocol option.
63
+ The constraint covers the envelope, not argument correctness or task quality.
64
+ Runtime still validates native tool names, arguments, permissions and budgets.
65
+ Assistant text is never converted to a tool call by this setting.
66
+
31
67
  The optional [stateless conversation API](docs/conversations.md) uses complete,
32
68
  bounded portable history through `prepare_turn()` and `turn()`. It requires no
33
69
  workspace for ordinary callbacks or tool-free turns. Durable sessions and human
@@ -695,3 +731,5 @@ budget as a durable ledger or reset uncertain allowances after a restart.
695
731
  PostgreSQL accounting remains the authoritative provider-call evidence. Saved
696
732
  host outcomes require a fenced ToolJournal or equivalent durable application
697
733
  repository; interrupted effects must not be repeated automatically.
734
+
735
+ See [execution correlation and graceful drain](docs/execution-lifecycle.md) for the host shutdown contract.
@@ -94,6 +94,7 @@ src/prosaic_runtime/events.py
94
94
  src/prosaic_runtime/execution.py
95
95
  src/prosaic_runtime/execution_budget.py
96
96
  src/prosaic_runtime/http_bounds.py
97
+ src/prosaic_runtime/lifecycle.py
97
98
  src/prosaic_runtime/openai_compatible.py
98
99
  src/prosaic_runtime/openai_compatible_compaction.py
99
100
  src/prosaic_runtime/openai_compatible_filters.py
@@ -131,6 +132,7 @@ tests/test_conformance.py
131
132
  tests/test_context_assembly.py
132
133
  tests/test_context_contracts.py
133
134
  tests/test_context_execution.py
135
+ tests/test_controlled_tool_failure.py
134
136
  tests/test_conversation_bounds.py
135
137
  tests/test_conversation_contracts.py
136
138
  tests/test_conversation_distribution.py
@@ -144,9 +146,13 @@ tests/test_custom_tools.py
144
146
  tests/test_distribution_metadata.py
145
147
  tests/test_execution_budget.py
146
148
  tests/test_features.py
149
+ tests/test_finalization.py
147
150
  tests/test_http_boundaries.py
151
+ tests/test_http_correlation.py
148
152
  tests/test_input_boundary.py
149
153
  tests/test_invocation_budgets.py
154
+ tests/test_lifecycle.py
155
+ tests/test_omlx_tool_envelope.py
150
156
  tests/test_package_compatibility.py
151
157
  tests/test_production_smoke.py
152
158
  tests/test_prompt_injection.py
@@ -1,5 +1,5 @@
1
1
  """Execute neutral Prosaic artifacts with explicitly granted authority."""
2
- __version__ = '0.9.4'
2
+ __version__ = '0.9.6'
3
3
 
4
4
  from .artifacts import ProsaicArtifact
5
5
  from .config import EndpointConfig, RuntimeConfig, RunLimits, CliSandboxConfig
@@ -9,7 +9,7 @@ from .types import Result
9
9
  from .tools import CustomTool, validate_custom_tools, custom_descriptors
10
10
  from .admission import validate_execution_artifact
11
11
  from .operation_context import InvocationScope, ToolExecutionContext
12
- from .tool_effects import ToolClaim, ToolJournal, tool_journal_descriptor
12
+ from .tool_effects import ToolFailure, ToolClaim, ToolJournal, tool_journal_descriptor
13
13
  from .telemetry import ObserverEmitter
14
14
  from .conformance import evaluate_conformance
15
15
 
@@ -44,3 +44,8 @@ from .context_counters import (TokenCounter, TokenCounterRegistration,
44
44
  __all__ += ["ContextPolicy", "TokenCounterDescriptor", "ContextAssemblyReport",
45
45
  "ContextSelection", "ContextWorkState", "TokenCounter", "TokenCounterRegistration",
46
46
  "TokenCounterRegistry"]
47
+
48
+ from .lifecycle import ExecutionLifecycle, Draining
49
+ __all__ += ["ExecutionLifecycle", "Draining"]
50
+
51
+ __all__ += ["ToolFailure"]
@@ -44,6 +44,8 @@ class Capture:
44
44
  self.recorder.prepare(intent)
45
45
  except Exception as exc:
46
46
  raise AccountingError('intent persistence failed') from exc
47
+ request.add_header("X-Correlation-ID", call_id)
48
+ request.add_header("X-Client-Request-Id", call_id)
47
49
  self.call_ids.append(call_id)
48
50
  observer = AnthropicObservation if self.provider == 'anthropic' else Observation
49
51
  observation = observer(self.recorder, call_id, on_observation=self.on_observation)
@@ -11,6 +11,7 @@ import re
11
11
  import time
12
12
 
13
13
  from .runtime import ProsaicRuntime
14
+ from .lifecycle import tracked
14
15
  from .policy import RunPolicy
15
16
  from .types import Result
16
17
  from .budgets import InvocationBudgetExceeded
@@ -162,6 +163,10 @@ class RuntimeAdapter:
162
163
  def config(self):
163
164
  return self.runtime.config
164
165
 
166
+ @property
167
+ def lifecycle(self):
168
+ return self.runtime.lifecycle
169
+
165
170
  @property
166
171
  def capabilities(self):
167
172
  return self.runtime.capabilities | frozenset({'runtime_adapter_v1'})
@@ -174,6 +179,7 @@ class RuntimeAdapter:
174
179
  def cli_tool_names(self):
175
180
  return self.runtime.cli_tool_names
176
181
 
182
+ @tracked
177
183
  def run(self, artifact, arguments='', *, policy=None, on_event=None, cancelled=None, **options):
178
184
  self.validate_binding()
179
185
  policy = policy or RunPolicy()
@@ -23,7 +23,8 @@ API_VERSION = '2023-06-01'
23
23
 
24
24
  def validate_anthropic_controls(features, prompt_metadata):
25
25
  """Validate known Messages controls without constructing a model request."""
26
- unsupported = {'json_mode', 'reasoning_effort', 'effort', 'thinking', 'stream_options', 'web_tools'}
26
+ unsupported = {'json_mode', 'reasoning_effort', 'effort', 'thinking', 'stream_options', 'web_tools',
27
+ 'omlx_tool_call_envelope'}
27
28
  for name in unsupported:
28
29
  value = features.get(name)
29
30
  if value not in (None, False, 'off', 'false', 'disabled'):
@@ -57,6 +57,7 @@ class ExecutionBackend:
57
57
  identical_tool_rounds = 0
58
58
  tools_disabled = False
59
59
  tool_no_progress_forced = False
60
+ finalization_forced = False
60
61
  max_tool_rounds = _feature_int(
61
62
  llm.features,
62
63
  "max_tool_rounds",
@@ -71,6 +72,11 @@ class ExecutionBackend:
71
72
  minimum=2,
72
73
  maximum=10,
73
74
  )
75
+ finalize_after = llm.features.get('finalize_after_tool_rounds')
76
+ if 'finalize_after_tool_rounds' in llm.features and (
77
+ type(finalize_after) is not int or not 1 <= finalize_after <= max_tool_rounds
78
+ ):
79
+ raise ValueError('finalize_after_tool_rounds must be an integer between 1 and max_tool_rounds')
74
80
  run_started = time.monotonic()
75
81
 
76
82
  while True:
@@ -192,6 +198,21 @@ class ExecutionBackend:
192
198
  },
193
199
  )
194
200
  if turn.tool_calls:
201
+ if finalization_forced:
202
+ return CliRunResult(
203
+ exit_code=1, stdout='',
204
+ stderr='model requested tools during final-answer phase',
205
+ token_usage=token_usage,
206
+ metadata={
207
+ 'provider': self.name,
208
+ 'provider_error_code': 'tool_finalization_not_honored',
209
+ 'failure_reason': 'tool_finalization_not_honored',
210
+ 'tool_call_count': tool_call_count,
211
+ 'tool_rounds': tool_rounds,
212
+ 'token_usage_details': token_usage_details,
213
+ **_transcript_metadata(transcript),
214
+ },
215
+ )
195
216
  if initial_messages is not None:
196
217
  if turn.finish_reason not in {'tool_calls', 'tool_use'}:
197
218
  return CliRunResult(1, '', 'incomplete tool batch', token_usage=token_usage,
@@ -339,7 +360,13 @@ class ExecutionBackend:
339
360
  if acquiring:
340
361
  messages.append({'role': 'user', 'content': prompt_metadata['final_prompt']})
341
362
  emit('acquisition_completed', tool=initial_tool, turn=turn_number)
342
- if identical_tool_rounds >= max_identical_tool_rounds:
363
+ if finalize_after is not None and tool_rounds >= finalize_after:
364
+ tools_disabled = True
365
+ finalization_forced = True
366
+ messages.append({'role': 'system', 'content':
367
+ 'The tool phase has ended. Return the requested final answer using '
368
+ 'the information available. Do not request any more tools.'})
369
+ elif identical_tool_rounds >= max_identical_tool_rounds:
343
370
  tools_disabled = True
344
371
  tool_no_progress_forced = True
345
372
  initial_guidance = messages[0].get("content")
@@ -0,0 +1,85 @@
1
+ """Cooperative admission and drain for synchronous execution and persistence.
2
+
3
+ Hosts own signal handlers and must wait before closing stores or cancelling work.
4
+ A timeout reports unfinished work; it never cancels or kills admitted execution.
5
+ """
6
+ from contextlib import contextmanager
7
+ from functools import wraps
8
+ import math
9
+ import threading
10
+ import time
11
+
12
+
13
+ class Draining(RuntimeError):
14
+ """New root execution was refused because its lifecycle is draining."""
15
+
16
+
17
+ class ExecutionLifecycle:
18
+ def __init__(self):
19
+ self._condition = threading.Condition()
20
+ self._local = threading.local()
21
+ self._draining = False
22
+ self._active = 0
23
+
24
+ @property
25
+ def draining(self):
26
+ with self._condition:
27
+ return self._draining
28
+
29
+ @property
30
+ def active(self):
31
+ with self._condition:
32
+ return self._active
33
+
34
+ @contextmanager
35
+ def work(self):
36
+ """Admit synchronous work; nested work on the admitted thread may finish."""
37
+ depth = getattr(self._local, 'depth', 0)
38
+ with self._condition:
39
+ if self._draining and not depth:
40
+ raise Draining('service-draining')
41
+ self._active += 1
42
+ self._local.depth = depth + 1
43
+ try:
44
+ yield
45
+ finally:
46
+ self._local.depth = depth
47
+ with self._condition:
48
+ self._active -= 1
49
+ self._condition.notify_all()
50
+
51
+ def begin(self):
52
+ """Permanently fence new root work. Does not cancel admitted work."""
53
+ with self._condition:
54
+ self._draining = True
55
+ self._condition.notify_all()
56
+
57
+ def wait(self, timeout=None):
58
+ """Fence and wait through persistence; return False on an optional timeout.
59
+
60
+ Call from a separate supervisor thread, never inside admitted work.
61
+ Nested admission is thread-scoped, not inherited by new worker threads.
62
+ """
63
+ if getattr(self._local, 'depth', 0):
64
+ raise RuntimeError('cannot wait for own admitted execution')
65
+ if timeout is not None and (isinstance(timeout, bool) or not isinstance(timeout, (int, float))
66
+ or not math.isfinite(timeout) or timeout < 0):
67
+ raise ValueError('timeout must be finite and nonnegative')
68
+ deadline = None if timeout is None else time.monotonic() + timeout
69
+ self.begin()
70
+ with self._condition:
71
+ while self._active:
72
+ remaining = None if deadline is None else deadline - time.monotonic()
73
+ if remaining is not None and remaining <= 0:
74
+ return False
75
+ self._condition.wait(remaining)
76
+ return True
77
+
78
+
79
+ def tracked(method):
80
+ """Track a synchronous operation on its owner's public lifecycle."""
81
+ @wraps(method)
82
+ def execute(self, *args, **kwargs):
83
+ with self.lifecycle.work():
84
+ return method(self, *args, **kwargs)
85
+ return execute
@@ -229,6 +229,7 @@ class OpenAICompatibleBackend(ExecutionBackend):
229
229
 
230
230
  def run_prompt(self, request: CliRunRequest) -> CliRunResult:
231
231
  llm = self._config
232
+ _omlx_tool_envelope(llm.features)
232
233
  assert llm.base_url is not None
233
234
  assert llm.model is not None
234
235
  prompt_metadata = _prompt_metadata(request)
@@ -409,6 +410,12 @@ class OpenAICompatibleBackend(ExecutionBackend):
409
410
  if tools:
410
411
  payload["tools"] = tools
411
412
  payload["tool_choice"] = "auto"
413
+ envelope = _omlx_tool_envelope(llm.features)
414
+ if envelope and tools:
415
+ # oMLX constrains generation; only native tool_calls may execute.
416
+ # An answer JSON grammar cannot also constrain the tool envelope.
417
+ payload.pop('response_format', None)
418
+ payload['structured_outputs'] = {'regex': r'<tool_call>\s*\{[\s\S]*\}\s*</tool_call>'}
412
419
  if streaming:
413
420
  payload["stream"] = True
414
421
  if _feature_enabled(llm.features, "stream_options", default=True):
@@ -1514,6 +1521,16 @@ def _api_key(
1514
1521
  return token, ""
1515
1522
 
1516
1523
 
1524
+ def _omlx_tool_envelope(features):
1525
+ envelope = features.get('omlx_tool_call_envelope', False)
1526
+ if type(envelope) is not bool or (envelope and (
1527
+ type(features.get('finalize_after_tool_rounds')) is not int
1528
+ or features['finalize_after_tool_rounds'] < 1
1529
+ )):
1530
+ raise ValueError('omlx_tool_call_envelope requires a boolean and explicit finalize_after_tool_rounds')
1531
+ return envelope
1532
+
1533
+
1517
1534
  def _feature_enabled(
1518
1535
  features: dict[str, object], name: str, *, default: bool
1519
1536
  ) -> bool:
@@ -23,6 +23,7 @@ from .cli_tools import load_cli_tools, cli_tool_context
23
23
  from .tool_registry import BoundedToolRegistry, DisabledBuiltinRegistry, InvalidToolRequest
24
24
  from .accounting import AccountingError, resolve_context
25
25
  from .accounting_capture import Capture, InvalidEvidence, strict_json
26
+ from .lifecycle import ExecutionLifecycle, tracked
26
27
  from .http_bounds import LimitExceeded, set_response_timeout, BoundedNativeStream
27
28
  from .operation_context import InvocationScope
28
29
  from .tool_effects import admit_tool_effects, ToolEffectFailure
@@ -209,6 +210,10 @@ class _BoundedMixin:
209
210
  observe('provider_request_started')
210
211
  outcome = 'execution_failure'
211
212
  status = None
213
+ if self.capture is None:
214
+ call_id = uuid4().hex
215
+ request.add_header("X-Correlation-ID", call_id)
216
+ request.add_header("X-Client-Request-Id", call_id)
212
217
  opener = urllib.request.build_opener(_NoRedirect())
213
218
  try:
214
219
  # Observers and persisted intent preparation can stop execution.
@@ -338,9 +343,12 @@ class ProsaicRuntime:
338
343
  capabilities = frozenset({'conformance_v1', 'invocation_budgets_v1', 'observer_v1', 'accounting_v1', 'read_receipts_v1', 'initial_tool_v1', 'initial_tool_enforcement_v1', 'acquisition_v1', 'custom_tools_v1', 'cli_tools_v1', 'cli_sandbox_v1'})
339
344
  capabilities |= frozenset({'tool_context_v1', 'tool_journal_v1'})
340
345
  capabilities |= frozenset({'conversation_turn_v1'})
341
- capabilities |= frozenset({'context_assembly_v1'})
346
+ capabilities |= frozenset({'context_assembly_v1', 'http_correlation_v1', 'execution_lifecycle_v1'})
342
347
  def __init__(self, config: RuntimeConfig, *, source=".prosaic", executable="prosaic", custom_tools=None,
343
- accounting=None, context_defaults=None):
348
+ accounting=None, context_defaults=None, lifecycle=None):
349
+ self.lifecycle = lifecycle if lifecycle is not None else ExecutionLifecycle()
350
+ if not isinstance(self.lifecycle, ExecutionLifecycle):
351
+ raise TypeError("lifecycle must be an ExecutionLifecycle")
344
352
  self.config = config
345
353
  self.source = Path(source)
346
354
  self.executable = executable
@@ -371,6 +379,7 @@ class ProsaicRuntime:
371
379
  context_selection=context_selection, context_report=context_report,
372
380
  context_work=context_work)
373
381
 
382
+ @tracked
374
383
  def turn(self, artifact, *, message=None, history=(), input_data=None,
375
384
  policy=None, limits=None, workspace=None, on_event=None, cancelled=None,
376
385
  env=None, context=None, accounting=None, observer=None,
@@ -540,6 +549,7 @@ class ProsaicRuntime:
540
549
  return self._preflight_names(names, str(Path(cwd).resolve()), policy,
541
550
  dict(os.environ if env is None else env), deadline)
542
551
 
552
+ @tracked
543
553
  def run(self, artifact: str | ProsaicArtifact, arguments: str = "", *,
544
554
  cwd: str | Path = '.', policy: RunPolicy | None = None,
545
555
  on_event=None, cancelled=None, env=None,
@@ -8,6 +8,28 @@ from typing import Protocol
8
8
  from .operation_context import InvocationScope, bounded_reference
9
9
 
10
10
 
11
+ @dataclass(frozen=True)
12
+ class ToolFailure:
13
+ """A trusted callback's proven completed failure, never an unknown effect.
14
+
15
+ Return only after establishing that the operation failed deterministically.
16
+ Generic exceptions and lost acknowledgements retain uncertain-effect fencing.
17
+ Codes are safe public machine identifiers, never raw exception text.
18
+ """
19
+ code: str
20
+
21
+ def __post_init__(self):
22
+ if type(self.code) is not str or re.fullmatch(r'[a-z][a-z0-9_]{0,63}', self.code) is None:
23
+ raise ValueError('invalid controlled tool failure code')
24
+
25
+
26
+ def failure_outcome(value):
27
+ if type(value) is ToolFailure:
28
+ ToolFailure.__post_init__(value)
29
+ return {'status': 'error', 'error': value.code}
30
+ return {'status': 'ok', 'result': value}
31
+
32
+
11
33
  @dataclass(frozen=True)
12
34
  class ToolClaim:
13
35
  state: str
@@ -79,7 +101,11 @@ def canonical_json(value):
79
101
 
80
102
  def snapshot_outcome(outcome, maximum):
81
103
  from .tools import depth
82
- if type(outcome) is not dict or set(outcome) != {'status', 'result'} or outcome['status'] != 'ok':
104
+ if type(outcome) is not dict:
105
+ raise ValueError('invalid committed outcome')
106
+ if set(outcome)=={'status','error'} and outcome['status']=='error':
107
+ ToolFailure(outcome['error'])
108
+ elif set(outcome)!={'status','result'} or outcome['status']!='ok':
83
109
  raise ValueError('invalid committed outcome')
84
110
  depth(outcome)
85
111
  serialized = canonical_json(outcome)
@@ -11,7 +11,7 @@ from jsonschema.exceptions import ValidationError, SchemaError
11
11
  from .events import Cancelled
12
12
  from .policy import BUILTIN_TOOLS
13
13
  from .operation_context import ToolExecutionContext, bounded_reference
14
- from .tool_effects import ToolEffectFailure, canonical_json, snapshot_outcome, validate_claim
14
+ from .tool_effects import ToolEffectFailure, canonical_json, snapshot_outcome, validate_claim, failure_outcome
15
15
 
16
16
 
17
17
  class ToolDeadlineExceeded(TimeoutError):
@@ -168,8 +168,8 @@ def parse_arguments(raw, schema, maximum):
168
168
 
169
169
  def bounded_result(value, maximum):
170
170
  try:
171
- depth(value)
172
- envelope = {'status': 'ok', 'result': value}
171
+ envelope = failure_outcome(value)
172
+ depth(envelope['result'] if envelope['status']=='ok' else envelope)
173
173
  text = json.dumps(envelope, allow_nan=False)
174
174
  if len(text.encode('utf-8')) > maximum:
175
175
  return {'status': 'error', 'error': 'result_limit'}
@@ -250,7 +250,7 @@ def execute_custom_tool(tool, raw_arguments, *, check_boundary, scope=None,
250
250
  check_boundary()
251
251
  try:
252
252
  value = invoke(tool.handler, context)
253
- outcome = snapshot_outcome({'status': 'ok', 'result': value}, tool.max_result_bytes)
253
+ outcome = snapshot_outcome(failure_outcome(value), tool.max_result_bytes)
254
254
  except Exception:
255
255
  # Dispatch errors and unusable results cannot establish completion.
256
256
  if on_completion is not None:
@@ -44,7 +44,7 @@ def test_canonical_order_owned_snapshot_and_source_version():
44
44
  encode = lambda value: json.dumps(value, sort_keys=True, ensure_ascii=False,
45
45
  separators=(',', ':'), allow_nan=False)
46
46
  assert encode(a) == encode(b)
47
- assert a['version'] == 1 and a['runtimeVersion'] == '0.9.4'
47
+ assert a['version'] == 1 and a['runtimeVersion'] == '0.9.6'
48
48
  assert tuple(case['id'] for case in a['cases']) == CASES
49
49
  assert a['cases'][0]['origin'] == 'fixture'
50
50
  assert a['cases'][1]['state'] == 'not_run'