b3-prosaic-runtime 0.9.2__tar.gz → 0.9.4__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 (172) hide show
  1. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/CHANGELOG.md +7 -0
  2. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/PKG-INFO +1 -1
  3. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/conversations.md +42 -0
  4. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/pyproject.toml +1 -1
  5. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/scripts/conversation_wheel_smoke.py +18 -3
  6. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/scripts/production_wheel_smoke.py +1 -1
  7. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/PKG-INFO +1 -1
  8. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/SOURCES.txt +9 -0
  9. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/__init__.py +9 -1
  10. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/anthropic.py +4 -1
  11. b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_assembly.py +92 -0
  12. b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_contracts.py +230 -0
  13. b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_counters.py +74 -0
  14. b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_execution.py +70 -0
  15. b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_work.py +89 -0
  16. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/conversation.py +23 -2
  17. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/conversation_preparation.py +42 -4
  18. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/execution.py +19 -18
  19. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/runtime.py +78 -8
  20. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/turn_capture.py +5 -1
  21. b3_prosaic_runtime-0.9.4/tests/support/context_counter.py +25 -0
  22. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conformance.py +1 -1
  23. b3_prosaic_runtime-0.9.4/tests/test_context_assembly.py +174 -0
  24. b3_prosaic_runtime-0.9.4/tests/test_context_contracts.py +132 -0
  25. b3_prosaic_runtime-0.9.4/tests/test_context_execution.py +310 -0
  26. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_turns.py +19 -0
  27. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_distribution_metadata.py +1 -1
  28. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_package_compatibility.py +1 -1
  29. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/LICENSE +0 -0
  30. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/LICENSE-MIT +0 -0
  31. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/MANIFEST.in +0 -0
  32. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/NOTICE.md +0 -0
  33. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/README.md +0 -0
  34. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/accounting.md +0 -0
  35. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/adr-cli-sandbox.md +0 -0
  36. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/cli-tools.md +0 -0
  37. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/conformance.md +0 -0
  38. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/package-index.md +0 -0
  39. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/production-acceptance.md +0 -0
  40. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/release-0.7.1.md +0 -0
  41. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/security.md +0 -0
  42. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/structured-tools.md +0 -0
  43. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/telemetry.md +0 -0
  44. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/tool-effects.md +0 -0
  45. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/workflow-factory-runtime-implementation.md +0 -0
  46. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/acquire-pilot.md +0 -0
  47. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/acquired-reviewer.md +0 -0
  48. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/catalog-reader.md +0 -0
  49. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/cli-spec-reviewer.md +0 -0
  50. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/launch-analyst.md +0 -0
  51. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/launch-briefer.md +0 -0
  52. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/launch-decision.md +0 -0
  53. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/launch-skeptic.md +0 -0
  54. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/native-summarizer.md +0 -0
  55. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/preloaded-reviewer.md +0 -0
  56. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/reviewer.md +0 -0
  57. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/summarizer.md +0 -0
  58. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/tools/analyze-spec.yml +0 -0
  59. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/README.md +0 -0
  60. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/anthropic.yml +0 -0
  61. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/catalog.json +0 -0
  62. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/catalog_tools.py +0 -0
  63. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/cli-tool/prosaic_example_analyzer.py +0 -0
  64. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/cli-tool/pyproject.toml +0 -0
  65. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/cli-tools-sandboxed.yml +0 -0
  66. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/cli-tools.yml +0 -0
  67. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/custom-tools.yml +0 -0
  68. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/launch/brief.md +0 -0
  69. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/launch/field-notes.md +0 -0
  70. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/launch/metrics.md +0 -0
  71. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/pilot.md +0 -0
  72. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/requirements.md +0 -0
  73. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/prosaic-runtime.yaml +0 -0
  74. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_acquisition.py +0 -0
  75. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_cli_tool.py +0 -0
  76. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_custom_tool.py +0 -0
  77. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_examples.py +0 -0
  78. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_sandbox_probe.py +0 -0
  79. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_tiers.py +0 -0
  80. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/tokenproxy.yml +0 -0
  81. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/understanding/.prosaic/subagents/reviewer.md +0 -0
  82. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/understanding/.prosaic/tools/understanding.yml +0 -0
  83. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/understanding/runtime.yml +0 -0
  84. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/with-tools.yml +0 -0
  85. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/scripts/release_artifacts.py +0 -0
  86. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/scripts/wheel_smoke.py +0 -0
  87. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/setup.cfg +0 -0
  88. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/dependency_links.txt +0 -0
  89. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/entry_points.txt +0 -0
  90. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/requires.txt +0 -0
  91. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/top_level.txt +0 -0
  92. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/accounting.py +0 -0
  93. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/accounting_capture.py +0 -0
  94. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/adapter.py +0 -0
  95. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/admission.py +0 -0
  96. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/anthropic_stream.py +0 -0
  97. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/anthropic_usage.py +0 -0
  98. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/artifacts.py +0 -0
  99. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/budgets.py +0 -0
  100. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/cli.py +0 -0
  101. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/cli_tools.py +0 -0
  102. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/config.py +0 -0
  103. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/conformance.py +0 -0
  104. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/console.py +0 -0
  105. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/diagnostics.py +0 -0
  106. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/events.py +0 -0
  107. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/execution_budget.py +0 -0
  108. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/http_bounds.py +0 -0
  109. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible.py +0 -0
  110. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible_compaction.py +0 -0
  111. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible_filters.py +0 -0
  112. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible_progress.py +0 -0
  113. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible_transcript.py +0 -0
  114. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/operation_context.py +0 -0
  115. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/policy.py +0 -0
  116. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/provider_types.py +0 -0
  117. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/sandbox.py +0 -0
  118. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/smoke_prose/pilot.md +0 -0
  119. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/smoke_prose/subagents/native-summarizer.md +0 -0
  120. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/smoke_prose/subagents/reviewer.md +0 -0
  121. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/smoke_prose/subagents/summarizer.md +0 -0
  122. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/structured_tools.py +0 -0
  123. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/telemetry.py +0 -0
  124. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/tool_effects.py +0 -0
  125. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/tool_registry.py +0 -0
  126. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/tools.py +0 -0
  127. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/types.py +0 -0
  128. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/support/conversation_server.py +0 -0
  129. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/support/tool_journal.py +0 -0
  130. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_accounting.py +0 -0
  131. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_acquisition.py +0 -0
  132. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_anthropic.py +0 -0
  133. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_anthropic_accounting.py +0 -0
  134. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_anthropic_package.py +0 -0
  135. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_anthropic_stream.py +0 -0
  136. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_artifact_admission.py +0 -0
  137. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_cli_sandbox.py +0 -0
  138. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_cli_tool_example.py +0 -0
  139. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_cli_tools.py +0 -0
  140. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_cli_tools_boundaries.py +0 -0
  141. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_config.py +0 -0
  142. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_bounds.py +0 -0
  143. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_contracts.py +0 -0
  144. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_distribution.py +0 -0
  145. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_failure_evidence.py +0 -0
  146. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_review_regressions.py +0 -0
  147. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_tool_batches.py +0 -0
  148. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_custom_tool_example.py +0 -0
  149. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_custom_tool_transport.py +0 -0
  150. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_custom_tools.py +0 -0
  151. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_execution_budget.py +0 -0
  152. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_features.py +0 -0
  153. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_http_boundaries.py +0 -0
  154. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_input_boundary.py +0 -0
  155. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_invocation_budgets.py +0 -0
  156. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_production_smoke.py +0 -0
  157. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_prompt_injection.py +0 -0
  158. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_provider_execution.py +0 -0
  159. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_read_provenance.py +0 -0
  160. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_redirect_safety.py +0 -0
  161. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_release_artifacts.py +0 -0
  162. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_run_adapter.py +0 -0
  163. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_runtime.py +0 -0
  164. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_sample_program.py +0 -0
  165. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_sandbox_probe_example.py +0 -0
  166. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_structured_tools.py +0 -0
  167. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_telemetry.py +0 -0
  168. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_tier_examples.py +0 -0
  169. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_tool_effect_workers.py +0 -0
  170. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_tool_effects.py +0 -0
  171. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_transport.py +0 -0
  172. {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_usage_integrity.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3 — 2026-10-14
4
+
5
+ - Normalize negative provider timeout exit statuses to 124 in conversation turn
6
+ results so Harness can persist failed timeout receipts without `store_corrupt`.
7
+ - Preserve existing run/CLI timeout statuses and the conversation storage format.
8
+ - Cover streaming and nonstreaming provider timeouts through the public turn API.
9
+
3
10
  ## 0.9.2 — 2026-10-13
4
11
 
5
12
  - Add a host-owned ExecutionBudget that shares deadlines, provider/tool dispatch
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3-prosaic-runtime
3
- Version: 0.9.2
3
+ Version: 0.9.4
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
@@ -1,5 +1,47 @@
1
1
  # Stateless conversation turns (0.9.0 candidate)
2
2
 
3
+ ## Optional token-aware context admission
4
+
5
+ `context_assembly_v1` adds optional `ContextPolicy` and host-supplied
6
+ `TokenCounterRegistry` arguments to preparation and execution. Existing calls
7
+ keep their version-1 encoding and identity. Opted-in preparation uses
8
+ `conversation-turn-v2` and exposes an owned `context_report`, `context_selection`
9
+ and `context_work`. Supply the same counter registry when executing a prepared
10
+ turn. Durable hosts retain the frozen selection/report and remaining work state.
11
+
12
+ Choose `fullHistory` to reject overflow, or explicitly choose `omitOldestTurns`
13
+ to omit oldest complete turns only from this request. Source history stays
14
+ unchanged. Instructions, current user/inputData, tool schemas and current-turn
15
+ tool exchanges remain mandatory. Tool continuations that cannot fit terminate
16
+ with bounded failure evidence; completed or uncertain effects are preserved and
17
+ never automatically retried. Counters operate on copies of final native payloads.
18
+
19
+ Counters must match the provider, model, pinned tokenizer/template digests and
20
+ framing revision, and certify exact or conservative-upper-bound counts. Runtime
21
+ ships no universal tokenizer or qualified production counter. Synthetic fixture
22
+ counts demonstrate admission behavior only. Provider-reported usage is execution
23
+ accounting evidence; Lago receives billing events separately.
24
+
25
+ Policies also bound candidate checks, cumulative encoded candidate bytes and
26
+ active assembly time. Provider waiting is excluded. Time checks are cooperative:
27
+ trusted counters must be bounded local code without network I/O or inference.
28
+ `context_checkpoint` durably records resource reservations before callbacks;
29
+ `on_admitted` lets the host mark dispatch only after successful readmission and
30
+ all static checks, before accounting intent and provider effects.
31
+
32
+ ```python
33
+ from prosaic_runtime import ContextPolicy
34
+
35
+ # token_counters is a host-qualified immutable TokenCounterRegistry.
36
+ context_policy = ContextPolicy(
37
+ mode='omitOldestTurns', context_window_tokens=32768,
38
+ reserved_output_tokens=2048, safety_margin_tokens=256,
39
+ counter_identity='host-pinned-counter-v1')
40
+ prepared = runtime.prepare_turn(agent, message='Next question', history=history,
41
+ policy=policy, context_policy=context_policy, token_counters=token_counters)
42
+ result = runtime.turn(prepared, token_counters=token_counters)
43
+ ```
44
+
3
45
  `conversation_turn_v1` adds `prepare_turn()` and `turn()` to `ProsaicRuntime`.
4
46
  Runtime owns one bounded model/tool execution. Your host supplies inspected,
5
47
  approved prose, admitted user input and the complete portable history. Durable
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "b3-prosaic-runtime"
7
- version = "0.9.2"
7
+ version = "0.9.4"
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.2'),
25
+ (api, 'b3-prosaic-runtime', '0.9.4'),
26
26
  (recorder, 'b3-prosaic-runtime-postgres', '0.3.0')):
27
27
  owner = metadata.distribution(distribution)
28
28
  assert module.__version__ == owner.version == version
@@ -39,6 +39,7 @@ def ownership():
39
39
  continue
40
40
  raise AssertionError('legacy distribution overlaps public imports')
41
41
  assert 'conversation_turn_v1' in api.ProsaicRuntime.capabilities
42
+ assert 'context_assembly_v1' in api.ProsaicRuntime.capabilities
42
43
  return api
43
44
 
44
45
 
@@ -56,7 +57,7 @@ def worker():
56
57
  self.connection.settimeout(2)
57
58
  def do_POST(self):
58
59
  length = int(self.headers['Content-Length'])
59
- assert 0 < length <= 196608 and len(requests) < 2
60
+ assert 0 < length <= 196608 and len(requests) < 3
60
61
  requests.append(json.loads(self.rfile.read(length)))
61
62
  text = ('Prague' if len(requests) == 1 else 'Prague again')
62
63
  body = json.dumps({'choices': [{'message': {'role': 'assistant', 'content': text},
@@ -93,9 +94,23 @@ def worker():
93
94
  history = requests[1]['messages']
94
95
  assert [m['role'] for m in history] == ['system', 'user', 'assistant', 'user']
95
96
  assert history[2]['content'] == 'Prague'
97
+ class FixtureCounter:
98
+ def count_tokens(self, request):
99
+ return 250 if 'Which city?' in json.dumps(request) else 10
100
+ descriptor = api.TokenCounterDescriptor('synthetic-v1', 'openai-compatible',
101
+ 'synthetic', 'openai-chat-v1', 200, '1', '0'*64, '1'*64,
102
+ 'exact', 'Exact units for this synthetic test grammar only.')
103
+ counters = api.TokenCounterRegistry({'synthetic-v1':
104
+ api.TokenCounterRegistration(descriptor, FixtureCounter())})
105
+ context_policy = api.ContextPolicy('omitOldestTurns', 200, 20, 0, 'synthetic-v1')
106
+ fitted = runtime.turn(agent, message='Now?', history=first.completed_turn.messages,
107
+ policy=policy, context_policy=context_policy, token_counters=counters)
108
+ assert fitted.result.exit_code == 0 and len(requests) == 3
109
+ assert fitted.context_report.omitted_turn_count == 1
110
+ assert 'Which city?' not in json.dumps(requests[2])
96
111
  ownership()
97
112
  print(json.dumps({'version': 1, 'runtimeVersion': api.__version__,
98
- 'checks': ['installedOwnership', 'purePreparation', 'twoTurnHistory', 'noWorkspace'],
113
+ 'checks': ['installedOwnership', 'purePreparation', 'twoTurnHistory', 'noWorkspace', 'syntheticContextAssembly'],
99
114
  'providerRequests': len(requests), 'liveProvider': False}))
100
115
  finally:
101
116
  server.shutdown()
@@ -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.2"),
52
+ (runtime, "b3-prosaic-runtime", "0.9.4"),
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.2
3
+ Version: 0.9.4
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
@@ -82,6 +82,11 @@ src/prosaic_runtime/cli_tools.py
82
82
  src/prosaic_runtime/config.py
83
83
  src/prosaic_runtime/conformance.py
84
84
  src/prosaic_runtime/console.py
85
+ src/prosaic_runtime/context_assembly.py
86
+ src/prosaic_runtime/context_contracts.py
87
+ src/prosaic_runtime/context_counters.py
88
+ src/prosaic_runtime/context_execution.py
89
+ src/prosaic_runtime/context_work.py
85
90
  src/prosaic_runtime/conversation.py
86
91
  src/prosaic_runtime/conversation_preparation.py
87
92
  src/prosaic_runtime/diagnostics.py
@@ -123,6 +128,9 @@ tests/test_cli_tools.py
123
128
  tests/test_cli_tools_boundaries.py
124
129
  tests/test_config.py
125
130
  tests/test_conformance.py
131
+ tests/test_context_assembly.py
132
+ tests/test_context_contracts.py
133
+ tests/test_context_execution.py
126
134
  tests/test_conversation_bounds.py
127
135
  tests/test_conversation_contracts.py
128
136
  tests/test_conversation_distribution.py
@@ -157,5 +165,6 @@ tests/test_tool_effect_workers.py
157
165
  tests/test_tool_effects.py
158
166
  tests/test_transport.py
159
167
  tests/test_usage_integrity.py
168
+ tests/support/context_counter.py
160
169
  tests/support/conversation_server.py
161
170
  tests/support/tool_journal.py
@@ -1,5 +1,5 @@
1
1
  """Execute neutral Prosaic artifacts with explicitly granted authority."""
2
- __version__ = '0.9.2'
2
+ __version__ = '0.9.4'
3
3
 
4
4
  from .artifacts import ProsaicArtifact
5
5
  from .config import EndpointConfig, RuntimeConfig, RunLimits, CliSandboxConfig
@@ -36,3 +36,11 @@ __all__ += ["RuntimeAdapter"]
36
36
 
37
37
  from .execution_budget import ExecutionBudget
38
38
  __all__ += ["ExecutionBudget"]
39
+
40
+ from .context_contracts import (ContextPolicy, TokenCounterDescriptor,
41
+ ContextAssemblyReport, ContextSelection, ContextWorkState)
42
+ from .context_counters import (TokenCounter, TokenCounterRegistration,
43
+ TokenCounterRegistry)
44
+ __all__ += ["ContextPolicy", "TokenCounterDescriptor", "ContextAssemblyReport",
45
+ "ContextSelection", "ContextWorkState", "TokenCounter", "TokenCounterRegistration",
46
+ "TokenCounterRegistry"]
@@ -177,8 +177,11 @@ class AnthropicBackend(ExecutionBackend):
177
177
  payload.pop('stream_options', None)
178
178
  return payload
179
179
 
180
+ def native_payload(self, payload):
181
+ return encode_messages(payload)
182
+
180
183
  def _post_chat_turn(self, payload, request, deadline, streaming):
181
- native = encode_messages(payload)
184
+ native = self.native_payload(payload)
182
185
  self.validate_payload(native)
183
186
  data = self.encode_payload(native)
184
187
  token, error = _api_key(self._config.api_key_env, self._config.api_key_file, request.env)
@@ -0,0 +1,92 @@
1
+ """Full-history-first suffix fitting over validated owned neutral turns."""
2
+ import hashlib
3
+ import json
4
+ from .conversation import TurnAdmissionError, encode_owned
5
+ from .context_contracts import ContextAssemblyReport, ContextSelection, ContextWorkState
6
+ from .context_counters import count_owned
7
+ from .context_work import ContextWorkBudget
8
+
9
+
10
+ def context_digest(value):
11
+ return hashlib.sha256(encode_owned(value)).hexdigest()
12
+
13
+
14
+ def _boundaries(history):
15
+ return [index for index, message in enumerate(history) if message['role'] == 'user']
16
+
17
+
18
+ def assemble_context(source_history, build_candidate, policy, registration, limits, *, work=None):
19
+ budget = ContextWorkBudget(policy, work=work)
20
+ boundaries = _boundaries(source_history)
21
+ total, omitted = len(boundaries), 0
22
+ while True:
23
+ start = boundaries[omitted] if omitted < total else len(source_history)
24
+ selected = source_history[start:]
25
+ def construct():
26
+ candidate = build_candidate(selected)
27
+ size = len(json.dumps(candidate, allow_nan=False).encode('utf-8'))
28
+ return candidate, size
29
+ native, size = budget.construct(construct)
30
+ callback = (lambda: count_owned(registration, native)) if size <= limits.max_input_bytes else None
31
+ count = budget.check_candidate(size, callback)
32
+ if count is not None and count <= policy.input_capacity_tokens:
33
+ budget.reserve_readmission(size)
34
+ report = ContextAssemblyReport.from_dict({'version': 1, 'mode': policy.mode,
35
+ 'counterIdentity': policy.counter_identity,
36
+ 'sourceHistoryDigest': context_digest(source_history),
37
+ 'selectedHistoryDigest': context_digest(selected),
38
+ 'inputTokens': count, 'countingKind': registration.descriptor.counting_kind,
39
+ 'reservedOutputTokens': policy.reserved_output_tokens,
40
+ 'safetyMarginTokens': policy.safety_margin_tokens,
41
+ 'inputCapacityTokens': policy.input_capacity_tokens,
42
+ 'encodedInputBytes': size, 'retainedTurnCount': total - omitted,
43
+ 'omittedTurnCount': omitted,
44
+ 'retainedRange': {'startTurn': omitted + 1, 'endTurnExclusive': total + 1} if omitted < total else None,
45
+ 'omittedRange': {'startTurn': 1, 'endTurnExclusive': omitted + 1} if omitted else None,
46
+ 'candidateChecks': budget.state.candidate_checks,
47
+ 'candidateBytes': budget.state.candidate_bytes})
48
+ selection = ContextSelection.from_dict({'version': 1,
49
+ 'contextPolicyIdentity': context_digest(policy.to_dict()),
50
+ 'counterDescriptorIdentity': context_digest(registration.descriptor.to_dict()),
51
+ 'sourceHistoryDigest': report.source_history_digest,
52
+ 'selectedHistoryDigest': report.selected_history_digest,
53
+ 'retainedRange': report.retained_range, 'omittedRange': report.omitted_range,
54
+ 'initialReportIdentity': context_digest(report.to_dict())})
55
+ return selected, report, selection, budget.state
56
+ if policy.mode == 'fullHistory' or omitted == total:
57
+ raise TurnAdmissionError('contextLimit',
58
+ 'maxInputBytes' if count is None else 'contextWindowTokens')
59
+ omitted += 1
60
+
61
+
62
+ def restore_selection(source_history, policy, registration, selection, report, work):
63
+ try:
64
+ if (type(selection) is not ContextSelection or type(report) is not ContextAssemblyReport
65
+ or type(work) is not ContextWorkState):
66
+ raise ValueError('context records')
67
+ boundaries = _boundaries(source_history)
68
+ omitted = report.omitted_turn_count
69
+ if omitted > len(boundaries):
70
+ raise ValueError('omission range')
71
+ start = boundaries[omitted] if omitted < len(boundaries) else len(source_history)
72
+ selected = source_history[start:]
73
+ expected = {'version': 1, 'contextPolicyIdentity': context_digest(policy.to_dict()),
74
+ 'counterDescriptorIdentity': context_digest(registration.descriptor.to_dict()),
75
+ 'sourceHistoryDigest': context_digest(source_history),
76
+ 'selectedHistoryDigest': context_digest(selected),
77
+ 'retainedRange': report.retained_range, 'omittedRange': report.omitted_range,
78
+ 'initialReportIdentity': context_digest(report.to_dict())}
79
+ if (selection.to_dict() != expected or report.source_history_digest != expected['sourceHistoryDigest']
80
+ or report.selected_history_digest != expected['selectedHistoryDigest']
81
+ or report.retained_turn_count != len(boundaries) - omitted
82
+ or report.mode != policy.mode or report.counter_identity != policy.counter_identity
83
+ or report.input_capacity_tokens != policy.input_capacity_tokens
84
+ or report.reserved_output_tokens != policy.reserved_output_tokens
85
+ or report.safety_margin_tokens != policy.safety_margin_tokens
86
+ or report.counting_kind != registration.descriptor.counting_kind
87
+ or work.candidate_checks < report.candidate_checks
88
+ or work.candidate_bytes < report.candidate_bytes):
89
+ raise ValueError('selection binding')
90
+ return selected, report, selection, work
91
+ except (ValueError, TypeError, AttributeError):
92
+ raise TurnAdmissionError('bindingMismatch') from None
@@ -0,0 +1,230 @@
1
+ """Optional owned context-admission contracts; no executable serialization."""
2
+ from dataclasses import dataclass, fields
3
+ import json
4
+ import re
5
+
6
+ from .conversation import TurnAdmissionError, encode_owned, _OwnedRecord
7
+ from .operation_context import bounded_reference
8
+
9
+
10
+ MAX_INTEGER = 2**63 - 1
11
+ FRAMING_REVISIONS = {'openai-compatible': 'openai-chat-v1',
12
+ 'anthropic': 'anthropic-messages-v1'}
13
+
14
+
15
+ def _camel(name):
16
+ head, *tail = name.split('_')
17
+ return head + ''.join(piece.title() for piece in tail)
18
+
19
+
20
+ def _integer(value, minimum=0, maximum=MAX_INTEGER):
21
+ if type(value) is not int or not minimum <= value <= maximum:
22
+ raise ValueError('integer bound')
23
+
24
+
25
+ def _digest(value):
26
+ if type(value) is not str or not re.fullmatch('[0-9a-f]{64}', value):
27
+ raise ValueError('digest')
28
+
29
+
30
+ def _reference(value):
31
+ if not bounded_reference(value):
32
+ raise ValueError('reference')
33
+
34
+
35
+ def _closed(document, keys):
36
+ if type(document) is not dict or document.keys() != set(keys):
37
+ raise ValueError('closed fields')
38
+
39
+
40
+ def _owned(document, *, maximum=16_384, nodes=512):
41
+ return json.loads(encode_owned(document, max_bytes=maximum,
42
+ max_depth=16, max_nodes=nodes))
43
+
44
+
45
+ def _range(value):
46
+ if value is None:
47
+ return 0
48
+ _closed(value, ('startTurn', 'endTurnExclusive'))
49
+ _integer(value['startTurn'], 1)
50
+ _integer(value['endTurnExclusive'], value['startTurn'] + 1)
51
+ return value['endTurnExclusive'] - value['startTurn']
52
+
53
+
54
+ class _DocumentMixin:
55
+ def to_dict(self):
56
+ try:
57
+ document = {_camel(f.name): getattr(self, f.name) for f in fields(self)}
58
+ return _owned(document)
59
+ except (ValueError, TypeError, UnicodeError, RecursionError):
60
+ raise TurnAdmissionError('invalidPolicy') from None
61
+
62
+ @classmethod
63
+ def from_dict(cls, document):
64
+ try:
65
+ document = _owned(document)
66
+ names = {_camel(f.name): f.name for f in fields(cls)}
67
+ _closed(document, names)
68
+ return cls(**{names[key]: value for key, value in document.items()})
69
+ except (ValueError, TypeError, UnicodeError, RecursionError):
70
+ raise TurnAdmissionError('invalidPolicy') from None
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class ContextPolicy(_DocumentMixin):
75
+ mode: str
76
+ context_window_tokens: int
77
+ reserved_output_tokens: int
78
+ safety_margin_tokens: int
79
+ counter_identity: str
80
+ max_candidate_checks: int = 128
81
+ max_assembly_bytes: int = 33_554_432
82
+ assembly_timeout_ms: int = 2_000
83
+
84
+ def __post_init__(self):
85
+ try:
86
+ if type(self.mode) is not str or self.mode not in {'fullHistory', 'omitOldestTurns'}:
87
+ raise ValueError('mode')
88
+ _integer(self.context_window_tokens, 1)
89
+ _integer(self.reserved_output_tokens, 1)
90
+ _integer(self.safety_margin_tokens)
91
+ _integer(self.max_candidate_checks, 2, 256)
92
+ _integer(self.max_assembly_bytes, 1, 268_435_456)
93
+ _integer(self.assembly_timeout_ms, 1, 30_000)
94
+ _reference(self.counter_identity)
95
+ if self.reserved_output_tokens + self.safety_margin_tokens >= self.context_window_tokens:
96
+ raise ValueError('input capacity')
97
+ self.to_dict()
98
+ except (ValueError, TypeError, UnicodeError):
99
+ raise TurnAdmissionError('invalidPolicy') from None
100
+
101
+ @property
102
+ def input_capacity_tokens(self):
103
+ return self.context_window_tokens - self.reserved_output_tokens - self.safety_margin_tokens
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class TokenCounterDescriptor(_DocumentMixin):
108
+ identity: str
109
+ provider: str
110
+ model: str
111
+ framing_revision: str
112
+ context_capacity_tokens: int
113
+ implementation_version: str
114
+ tokenizer_assets_digest: str
115
+ template_assets_digest: str
116
+ counting_kind: str
117
+ guarantee: str
118
+
119
+ def __post_init__(self):
120
+ try:
121
+ for name in ('identity', 'model', 'implementation_version'):
122
+ _reference(getattr(self, name))
123
+ if (type(self.provider) is not str or self.provider not in FRAMING_REVISIONS
124
+ or self.framing_revision != FRAMING_REVISIONS[self.provider]):
125
+ raise ValueError('unsupported framing')
126
+ _integer(self.context_capacity_tokens, 1)
127
+ _digest(self.tokenizer_assets_digest)
128
+ _digest(self.template_assets_digest)
129
+ if self.counting_kind not in {'exact', 'conservativeUpperBound'}:
130
+ raise ValueError('counting guarantee')
131
+ if (type(self.guarantee) is not str or not self.guarantee.strip()
132
+ or len(self.guarantee.encode('utf-8')) > 8192):
133
+ raise ValueError('guarantee bound')
134
+ self.to_dict()
135
+ except (ValueError, TypeError, UnicodeError):
136
+ raise TurnAdmissionError('invalidPolicy') from None
137
+
138
+
139
+ class _ContextRecord(_OwnedRecord):
140
+ maximum = 16_384
141
+ nodes = 512
142
+ keys = frozenset()
143
+
144
+ def __setattr__(self, name, value):
145
+ raise AttributeError('immutable context record')
146
+
147
+ def __delattr__(self, name):
148
+ raise AttributeError('immutable context record')
149
+
150
+ @classmethod
151
+ def from_dict(cls, document):
152
+ try:
153
+ document = _owned(document, maximum=cls.maximum, nodes=cls.nodes)
154
+ _closed(document, cls.keys)
155
+ cls.validate(document)
156
+ return cls._create(encode_owned(document, max_bytes=cls.maximum,
157
+ max_depth=16, max_nodes=cls.nodes))
158
+ except (ValueError, TypeError, KeyError, UnicodeError, RecursionError):
159
+ raise TurnAdmissionError('invalidPolicy') from None
160
+
161
+ def __getattr__(self, name):
162
+ key = _camel(name)
163
+ if key not in self.keys:
164
+ raise AttributeError(name)
165
+ return self.to_dict()[key]
166
+
167
+
168
+ class ContextAssemblyReport(_ContextRecord):
169
+ keys = frozenset({'version', 'mode', 'counterIdentity', 'sourceHistoryDigest',
170
+ 'selectedHistoryDigest', 'inputTokens', 'countingKind', 'reservedOutputTokens',
171
+ 'safetyMarginTokens', 'inputCapacityTokens', 'encodedInputBytes',
172
+ 'retainedTurnCount', 'omittedTurnCount', 'retainedRange', 'omittedRange',
173
+ 'candidateChecks', 'candidateBytes'})
174
+
175
+ @staticmethod
176
+ def validate(d):
177
+ _integer(d['version'], 1, 1)
178
+ if d['mode'] not in {'fullHistory', 'omitOldestTurns'} or d['countingKind'] not in {'exact', 'conservativeUpperBound'}:
179
+ raise ValueError('report enum')
180
+ _reference(d['counterIdentity'])
181
+ _digest(d['sourceHistoryDigest'])
182
+ _digest(d['selectedHistoryDigest'])
183
+ for name in ('inputTokens', 'safetyMarginTokens', 'retainedTurnCount',
184
+ 'omittedTurnCount', 'candidateBytes'):
185
+ _integer(d[name])
186
+ for name in ('reservedOutputTokens', 'inputCapacityTokens', 'encodedInputBytes', 'candidateChecks'):
187
+ _integer(d[name], 1)
188
+ if (d['inputTokens'] > d['inputCapacityTokens'] or
189
+ _range(d['retainedRange']) != d['retainedTurnCount'] or
190
+ _range(d['omittedRange']) != d['omittedTurnCount']):
191
+ raise ValueError('report capacity/range')
192
+ omitted = d['omittedTurnCount']
193
+ if ((d['omittedRange'] is not None and d['omittedRange']['startTurn'] != 1) or
194
+ (d['retainedRange'] is not None and d['retainedRange']['startTurn'] != omitted + 1) or
195
+ (d['mode'] == 'fullHistory' and omitted)):
196
+ raise ValueError('report partition')
197
+
198
+
199
+ class ContextSelection(_ContextRecord):
200
+ maximum, nodes = 8192, 256
201
+ keys = frozenset({'version', 'contextPolicyIdentity', 'counterDescriptorIdentity',
202
+ 'sourceHistoryDigest', 'selectedHistoryDigest', 'retainedRange',
203
+ 'omittedRange', 'initialReportIdentity'})
204
+
205
+ @staticmethod
206
+ def validate(d):
207
+ _integer(d['version'], 1, 1)
208
+ for name in ('contextPolicyIdentity', 'counterDescriptorIdentity',
209
+ 'sourceHistoryDigest', 'selectedHistoryDigest', 'initialReportIdentity'):
210
+ _digest(d[name])
211
+ omitted = _range(d['omittedRange'])
212
+ _range(d['retainedRange'])
213
+ if ((d['omittedRange'] is not None and d['omittedRange']['startTurn'] != 1) or
214
+ (d['retainedRange'] is not None and d['retainedRange']['startTurn'] != omitted + 1)):
215
+ raise ValueError('selection partition')
216
+
217
+
218
+ class ContextWorkState(_ContextRecord):
219
+ maximum, nodes = 4096, 128
220
+ keys = frozenset({'version', 'candidateChecks', 'candidateBytes',
221
+ 'activeAssemblyMicros', 'reservedActiveMicros', 'readmissionCandidateBytes',
222
+ 'readmissionPending', 'checkpointSequence'})
223
+
224
+ @staticmethod
225
+ def validate(d):
226
+ _integer(d['version'], 1, 1)
227
+ for name in ContextWorkState.keys - {'version', 'readmissionPending'}:
228
+ _integer(d[name])
229
+ if type(d['readmissionPending']) is not bool:
230
+ raise ValueError('readmission flag')
@@ -0,0 +1,74 @@
1
+ """Trusted local counting implementations with immutable admitted descriptors."""
2
+ from collections.abc import Mapping
3
+ from dataclasses import dataclass
4
+ import json
5
+ from types import MappingProxyType
6
+ from typing import Protocol
7
+
8
+ from .conversation import TurnAdmissionError, encode_owned
9
+ from .context_contracts import ContextPolicy, TokenCounterDescriptor, MAX_INTEGER
10
+
11
+
12
+ class TokenCounter(Protocol):
13
+ def count_tokens(self, request: dict[str, object]) -> int: ...
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class TokenCounterRegistration:
18
+ descriptor: TokenCounterDescriptor
19
+ counter: TokenCounter
20
+
21
+ def __post_init__(self):
22
+ if type(self.descriptor) is not TokenCounterDescriptor or not callable(getattr(self.counter, 'count_tokens', None)):
23
+ raise TurnAdmissionError('invalidPolicy')
24
+ object.__setattr__(self, 'descriptor', TokenCounterDescriptor.from_dict(self.descriptor.to_dict()))
25
+
26
+
27
+ class TokenCounterRegistry:
28
+ __slots__ = ('_registrations',)
29
+
30
+ def __init__(self, registrations):
31
+ try:
32
+ if not isinstance(registrations, Mapping) or len(registrations) > 256:
33
+ raise ValueError('registry bounds')
34
+ owned = {}
35
+ for identity, entry in registrations.items():
36
+ if (type(entry) is not TokenCounterRegistration or type(identity) is not str
37
+ or identity != entry.descriptor.identity):
38
+ raise ValueError('registration identity')
39
+ owned[identity] = TokenCounterRegistration(entry.descriptor, entry.counter)
40
+ object.__setattr__(self, '_registrations', MappingProxyType(owned))
41
+ except (ValueError, TypeError, AttributeError):
42
+ raise TurnAdmissionError('invalidPolicy') from None
43
+
44
+ def __setattr__(self, name, value):
45
+ raise AttributeError('immutable registry')
46
+
47
+ def resolve(self, identity):
48
+ try:
49
+ return self._registrations[identity]
50
+ except (KeyError, TypeError):
51
+ raise TurnAdmissionError('invalidPolicy') from None
52
+
53
+
54
+ def admit_counter(policy, registry, endpoint):
55
+ if type(policy) is not ContextPolicy or type(registry) is not TokenCounterRegistry:
56
+ raise TurnAdmissionError('invalidPolicy')
57
+ registration = registry.resolve(policy.counter_identity)
58
+ descriptor = registration.descriptor
59
+ if (descriptor.provider, descriptor.model) != (endpoint.provider, endpoint.model):
60
+ raise TurnAdmissionError('unsupportedCapability')
61
+ if policy.context_window_tokens > descriptor.context_capacity_tokens:
62
+ raise TurnAdmissionError('invalidPolicy')
63
+ return registration
64
+
65
+
66
+ def count_owned(registration, native_request):
67
+ try:
68
+ request = json.loads(encode_owned(native_request))
69
+ count = registration.counter.count_tokens(request)
70
+ if type(count) is not int or not 0 <= count <= MAX_INTEGER:
71
+ raise ValueError('count bound')
72
+ return count
73
+ except Exception:
74
+ raise TurnAdmissionError('invalidPolicy') from None