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