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.
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/CHANGELOG.md +7 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/PKG-INFO +1 -1
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/conversations.md +42 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/pyproject.toml +1 -1
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/scripts/conversation_wheel_smoke.py +18 -3
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/scripts/production_wheel_smoke.py +1 -1
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/PKG-INFO +1 -1
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/SOURCES.txt +9 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/__init__.py +9 -1
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/anthropic.py +4 -1
- b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_assembly.py +92 -0
- b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_contracts.py +230 -0
- b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_counters.py +74 -0
- b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_execution.py +70 -0
- b3_prosaic_runtime-0.9.4/src/prosaic_runtime/context_work.py +89 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/conversation.py +23 -2
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/conversation_preparation.py +42 -4
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/execution.py +19 -18
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/runtime.py +78 -8
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/turn_capture.py +5 -1
- b3_prosaic_runtime-0.9.4/tests/support/context_counter.py +25 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conformance.py +1 -1
- b3_prosaic_runtime-0.9.4/tests/test_context_assembly.py +174 -0
- b3_prosaic_runtime-0.9.4/tests/test_context_contracts.py +132 -0
- b3_prosaic_runtime-0.9.4/tests/test_context_execution.py +310 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_turns.py +19 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_distribution_metadata.py +1 -1
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_package_compatibility.py +1 -1
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/LICENSE +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/LICENSE-MIT +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/MANIFEST.in +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/NOTICE.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/README.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/accounting.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/adr-cli-sandbox.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/cli-tools.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/conformance.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/package-index.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/production-acceptance.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/release-0.7.1.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/security.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/structured-tools.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/telemetry.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/tool-effects.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/docs/workflow-factory-runtime-implementation.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/acquire-pilot.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/acquired-reviewer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/catalog-reader.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/cli-spec-reviewer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/launch-analyst.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/launch-briefer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/launch-decision.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/launch-skeptic.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/native-summarizer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/preloaded-reviewer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/reviewer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/subagents/summarizer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/.prosaic/tools/analyze-spec.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/README.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/anthropic.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/catalog.json +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/catalog_tools.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/cli-tool/prosaic_example_analyzer.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/cli-tool/pyproject.toml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/cli-tools-sandboxed.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/cli-tools.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/custom-tools.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/launch/brief.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/launch/field-notes.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/launch/metrics.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/pilot.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/evidence/requirements.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/prosaic-runtime.yaml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_acquisition.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_cli_tool.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_custom_tool.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_examples.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_sandbox_probe.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/run_tiers.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/tokenproxy.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/understanding/.prosaic/subagents/reviewer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/understanding/.prosaic/tools/understanding.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/understanding/runtime.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/examples/with-tools.yml +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/scripts/release_artifacts.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/scripts/wheel_smoke.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/setup.cfg +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/dependency_links.txt +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/entry_points.txt +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/requires.txt +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/top_level.txt +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/accounting.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/accounting_capture.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/adapter.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/admission.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/anthropic_stream.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/anthropic_usage.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/artifacts.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/budgets.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/cli.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/cli_tools.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/config.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/conformance.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/console.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/diagnostics.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/events.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/execution_budget.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/http_bounds.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible_compaction.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible_filters.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible_progress.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/openai_compatible_transcript.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/operation_context.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/policy.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/provider_types.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/sandbox.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/smoke_prose/pilot.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/smoke_prose/subagents/native-summarizer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/smoke_prose/subagents/reviewer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/smoke_prose/subagents/summarizer.md +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/structured_tools.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/telemetry.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/tool_effects.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/tool_registry.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/tools.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/prosaic_runtime/types.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/support/conversation_server.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/support/tool_journal.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_accounting.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_acquisition.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_anthropic.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_anthropic_accounting.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_anthropic_package.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_anthropic_stream.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_artifact_admission.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_cli_sandbox.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_cli_tool_example.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_cli_tools.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_cli_tools_boundaries.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_config.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_bounds.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_contracts.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_distribution.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_failure_evidence.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_review_regressions.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_conversation_tool_batches.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_custom_tool_example.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_custom_tool_transport.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_custom_tools.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_execution_budget.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_features.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_http_boundaries.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_input_boundary.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_invocation_budgets.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_production_smoke.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_prompt_injection.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_provider_execution.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_read_provenance.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_redirect_safety.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_release_artifacts.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_run_adapter.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_runtime.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_sample_program.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_sandbox_probe_example.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_structured_tools.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_telemetry.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_tier_examples.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_tool_effect_workers.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_tool_effects.py +0 -0
- {b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/tests/test_transport.py +0 -0
- {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,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.
|
|
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.
|
|
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) <
|
|
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.
|
|
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
|
{b3_prosaic_runtime-0.9.2 → b3_prosaic_runtime-0.9.4}/src/b3_prosaic_runtime.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
+
__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 =
|
|
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
|