From 8fccc92947c9ac007955a1e1c141e05a2737047f Mon Sep 17 00:00:00 2001 From: Renato Date: Thu, 6 Aug 2026 10:24:36 +0200 Subject: [PATCH 1/3] feat: add v0.2 learning loop and privacy-safe telemetry --- .github/ISSUE_TEMPLATE/bug_report.yml | 12 +- .github/pull_request_template.md | 11 + .github/workflows/ci.yml | 39 +- .github/workflows/pages.yml | 35 - .github/workflows/release.yml | 44 +- .gitignore | 5 +- ACKNOWLEDGMENTS.md | 11 +- CHANGELOG.md | 87 +- CITATION.cff | 10 +- CONTRIBUTING.md | 81 +- LICENSE | 2 +- MANIFEST.in | 7 +- README.md | 649 ++++---- ROADMAP.md | 114 +- SECURITY.md | 83 +- apply_roadmap_update.py | 67 - assets/marginal-project-mark.png | Bin 144758 -> 0 bytes assets/marginal-social-preview.png | Bin 347894 -> 0 bytes codemeta.json | 12 +- demos/killer-demo/README.md | 19 - demos/killer-demo/index.html | 4 +- demos/killer-demo/trace.jsonl | 18 +- docs/api.md | 235 ++- docs/architecture.md | 119 +- docs/benchmarking.md | 68 +- docs/concepts.md | 95 +- docs/faq.md | 64 +- docs/governance.md | 11 +- docs/index.md | 11 +- docs/integrations.md | 103 +- docs/learning-loop.md | 70 + docs/privacy.md | 191 +++ docs/public-benchmarks.md | 76 +- docs/quickstart.md | 161 +- .../2026-08-06-learning-loop-foundation.md | 186 +++ .../plans/2026-08-06-privacy-profiles.md | 123 ++ ...6-08-06-learning-loop-foundation-design.md | 77 + .../2026-08-06-privacy-profiles-design.md | 130 ++ docs/universal-runtime.md | 71 + examples/privacy_profiles.py | 68 + examples/public_eval/baseline.jsonl | 2 - examples/public_eval/marginal.jsonl | 2 - examples/shadow_mode.py | 41 + examples/universal_runtime.py | 36 + poetry.lock | 1314 ----------------- pyproject.toml | 12 +- schemas/agent-capabilities-v1.json | 24 + schemas/agent-decision-v1.json | 47 + schemas/agent-event-v1.json | 111 ++ schemas/aggregate-export-v1.json | 95 ++ schemas/decision-ledger-v2.json | 78 + schemas/outcome-v1.json | 17 + schemas/safe-telemetry-v1.json | 392 +++++ schemas/token-usage-v2.json | 21 + src/marginal/__init__.py | 96 +- src/marginal/adapters.py | 186 ++- src/marginal/budget.py | 62 +- src/marginal/cli.py | 164 +- src/marginal/estimator.py | 236 ++- src/marginal/killer_demo.py | 4 +- src/marginal/ledger.py | 377 +++++ src/marginal/models.py | 95 +- src/marginal/modes.py | 30 + src/marginal/outcomes.py | 63 + src/marginal/policy.py | 146 +- src/marginal/privacy.py | 804 ++++++++++ src/marginal/profiles.py | 76 + src/marginal/protocol.py | 499 +++++++ src/marginal/public_eval.py | 129 +- src/marginal/registry.py | 42 + src/marginal/replay.py | 141 ++ src/marginal/runtime.py | 102 ++ src/marginal/schema.py | 41 + src/marginal/schemas/__init__.py | 1 + .../schemas/agent-capabilities-v1.json | 24 + src/marginal/schemas/agent-decision-v1.json | 47 + src/marginal/schemas/agent-event-v1.json | 111 ++ src/marginal/schemas/aggregate-export-v1.json | 95 ++ src/marginal/schemas/decision-ledger-v2.json | 78 + src/marginal/schemas/outcome-v1.json | 17 + src/marginal/schemas/safe-telemetry-v1.json | 392 +++++ src/marginal/schemas/token-usage-v2.json | 21 + src/marginal/trace.py | 20 +- src/marginal/treasury.py | 316 +++- tests/test_adapters_regression.py | 56 + tests/test_budget_regression.py | 46 + tests/test_cli.py | 21 - tests/test_cli_v2.py | 175 +++ tests/test_decision_ledger.py | 449 ++++++ tests/test_estimator_v2.py | 136 ++ tests/test_failure_settlement.py | 112 ++ tests/test_killer_demo.py | 18 + tests/test_models_v2.py | 101 ++ tests/test_packaged_schemas_v2.py | 36 + tests/test_policy_regression.py | 73 + tests/test_policy_v2.py | 51 + tests/test_privacy.py | 469 ++++++ tests/test_protocol.py | 286 ++++ tests/test_public_api_v2.py | 90 ++ tests/test_public_eval.py | 92 -- tests/test_public_eval_v2.py | 108 ++ tests/test_replay.py | 95 ++ tests/test_repository_consistency_v2.py | 78 + tests/test_runtime.py | 192 +++ tests/test_schema_conformance_v2.py | 192 +++ tests/test_shadow_mode.py | 100 ++ tests/test_trace_regression.py | 22 + tests/test_treasury_regression.py | 87 ++ 108 files changed, 10169 insertions(+), 2692 deletions(-) delete mode 100644 .github/workflows/pages.yml delete mode 100644 apply_roadmap_update.py delete mode 100644 assets/marginal-project-mark.png delete mode 100644 assets/marginal-social-preview.png delete mode 100644 demos/killer-demo/README.md create mode 100644 docs/learning-loop.md create mode 100644 docs/privacy.md create mode 100644 docs/superpowers/plans/2026-08-06-learning-loop-foundation.md create mode 100644 docs/superpowers/plans/2026-08-06-privacy-profiles.md create mode 100644 docs/superpowers/specs/2026-08-06-learning-loop-foundation-design.md create mode 100644 docs/superpowers/specs/2026-08-06-privacy-profiles-design.md create mode 100644 docs/universal-runtime.md create mode 100644 examples/privacy_profiles.py delete mode 100644 examples/public_eval/baseline.jsonl delete mode 100644 examples/public_eval/marginal.jsonl create mode 100644 examples/shadow_mode.py create mode 100644 examples/universal_runtime.py delete mode 100644 poetry.lock create mode 100644 schemas/agent-capabilities-v1.json create mode 100644 schemas/agent-decision-v1.json create mode 100644 schemas/agent-event-v1.json create mode 100644 schemas/aggregate-export-v1.json create mode 100644 schemas/decision-ledger-v2.json create mode 100644 schemas/outcome-v1.json create mode 100644 schemas/safe-telemetry-v1.json create mode 100644 schemas/token-usage-v2.json create mode 100644 src/marginal/ledger.py create mode 100644 src/marginal/modes.py create mode 100644 src/marginal/outcomes.py create mode 100644 src/marginal/privacy.py create mode 100644 src/marginal/profiles.py create mode 100644 src/marginal/protocol.py create mode 100644 src/marginal/registry.py create mode 100644 src/marginal/replay.py create mode 100644 src/marginal/runtime.py create mode 100644 src/marginal/schema.py create mode 100644 src/marginal/schemas/__init__.py create mode 100644 src/marginal/schemas/agent-capabilities-v1.json create mode 100644 src/marginal/schemas/agent-decision-v1.json create mode 100644 src/marginal/schemas/agent-event-v1.json create mode 100644 src/marginal/schemas/aggregate-export-v1.json create mode 100644 src/marginal/schemas/decision-ledger-v2.json create mode 100644 src/marginal/schemas/outcome-v1.json create mode 100644 src/marginal/schemas/safe-telemetry-v1.json create mode 100644 src/marginal/schemas/token-usage-v2.json create mode 100644 tests/test_adapters_regression.py create mode 100644 tests/test_budget_regression.py create mode 100644 tests/test_cli_v2.py create mode 100644 tests/test_decision_ledger.py create mode 100644 tests/test_estimator_v2.py create mode 100644 tests/test_failure_settlement.py create mode 100644 tests/test_models_v2.py create mode 100644 tests/test_packaged_schemas_v2.py create mode 100644 tests/test_policy_regression.py create mode 100644 tests/test_policy_v2.py create mode 100644 tests/test_privacy.py create mode 100644 tests/test_protocol.py create mode 100644 tests/test_public_api_v2.py delete mode 100644 tests/test_public_eval.py create mode 100644 tests/test_public_eval_v2.py create mode 100644 tests/test_replay.py create mode 100644 tests/test_repository_consistency_v2.py create mode 100644 tests/test_runtime.py create mode 100644 tests/test_schema_conformance_v2.py create mode 100644 tests/test_shadow_mode.py create mode 100644 tests/test_trace_regression.py create mode 100644 tests/test_treasury_regression.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e140e36..31e39eb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,20 +10,26 @@ body: id: version attributes: label: MARGINAL version - placeholder: "0.1.0" + placeholder: "0.2.0" validations: required: true - type: input id: python attributes: label: Python version and platform + placeholder: "Python 3.13 on Windows 11" validations: required: true + - type: input + id: mode + attributes: + label: Execution mode and policy/estimator identity + placeholder: "shadow; profile:quality-first@2.0.0; historical-mean@2.0.0" - type: textarea id: behavior attributes: label: What happened? - description: Include the decision reason and smallest reproducible example. + description: Include reason codes and the smallest reproducible example. validations: required: true - type: textarea @@ -35,5 +41,5 @@ body: - type: textarea id: trace attributes: - label: Redacted trace or logs + label: Redacted trace or Decision Ledger records description: Remove prompts, credentials, personal data, and proprietary content. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9b1faa6..98edcfb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,6 +12,17 @@ - [ ] `mypy src/marginal` - [ ] `pytest -q` - [ ] Documentation updated +- [ ] Public API, schemas, examples, roadmap, and changelog are consistent - [ ] Performance claims are reproducible and honestly labeled +- [ ] New persisted fields have an explicit privacy classification +- [ ] Strict telemetry contains no free text or unreviewed metadata +- [ ] Privacy fixtures use synthetic identifiers and no real user data ## Security and compatibility + +## Privacy review + +- Privacy profile(s) affected: +- New or changed persisted fields: +- Quasi-identifiers considered: +- Key-management or export implications: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd4ef92..4a505b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,41 +10,22 @@ permissions: jobs: test: - name: test (${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - name: Check out source - uses: actions/checkout@v6 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: pip - - - name: Install development dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e ".[dev]" - - - name: Check formatting - run: ruff format --check src tests examples - - - name: Lint - run: ruff check src tests examples - - - name: Type check - run: mypy src/marginal - - - name: Test - run: pytest -q - - - name: Build distributions - run: python -m build - - - name: Validate distributions - run: python -m twine check dist/* + - run: python -m pip install --upgrade pip + - run: python -m pip install -e ".[dev]" + - run: ruff format --check . + - run: ruff check . + - run: mypy src/marginal + - run: pytest -q + - run: python -m build + - run: python -m twine check dist/* diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100644 index 7374cfe..0000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Killer Demo Pages - -on: - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages - cancel-in-progress: true - -jobs: - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Check out source - uses: actions/checkout@v6 - - - name: Configure GitHub Pages - uses: actions/configure-pages@v5 - - - name: Upload Killer Demo - uses: actions/upload-pages-artifact@v4 - with: - path: demos/killer-demo - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3a6a3d..6938370 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,9 @@ name: Release on: - workflow_dispatch: push: - tags: - - "v*" + branches: [main] + workflow_dispatch: permissions: contents: write @@ -17,38 +16,19 @@ jobs: release: runs-on: ubuntu-latest steps: - - name: Check out source - uses: actions/checkout@v6 - - - name: Set up Python - uses: actions/setup-python@v6 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: "3.13" cache: pip - - - name: Install development dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e ".[dev]" - - - name: Check formatting - run: ruff format --check src tests examples - - - name: Lint - run: ruff check src tests examples - - - name: Type check - run: mypy src/marginal - - - name: Test - run: pytest -q - - - name: Build distributions - run: python -m build - - - name: Validate distributions - run: python -m twine check dist/* - + - run: python -m pip install --upgrade pip + - run: python -m pip install -e ".[dev]" + - run: ruff format --check src tests examples + - run: ruff check src tests examples + - run: mypy src/marginal + - run: pytest -q + - run: python -m build + - run: python -m twine check dist/* - name: Create release when the version is new env: GH_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 6a45168..ad04826 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ build/ .env *.jsonl .DS_Store -!demos/killer-demo/trace.jsonl + +# MARGINAL local privacy keys +*.privacy.key +.marginal/ diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index dd40f5d..31abd4d 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -1,15 +1,16 @@ # Acknowledgments -MARGINAL was created independently by SignalLayer Labs as an independent Apache-2.0 open-source reference -implementation for agent compute capital allocation. +MARGINAL was created by SignalLayer Labs as an independent Apache-2.0 open-source +implementation for economically disciplined AI-agent compute allocation. The project’s economic framing is inspired by Siqi Zhu’s 2026 position paper, “Agentic AI Systems Should Be Designed as Marginal Token Allocators”: . The paper and this software are separate works. The paper proposes a research agenda; -MARGINAL provides an independently developed runtime API, accounting model, tests, -documentation, and integration layer. +MARGINAL provides an independently developed runtime API, transactional accounting model, +Decision Ledger, tests, documentation, and universal adapter foundation. We also acknowledge the broader open-source and research community working on agent -budgets, cost observability, model routing, test-time scaling, and reliable orchestration. +budgets, cost observability, model routing, test-time scaling, causal evaluation, and +reliable orchestration. diff --git a/CHANGELOG.md b/CHANGELOG.md index a17a34f..3fcc882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1,75 @@ # Changelog -All notable changes to MARGINAL are documented here. The project follows Semantic -Versioning. +All notable changes to MARGINAL are documented here. The project follows Semantic Versioning. + +## [0.2.0] - 2026-08-06 + +### Added + +- `shadow`, `recommend`, and `enforce` execution modes; +- applied-versus-recommended decision fields and stable reason codes; +- additive `TokenUsage` breakdown for uncached input, cached input, output, reasoning, and total tokens; +- common token-breakdown extraction for provider-like usage objects; +- versioned `EstimatorIdentity`, `ValueEstimate`, contextual historical observations, uncertainty, confidence, sample size, provenance, and deterministic training-data fingerprints; +- `EstimatorRegistry` for explicit estimator name/version resolution; +- versioned `PolicyIdentity` and four transparent reference policy profiles; +- non-blocking unchecked reservations and separate internal reservation identities for accurate concurrent Shadow Mode observation; +- measured overrun accounting in non-blocking modes; +- explicit failed-action settlement through sync and async wrappers, while keeping failed work retryable; +- provider-neutral `Outcome` contract separated from action-level realized gain; +- schema-versioned `JsonlDecisionLedger` with strict envelope validation, run/task/trajectory/engine/model correlation, task/outcome consistency checks, and monotonic sequencing; +- `PrivacyProfile` with `local_full`, keyed `safe_telemetry`, and separate `aggregate_export` modes; +- privacy-preserving aggregate export with a configurable minimum group size of five by default; +- field classification for safe-by-default, pseudonymous, and potentially sensitive evidence; +- HMAC-SHA-256 identifier pseudonymization, UTC-day timestamp generalization, strict free-text removal, and local 256-bit key management; +- opaque random local identifier generation for runs, tasks, and other caller-defined namespaces; +- grouped aggregate exports with deterministic cost/gain buckets, no identifiers, and no timestamps; +- `ledger-export` CLI, overwrite protection, aggregate JSON Schema, privacy guide, and executable privacy example; +- Universal Agent Protocol v1 values, strict round-trip parsing, capability negotiation, directives, state-aware deduplication scopes, and `UniversalRuntime`; +- off-policy decision replay with explicit non-causal reporting; +- ledger validation, ledger reporting, and replay CLI commands; +- JSON schemas for events, decisions, capabilities, outcomes, token usage, and ledger records, plus an installed-resource API through `available_schemas()` and `load_schema()`; +- configurable public-benchmark confidence level, non-inferiority margin, random seed, and cost-per-resolved efficiency metrics; +- executable Shadow Mode and universal-runtime examples; +- complete Learning Loop Foundation, universal-runtime, API, architecture, benchmarking, security, and roadmap documentation. + +### Changed + +- `Decision` is backward compatible but now carries recommendation, mode, reason-code, uncertainty, confidence, and estimator metadata; +- `MarginalPolicy` now has a stable identity and supports both versioned and legacy custom estimators; +- `Treasury.summary()` includes mode, policy, estimator, observed overruns, failed settlements, outcomes, and estimator observations; +- child treasuries inherit the parent execution mode; +- trace events include execution mode plus policy and estimator identity; +- Decision Ledger records now declare their privacy profile; `local_full` remains backward compatible while strict profiles are opt-in; +- actual failed-call spend can be accounted without replacing the original execution exception; extraction failures conservatively settle the reserved estimate and remain chained as secondary errors; +- strict public-benchmark parsing rejects string-like booleans instead of silently coercing them; +- project description and documentation now consistently describe MARGINAL as a learning-loop foundation rather than only a static wrapper. + +### Compatibility + +- existing v0.1 constructors, enforced execution, `JsonlTraceSink`, synchronous and asynchronous wrappers, demos, and CLI commands remain supported; +- new dataclass fields have backward-compatible defaults; +- the runtime core continues to have zero mandatory dependencies. + +### Scientific limitations + +- historical estimates are observational and do not establish causal action value; +- policy replay does not simulate unobserved trajectories or prove quality preservation; +- vendor-specific Codex, Claude Code, GitHub Copilot, and OpenCode adapters remain future milestones; +- reference profiles are transparent defaults, not universal calibrations. ## [0.1.0] - 2026-08-04 ### Added - provider-neutral `Action`, `Cost`, `Decision`, and `Allocation` value objects; -- deterministic `fund_best` candidate ranking and reservation; +- deterministic candidate ranking, authorization, reservation, settlement, and abort; - hard budgets for tokens, direct USD, latency, and risk; -- pending reservations that prevent concurrent and hierarchical oversubscription; +- pending reservations and hierarchical parent/child accounting; - protected verification reserves; - marginal-value policy with token, latency, and risk shadow prices; -- expected-gain capping against the remaining success target; - exact action and callable-input fingerprinting; -- duplicate prevention for pending and completed work; -- hierarchical treasuries with atomic child and parent accounting; -- explicit abort lifecycle and automatic release on callable failure; -- truthful settlement of actual usage with `BudgetOverrun` reporting; -- synchronous and asynchronous guarded-call and funded-allocation adapters; -- common OpenAI-, Anthropic-, and LiteLLM-like usage extraction; -- append-only JSONL traces and trace reporting CLI; -- transactional trace failure handling that preserves budget and primary-error semantics; -- deterministic synthetic benchmark and integration examples; -- end-to-end Killer Demo with a real generated code defect, verified baseline, action - rankings, HTML/Markdown/SVG/JSON artifacts, one-command CLI execution, and a GitHub - Pages deployment workflow; -- Python 3.10–3.13 CI, CodeQL, release automation, and community documentation. - -### Limitations - -- the bundled savings result is synthetic and is not a production performance claim; -- expected gains are caller-provided or based on transparent observed averages; -- causal value estimation and counterfactual replay are not included in this release. +- synchronous and asynchronous guarded-call adapters; +- append-only JSONL traces and CLI reporting; +- synthetic benchmark, public comparison utility, and Killer Demo; +- Python 3.10–3.13 CI, CodeQL, packaging, and community documentation. diff --git a/CITATION.cff b/CITATION.cff index 325455e..47c7489 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,11 +1,11 @@ cff-version: 1.2.0 message: "If you use MARGINAL in research or production evaluation, please cite this software." -title: "MARGINAL: Compute Capital Allocation for AI Agents" +title: "MARGINAL: Economically Disciplined Compute Allocation for AI Agents" type: software authors: - name: SignalLayer Labs -version: 0.1.0 -date-released: 2026-08-04 +version: 0.2.0 +date-released: 2026-08-06 license: Apache-2.0 repository-code: "https://github.com/SignalLayerLabs/Marginal" url: "https://github.com/SignalLayerLabs/Marginal" @@ -13,5 +13,7 @@ keywords: - artificial intelligence - AI agents - token optimization - - cost optimization + - compute governance + - decision ledger - agent infrastructure + - AI FinOps diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bbb19cd..f33a47f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,50 +1,91 @@ # Contributing to MARGINAL -Thank you for helping build open infrastructure for agent compute capital allocation. +Thank you for helping build open infrastructure for economically disciplined AI agents. ## Principles -Contributions should preserve four properties: +Contributions must preserve these properties: -1. decisions remain explainable; -2. denied actions never execute or consume budget; -3. benchmark claims remain reproducible and honestly labeled; -4. the core keeps zero mandatory runtime dependencies. +1. decisions remain explainable and versioned; +2. Enforce Mode never executes denied actions; +3. Shadow and Recommend modes never silently change caller behavior; +4. actual spend is recorded truthfully, including failed calls; +5. task outcomes are not misrepresented as causal action value; +6. benchmark claims are reproducible and honestly labeled; +7. prompts, outputs, credentials, and proprietary code are not logged by default; +8. the core keeps zero mandatory runtime dependencies. ## Development setup ```bash git clone https://github.com/SignalLayerLabs/Marginal.git -cd marginal +cd Marginal python -m venv .venv source .venv/bin/activate python -m pip install -e ".[dev]" ``` -On Windows, activate with `.venv\\Scripts\\activate`. +On Windows, activate with `.venv\Scripts\activate`. ## Before opening a pull request ```bash -ruff format --check . -ruff check . +ruff format --check src tests examples +ruff check src tests examples mypy src/marginal pytest -q python -m build python -m twine check dist/* ``` -New behavior requires a failing test before implementation. Add or update documentation for -public APIs and include a reproducible benchmark when making performance claims. +Run the executable examples when changing their public APIs: -## Pull requests +```bash +python examples/shadow_mode.py +python examples/universal_runtime.py +``` + +New behavior requires a failing test before implementation. Public API changes require documentation, compatibility notes, and schema updates when applicable. Changes to protocol or ledger serialization must add strict round-trip tests and validate the matching JSON schemas. Privacy changes must test representative quasi-identifiers, unknown fields, key handling, and the absence of free text from strict exports. + +## Adapter contributions + +An adapter must: + +- use the Universal Agent Protocol rather than duplicate policy logic; +- publish its capabilities honestly; +- preserve action IDs and lifecycle correlation; +- distinguish supported directives from protocol extension points; +- settle actual usage or explicitly report that usage is unavailable; +- document fail-open and fail-closed behavior; +- include protocol conformance and end-to-end tests; +- avoid logging prompts or source code by default. + +An adapter must not advertise Full Compute Enforcement unless the underlying engine exposes official controls for the relevant model turns, tool calls, retry loops, and stopping behavior. -Keep pull requests focused. Explain: +## Estimator contributions -- the problem; -- the chosen design; -- user-visible behavior; -- validation performed; -- compatibility or security implications. +An estimator must expose a stable name, semantic version, configuration hash, training-data fingerprint when applicable, and provenance. It must report uncertainty or explicitly state that uncertainty is unavailable. Claims of causal marginal value require an identification strategy, not only historical correlation. + +## Ledger and replay contributions + +Ledger changes must preserve required envelope fields, monotonic process-local sequencing, strict parsing, and task/outcome correlation. Document any concurrency guarantees explicitly. Replay changes must remain labeled as off-policy diagnostics unless they implement and validate a defensible causal method. + +## Privacy contributions + +Privacy-sensitive changes must preserve the separation between the operational ledger and shareable exports. Contributors must: + +- classify every newly persisted field as safe by default, pseudonymous, or potentially sensitive; +- default unknown fields to potentially sensitive; +- keep `safe_telemetry` allowlist-based rather than blocklist-based; +- avoid adding free-form strings, model identity, metadata, tool arguments, or exception text to strict telemetry; +- use field-separated keyed pseudonyms for correlation identifiers; +- keep pseudonymization keys outside traces, examples, fixtures, and version control; +- update both root and packaged JSON Schemas when an export contract changes; +- document whether a change affects local ledgers, safe event-level telemetry, aggregate exports, or all three; +- state explicitly when a technique is pseudonymization rather than anonymization. + +Tests and examples must use synthetic identifiers. Never commit real customer, repository, model, incident, or employee names as telemetry fixtures. + +## Pull requests -By contributing, you agree that your contribution is licensed under Apache-2.0. +Keep pull requests focused. Explain the problem, interface, behavior, validation, compatibility, privacy, and scientific limitations. By contributing, you agree that your contribution is licensed under Apache-2.0. diff --git a/LICENSE b/LICENSE index 9190a0d..9da1dd9 100644 --- a/LICENSE +++ b/LICENSE @@ -177,7 +177,7 @@ APPENDIX: How to apply the Apache License to your work. - Copyright 2026 SignalLayerLabs + Copyright 2026 BlumFinancialLab Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MANIFEST.in b/MANIFEST.in index 01bf3e6..8c4083a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,4 +5,9 @@ recursive-include examples *.py recursive-include docs *.md include .github/CODEOWNERS recursive-include .github *.yml *.md -recursive-include demos *.md *.html *.svg *.json *.jsonl + +include ROADMAP.md +recursive-include schemas *.json +recursive-include demos *.md *.json *.html *.svg *.jsonl + +recursive-include assets *.png diff --git a/README.md b/README.md index e543c54..3ac1078 100644 --- a/README.md +++ b/README.md @@ -15,102 +15,121 @@ --- -MARGINAL is an open-source decision and accounting layer for AI agents. It evaluates -proposed model calls, tool calls, searches, retries, reviewers, and sub-agents before they -run, then funds only actions whose expected marginal value justifies their direct cost, -token scarcity, latency, and risk. +MARGINAL is an open-source decision, accounting, and evidence layer for AI agents. It evaluates proposed model calls, tool calls, searches, retries, reviewers, and sub-agents before they run, then accounts for what actually happened. > **Hard budgets ask “can we afford this?” MARGINAL also asks “is this worth funding?”** -MARGINAL does not compress prompts or replace an agent framework. It removes entire -low-value actions before they consume tokens. +Version `0.2.0` adds the **Learning Loop Foundation**: Shadow Mode, a versioned Decision Ledger, explicit privacy profiles, measured outcome contracts, versioned value estimators, policy replay, and a universal engine-neutral runtime for future Codex, Claude Code, GitHub Copilot, OpenCode, and other adapters. + +MARGINAL does not compress prompts or replace an agent framework. It can eliminate entire low-value actions before they consume tokens, or observe them without interference while evidence is collected. ## Why this exists -Agent runtimes commonly execute the next step because it appears in a workflow, because a -model requested it, or because a hard limit has not yet been reached. Those mechanisms do -not compare the expected improvement of the next action with its total economic cost. +Agent runtimes commonly execute the next step because it appears in a workflow, a model requested it, or a hard limit has not yet been reached. Those mechanisms answer whether work is permitted; they do not compare the expected improvement of the next action with its total economic cost. -MARGINAL adds that missing allocation layer: +MARGINAL adds that allocation layer while keeping execution and evidence explicit: - rank candidate actions by marginal value; -- reserve budget at authorization time; -- protect a verification reserve; -- prevent duplicate and concurrent double-spend; -- account for actual usage, including overruns; -- release reservations when execution fails; -- enforce parent and child budgets atomically; -- produce provider-neutral JSONL evidence; -- work with synchronous and asynchronous Python callables; -- keep zero mandatory runtime dependencies. - -## Killer demo: same verified fix, 94.09% fewer declared tokens - -The bundled end-to-end demo creates a real buggy Python micro-repository, executes the -same diagnose → fix → verify workflow twice, and checks the final result with a deterministic -verifier. The baseline runs every search, reviewer, rewrite, and audit. MARGINAL funds only -the highest-value action in each stage. - -[![Killer demo: baseline versus MARGINAL](demos/killer-demo/comparison.svg)](demos/killer-demo/RESULTS.md) - -| Metric | Run everything | MARGINAL | Savings | -|---|---:|---:|---:| -| Declared tokens | 72,800 | 4,300 | **94.09%** | -| Calls | 9 | 3 | **66.67%** | -| Estimated USD | $0.763 | $0.026 | **96.59%** | -| Estimated latency | 22,030 ms | 1,230 ms | **94.42%** | -| Verified outcome | PASS | PASS | preserved | +- reserve budget before execution and settle actual usage afterward; +- protect verification capacity; +- prevent concurrent double-spend and state-insensitive duplicates; +- observe recommendations without blocking through Shadow Mode; +- record versioned policy, estimator, cost, failure, and outcome evidence; +- protect quasi-identifiers and free text through explicit privacy profiles; +- support provider-neutral synchronous, asynchronous, and engine-adapter integrations; +- keep the runtime core free of mandatory dependencies. -```bash -marginal killer-demo --output killer-demo-output +## What changed in v0.2 + +### Shadow Mode + +Shadow Mode evaluates every proposed action but never blocks it: + +```text +agent proposes action + ↓ +MARGINAL recommends allow or deny + ↓ +action still executes + ↓ +actual cost and verified outcome are recorded ``` -The command produces a standalone HTML report, GitHub-ready Markdown, SVG comparison, -structured JSON, and the complete provider-neutral decision trace. This is a deterministic -functional demonstration using declared action-cost estimates, **not a production benchmark, -provider measurement, or universal savings claim**. +This is the safe default for collecting evidence before enforcement. + +### Decision Ledger + +`JsonlDecisionLedger` records schema-versioned, append-only evidence with: + +- run, task, trajectory, engine, and model identity; +- policy and estimator versions; +- recommended versus applied decisions; +- estimated and actual costs; +- failures, overruns, observations, and outcomes; +- deterministic sequence numbers for replay and audit. + +Prompts and model outputs are not recorded by default. That alone is not sufficient for safe +sharing: task IDs, action names, model identity, repository metadata, verifier details, and +error text can still reveal sensitive information. + +### Privacy profiles -[Open the full allocation report →](demos/killer-demo/RESULTS.md) +Every Decision Ledger field is treated as safe-by-default, pseudonymous, or potentially +sensitive. MARGINAL provides three explicit profiles: + +- `LOCAL_FULL` preserves the complete operational ledger on a trusted local filesystem; +- `SAFE_TELEMETRY` removes free text and metadata, pseudonymizes identifiers with a local + HMAC-SHA-256 key, and generalizes exact timestamps; +- `generate_local_identifier(...)` creates opaque random local IDs when correlation with + external names is unnecessary; +- `AGGREGATE_EXPORT` creates grouped generalized rows with no identifiers or timestamps and + suppresses groups smaller than five records by default. + +`aggregate_export` is a separate export path, not an operational ledger mode. Its default +minimum group size is five and can be raised for more conservative sharing. Pseudonymization +is not anonymization; inspect data before sharing it. See [Privacy profiles](docs/privacy.md). + +### Versioned Value Estimator + +`ValueEstimator` now returns `ValueEstimate` objects with expected gain, uncertainty, confidence, sample size, provenance, and a stable estimator identity. Explicit caller estimates remain supported, and historical observations can be contextualized by engine, phase, task type, language, and model. + +MARGINAL does **not** infer that every action caused a successful task. Action-level realized gain must be supplied explicitly through `Treasury.observe_value(...)`. + +### Universal Agent Protocol + +`AgentAction`, `AgentEvent`, `AgentDecision`, `AgentCapabilities`, and `UniversalRuntime` provide the shared contract for thin engine adapters. Engine-specific code translates native events; the economic policy remains in one core. + +The versioned JSON contracts ship inside the installed package as well as in the repository: + +```python +from marginal import available_schemas, load_schema + +print(available_schemas()) +event_schema = load_schema("agent-event-v1.json") +``` ## Install Install the tagged GitHub release: ```bash -pip install "marginal-ai @ git+https://github.com/SignalLayerLabs/Marginal.git@v0.1.0" +pip install "marginal-ai @ git+https://github.com/SignalLayerLabs/Marginal.git@v0.2.0" ``` For development: ```bash git clone https://github.com/SignalLayerLabs/Marginal.git -cd marginal +cd Marginal python -m pip install -e ".[dev]" ``` -## Five-minute integration +## Five-minute enforced integration ```python -from marginal import ( - Action, - ActionDenied, - BudgetLimits, - Cost, - MarginalPolicy, - PolicyConfig, - Treasury, - budgeted_call, - funded_call, -) - -policy = MarginalPolicy( - PolicyConfig( - outcome_value_usd=5.0, - token_shadow_price_per_million_usd=10.0, - minimum_roi=1.0, - ) -) +from marginal import Action, BudgetLimits, Cost, Treasury, budgeted_call, build_policy +policy = build_policy("balanced") treasury = Treasury( BudgetLimits( max_tokens=100_000, @@ -118,26 +137,23 @@ treasury = Treasury( verification_reserve_tokens=10_000, ), policy=policy, + mode="enforce", ) -try: - answer = budgeted_call( - treasury, - your_expensive_function, - "input", - action=Action( - name="research missing evidence", - kind="research", - cost=Cost(tokens=4_000, usd=0.04, latency_ms=1_500), - expected_gain=0.12, - ), - ) -except ActionDenied as exc: - print(f"Skipped: {exc}") +response = budgeted_call( + treasury, + your_expensive_function, + "input", + action=Action( + name="research missing evidence", + kind="research", + cost=Cost(tokens=4_000, usd=0.04, latency_ms=1_500), + expected_gain=0.12, + ), +) ``` -The wrapped function is never called when authorization is denied. Approved estimates are -reserved immediately, preventing parallel actions from oversubscribing the same treasury. +The wrapped function is never called when enforcement denies the action. Approved estimates are reserved immediately, preventing parallel actions from oversubscribing the same treasury. ## Fund the best next action @@ -164,161 +180,254 @@ if allocation is not None: result = funded_call(treasury, allocation, execute, allocation.action) ``` -`fund_best` evaluates every candidate against the same current state and reserves the -highest-scoring affordable action. `funded_call` executes that reservation and automatically -settles or releases it. Candidate rankings are recorded in the trace. - -## Use actual provider usage +`fund_best` evaluates candidates against one locked state, records the full ranking, and reserves only the highest-value affordable candidate. It remains an active selection API in every execution mode. -MARGINAL has no mandatory SDK dependency. Wrap the callable already used by your app and -extract actual token usage from its result: +## Use measured provider usage ```python -from marginal import budgeted_call, extract_common_llm_usage - response = budgeted_call( treasury, client.responses.create, - action=Action( - name="draft final answer", - kind="llm", - cost=Cost(tokens=5_000, usd=0.08), - expected_gain=0.15, - ), + action=action, usage_extractor=extract_common_llm_usage, model="YOUR_MODEL", input="Draft the answer.", ) ``` -The built-in extractor understands common OpenAI-, Anthropic-, and LiteLLM-like usage -objects. It replaces the token estimate and preserves direct USD, latency, and risk values -that provider responses do not expose consistently. - -A custom extractor receives `(result, estimated_cost)` and returns a complete `Cost`. +The total-token extractor preserves direct USD, latency, and risk values that a provider response does not expose consistently. `extract_common_token_usage` provides the additive input, cached-input, non-reasoning output, reasoning, and total breakdown. -## Async integration +Async callables use the same lifecycle: ```python -from marginal import async_budgeted_call - response = await async_budgeted_call( treasury, - async_client_call, + client.responses.create, action=action, usage_extractor=extract_common_llm_usage, + **request, ) ``` -Synchronous and asynchronous wrappers share the same reservation, settlement, duplicate, -and trace semantics. +## Start safely with Shadow Mode -## Decision model +```python +from marginal import ( + Action, + BudgetLimits, + Cost, + DecisionLedgerContext, + JsonlDecisionLedger, + Treasury, + budgeted_call, + build_policy, +) -The default policy converts all configured dimensions into a common USD-denominated value: +ledger = JsonlDecisionLedger( + "marginal-ledger.jsonl", + context=DecisionLedgerContext( + run_id="run-001", + task_id="task-042", + trajectory_id="baseline-a", + engine="codex", + model="your-model", + ), + privacy_profile="safe_telemetry", + privacy_key_path=".marginal/privacy.key", +) -```text -expected value = capped expected success gain × outcome value -cost value = direct USD + token shadow cost + latency shadow cost + risk shadow cost -marginal score = expected value − cost value -ROI = expected value ÷ cost value +treasury = Treasury( + BudgetLimits(max_tokens=50_000, verification_reserve_tokens=5_000), + policy=build_policy("quality-first"), + trace_sink=ledger, + mode="shadow", +) + +result = budgeted_call( + treasury, + your_expensive_function, + action=Action( + name="ask another reviewer", + kind="review", + cost=Cost(tokens=5_000), + expected_gain=0.01, + ), +) ``` -An action is approved only when: +Even when the policy recommendation is deny, Shadow Mode executes the callable, records the non-blocking override, and accounts for actual usage. -1. child and parent hard budgets remain valid; -2. pending reservations leave enough budget; -3. the verification reserve remains protected; -4. the action is not a pending or completed duplicate; -5. the target success probability has not been reached; -6. expected gain and ROI clear configured thresholds. +## Record verified outcomes and action-level learning -`Cost.usd` is direct estimated or measured spend. Shadow prices are optional opportunity -costs used by the policy; they do not silently alter the hard USD ledger. +```python +from marginal import Action, Outcome + +# Task outcome: evidence about the trajectory as a whole. +treasury.record_outcome( + Outcome( + task_id="task-042", + reward=1.0, + resolved=True, + verifier="pytest", + evidence={"suite": "tests/test_payment.py"}, + ) +) -## Reliable settlement +# Action-level realized gain: supplied only when the application can justify it. +treasury.observe_value( + Action(name="run targeted test", kind="verification"), + realized_gain=0.18, +) +``` -Authorization and settlement are separate: +Task outcomes and action-level realized gain are deliberately separate. A passing task alone does not establish the causal value of every action in its trajectory. -```text -propose → evaluate → reserve → execute → settle actual cost - ↘ abort and release on failure +## Universal runtime for engine adapters + +```python +from marginal import AgentAction, AgentCapabilities, Cost, UniversalRuntime + +runtime = UniversalRuntime( + treasury, + engine="opencode", + session_id="session-1", + task_id="task-42", + capabilities=AgentCapabilities( + observe_model_usage=True, + block_actions=True, + record_outcomes=True, + ), +) + +decision = runtime.before_action( + AgentAction( + action_id="read-1", + name="read complete repository", + kind="file_read", + estimated_cost=Cost(tokens=8_000), + expected_gain=0.03, + state_hash="workspace-sha", + phase="diagnose", + deduplication_scope="once_per_state", + ) +) + +# The adapter applies the decision, executes when allowed, then settles actual usage. +runtime.after_action("read-1", actual_cost=Cost(tokens=6_400)) ``` -If actual usage exceeds the reserved estimate, MARGINAL records the real spend first and -then raises `BudgetOverrun`. Accounting therefore remains truthful even when a provider or -tool costs more than predicted. +`UniversalRuntime` in `enforce` mode requires an adapter that declares `block_actions=True`; MARGINAL refuses to advertise enforcement through an observe-only integration. -Authorization tracing is transactional: if the trace sink fails, the reservation and counters -are rolled back. During execution failure, the original callable exception remains primary even -if abort tracing also fails. A settlement trace failure cannot undo external spend, so committed -usage remains recorded and the trace error is surfaced. +Protocol v1 defines `allow`, `deny`, `modify`, `defer`, `reuse`, `stop`, and `force_verify` directives so adapters can negotiate future control surfaces consistently. The v0.2 reference runtime currently derives `allow` and `deny` from the core decision; richer directives remain adapter and policy extension points and are not claimed as automatic behavior. -## Duplicate protection +## Measured token breakdown -For `budgeted_call` and `async_budgeted_call`, the fingerprint includes: +```python +from marginal import extract_common_token_usage + +usage = extract_common_token_usage(response) +print(usage.input_tokens) +print(usage.cached_input_tokens) +print(usage.output_tokens) +print(usage.reasoning_tokens) +print(usage.total_tokens) +``` -- the declared action; -- the callable identity; -- positional arguments; -- keyword arguments. +The normalized fields are additive: `input_tokens` means uncached input, while cached input is reported separately. -Only the SHA-256 digest is stored in normal traces. Inputs must be composed of supported, -deterministically serializable values, or the caller can provide an explicit -`Action.fingerprint`. +## Failed calls that still consumed resources -## Synthetic benchmark +Some provider calls fail after compute was consumed. Supply a failure usage extractor to keep accounting truthful while preserving the original exception: -The repository ships a deterministic functional benchmark. It is intentionally synthetic -and is **not a production performance claim**. +```python +result = budgeted_call( + treasury, + client.responses.create, + action=action, + failure_usage_extractor=lambda error, estimate: measured_or_best_known_cost(error), + **request, +) +``` -| Metric | Baseline | MARGINAL | Savings | -|---|---:|---:|---:| -| Tokens | 97,500 | 42,500 | **56.41%** | -| Calls | 25 | 15 | **40.00%** | -| Simulated USD | $1.1500 | $0.4500 | **60.87%** | -| Simulated latency | 17,750 ms | 7,750 ms | **56.34%** | -| Verified success | 100% | 100% | preserved | +Returning `None` means no external spend was observed and releases the reservation. Returning `Cost` settles the failed action. -Run it locally: +## Policy replay ```bash -marginal demo +marginal ledger-validate marginal-ledger.jsonl +marginal ledger-report marginal-ledger.jsonl +marginal replay marginal-ledger.jsonl --profile balanced +marginal ledger-export marginal-ledger.jsonl safe-export.jsonl \ + --privacy-profile safe_telemetry --privacy-key-file .marginal/export.key +marginal ledger-export marginal-ledger.jsonl aggregate.jsonl \ + --privacy-profile aggregate_export --minimum-group-size 5 ``` -See [benchmarks.md](benchmarks.md) and [docs/benchmarking.md](docs/benchmarking.md) for the -methodology and limitations. +Replay re-evaluates recorded proposed actions under another policy. It is an off-policy diagnostic based on recorded actions and costs. It is **not causal proof**, does not simulate missing trajectories, and does not establish preserved task quality. -## How MARGINAL differs +## Execution modes -| Category | Primary question | MARGINAL relationship | +| Mode | Applied behavior | Intended use | |---|---|---| -| Hard budget / circuit breaker | Can this session spend more? | Complementary; MARGINAL also prices expected value | -| Model router | Which model should answer? | A router can be one candidate action | -| Prompt compressor | Can the same call use fewer tokens? | Complementary; MARGINAL may eliminate the call | -| Workflow optimizer | Can a fixed workflow be simplified? | MARGINAL makes online decisions at runtime | -| Observability | What was spent? | MARGINAL decides before spending and records settlement | +| `shadow` | Execute every proposed action; record recommendation | Safe data collection and calibration | +| `recommend` | Execute every proposed action; surface recommendation | Human/agent advisory integrations | +| `enforce` | Apply allow/deny and hard-budget decisions | Validated production control | -## Core primitives +`fund_best` remains an active selection API in every mode because MARGINAL is explicitly being asked to choose among candidates. -| Primitive | Responsibility | -|---|---| -| `Action` | Declares proposed work, estimated cost, expected gain, and metadata | -| `Cost` | Normalizes tokens, direct USD, latency, and risk | -| `BudgetLimits` | Defines hard limits and protected verification reserves | -| `MarginalPolicy` | Produces deterministic, explainable decisions | -| `Treasury` | Ranks, reserves, settles, aborts, traces, and creates child budgets | -| `fund_best` | Selects and reserves the highest-value candidate | -| `budgeted_call` | Authorizes, executes, and settles a synchronous callable | -| `funded_call` | Executes and settles the action reserved by `fund_best` | -| `async_budgeted_call` | Guards an asynchronous callable | -| `async_funded_call` | Executes an asynchronous funded allocation | -| `JsonlTraceSink` | Produces append-only provider-neutral evidence | +## Reference policy profiles + +```python +from marginal import build_policy + +quality_first = build_policy("quality-first") +balanced = build_policy("balanced") +token_saver = build_policy("token-saver") +strict_budget = build_policy("strict-budget") +``` + +These are transparent reference defaults, not universally calibrated guarantees. Production policies should be validated against representative tasks and verifiers. + +## Decision model + +The reference policy converts configured dimensions into a common USD-denominated value: + +```text +expected value = capped expected success gain × outcome value +cost value = direct USD + token shadow cost + latency shadow cost + risk shadow cost +marginal score = expected value − cost value +ROI = expected value ÷ cost value +``` + +A recommended action must remain affordable, preserve verification reserves, avoid an exact duplicate under the chosen fingerprint scope, remain below the success target, and clear expected-gain and ROI thresholds. + +## Reliable accounting + +```text +propose → evaluate → reserve → execute → settle actual cost + ↘ abort when no spend occurred + ↘ settle failure when spend occurred +``` + +If actual usage exceeds the reservation, MARGINAL records the real spend first. Enforce mode then raises `BudgetOverrun`. Shadow and recommend modes record the observed overrun without changing caller behavior. + +If a failure usage extractor itself fails, MARGINAL conservatively settles the reserved estimate, releases the reservation, and keeps the original execution exception primary. A measured failed action is not marked as a completed duplicate, so a legitimate retry remains possible. + +## Duplicate protection and state-aware retries + +Guarded call fingerprints include the action, callable identity, arguments, and keyword arguments. Universal-agent actions additionally support: + +- `exact`; +- `once_per_state`; +- `once_per_phase`; +- `allow_retry`. + +This lets an adapter distinguish an accidental repeated read from a legitimate test rerun after the workspace changes. Shadow Mode can observe concurrent semantic duplicates without blocking them while still reserving and settling each execution separately. ## Hierarchical agent budgets ```python -root = Treasury(BudgetLimits(max_tokens=200_000, max_usd=5.0)) +root = Treasury(BudgetLimits(max_tokens=200_000, max_usd=5.0), mode="shadow") research = root.child("research", BudgetLimits(max_tokens=40_000, max_usd=1.0)) verification = root.child( "verification", @@ -326,91 +435,126 @@ verification = root.child( ) ``` -A child authorization reserves capacity from the child and every parent treasury under one -shared lock. A child settlement charges every level, preventing fan-out oversubscription. +A child authorization reserves capacity from every ancestor under one shared lock. Settlement charges every level, preventing parallel sub-agents from oversubscribing a parent budget. ## Trace and inspect decisions -```python -from marginal import JsonlTraceSink - -trace = JsonlTraceSink("marginal-trace.jsonl") -treasury = Treasury(BudgetLimits(max_tokens=50_000), trace_sink=trace) -``` +`JsonlTraceSink` preserves the v0.1 trace format. `JsonlDecisionLedger` is the strict v0.2 evidence format with schema, identity, sequencing, and correlation fields. ```bash marginal validate marginal-trace.jsonl marginal report marginal-trace.jsonl -marginal report marginal-trace.jsonl --json +marginal ledger-validate marginal-ledger.jsonl +marginal ledger-report marginal-ledger.jsonl +marginal ledger-export marginal-ledger.jsonl aggregate.jsonl \ + --privacy-profile aggregate_export --minimum-group-size 5 ``` -Trace events include candidate rankings, authorization decisions, reservations, commits, -aborts, actual usage, and overrun reasons. Prompts and model outputs are not recorded unless -an application explicitly places them in action metadata. +`LOCAL_FULL` is the backward-compatible ledger default. Use `SAFE_TELEMETRY` for strict +local telemetry and `AGGREGATE_EXPORT` for grouped sharing. Aggregate groups smaller than five +records are suppressed by default, and export destinations are never overwritten automatically. -## Architecture +A `CompositeTraceSink` can fan events to multiple sinks in order, but writes across different sinks are not an atomic distributed transaction. Use one authoritative ledger when atomic evidence is required. -```text -Agent / workflow / SDK call - │ - ▼ - Candidate actions - │ - ▼ -┌────────────────────────────────┐ -│ MARGINAL Treasury │ -│ ├─ hard child + parent budgets │ -│ ├─ pending reservations │ -│ ├─ verification reserve │ -│ ├─ duplicate detection │ -│ ├─ marginal-value ranking │ -│ └─ target-success stopping │ -└────────────────────────────────┘ - │ funded │ denied - ▼ └── no execution - Execute callable - │ success / failure - ▼ - Settle actual cost or release reservation - │ - └──► append-only JSONL trace +## Killer Demo + +The bundled deterministic demo still demonstrates the allocation mechanism: + +```bash +marginal killer-demo --output killer-demo-output ``` -See [docs/architecture.md](docs/architecture.md). +It uses declared action-cost estimates and a deterministic verifier. It is not provider telemetry, a production benchmark, or a universal savings claim. -## Research lineage +[Open the committed demo report →](demos/killer-demo/RESULTS.md) + +## Synthetic benchmark -MARGINAL is an independent open-source reference implementation inspired by the research -direction described in Siqi Zhu’s position paper, -[“Agentic AI Systems Should Be Designed as Marginal Token Allocators”](https://arxiv.org/abs/2605.01214). -The paper proposes the economic framing; this repository focuses on a small, immediately -usable runtime contract, accounting model, trace format, tests, and integrations. +The bundled deterministic benchmark exercises policy, reservation, accounting, and reproducibility: -Related systems such as [AgentBudget](https://agentbudget.dev/) focus on hard session cost -enforcement. MARGINAL is designed to complement them by deciding whether an affordable -next action has sufficient expected value. +```bash +marginal demo +``` -See [docs/research.md](docs/research.md) and [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md). +Its declared token, USD, and latency values are synthetic. The result tests mechanics and must not be presented as provider-measured savings. + +## Public benchmarking + +Real evaluations should compare the same model, task, tools, limits, and verifier with and without MARGINAL. The comparator accepts an explicit confidence level and preregistered quality margin: + +```bash +marginal public-eval baseline.jsonl marginal.jsonl \ + --confidence-level 0.95 --quality-margin-pp 1.0 +``` + +Report: + +- resolved rate and confidence intervals; +- input, cached input, output, reasoning, and total tokens where available; +- direct cost, latency, tool calls, and sub-agent calls; +- cost per verified successful task; +- regressions and recoveries; +- policy and estimator identities; +- raw paired evidence without dropped failures. + +Savings without preserved quality are not optimization. + +See [`docs/public-benchmarks.md`](docs/public-benchmarks.md) and [`docs/benchmarking.md`](docs/benchmarking.md). + +## How MARGINAL differs + +| Category | Primary question | MARGINAL relationship | +|---|---|---| +| Hard budget / circuit breaker | Can this session spend more? | Complementary; MARGINAL also evaluates expected value | +| Model router | Which model should answer? | A model choice can be represented as a candidate action | +| Prompt compressor | Can this call use fewer tokens? | Complementary; MARGINAL may avoid the entire action | +| Workflow optimizer | Can a fixed flow be simplified? | MARGINAL makes state-aware online decisions | +| Observability | What was spent? | MARGINAL decides before spending and settles afterward | +| Decision Ledger | Why did policy behavior change? | Records versioned recommendation, application, cost, and outcome evidence | + +## Core primitives + +| Primitive | Responsibility | +|---|---| +| `Action`, `Cost`, `TokenUsage` | Describe proposed work and estimated or measured resources | +| `Decision`, `Allocation` | Expose applied behavior, recommendation, reason, score, and funded candidate | +| `BudgetLimits`, `BudgetLedger` | Enforce hard limits, reservations, and verification reserves | +| `MarginalPolicy`, `ValueEstimator` | Score expected marginal value with versioned identities | +| `Treasury` | Coordinate ranking, authorization, settlement, hierarchy, evidence, and outcomes | +| `JsonlDecisionLedger` | Persist strict, append-only learning-loop evidence with an explicit privacy profile | +| `PrivacyProfile`, `export_decision_ledger` | Pseudonymize safe telemetry or create grouped aggregate exports | +| `AgentAction`, `AgentDecision`, `AgentEvent` | Normalize engine-adapter communication | +| `UniversalRuntime` | Correlate one engine session with transactional core operations | +| `replay_ledger` | Compare policy recommendations over recorded actions without causal claims | + +## Architecture + +```text +AI development agent + │ native hook/event + ▼ +thin engine adapter + │ universal protocol + ▼ +UniversalRuntime → Treasury → policy → versioned estimator + │ │ + │ ├─ reserve / settle / abort / failure settlement + │ └─ Decision Ledger → privacy profile → outcome / replay / export + ▼ +allow / deny today; richer negotiated directives through protocol extensions +``` + +See [`docs/architecture.md`](docs/architecture.md). ## Project status -MARGINAL `v0.1.0` is a reference implementation of **agent compute capital allocation**. -It provides deterministic online ranking, authorization, reservation, settlement, and -accounting. It does not claim causal value estimates, automatic counterfactual replay, or -guaranteed savings on arbitrary workloads. +MARGINAL `v0.2.0` is the **Learning Loop Foundation**. It provides a universal protocol, local runtime, non-blocking shadow evaluation, schema-versioned evidence, explicit privacy profiles, outcome recording, contextual historical estimates, failure settlement, and off-policy replay. -The next validation milestone is a public benchmark across real agent frameworks and task -sets, measuring cost per verified outcome rather than cost alone. +It does not yet claim complete vendor-specific adapters, causal marginal-value estimation, automatic regret minimization, or guaranteed savings on arbitrary workloads. The next validation milestone is a real paired Codex integration using measured telemetry and a predefined quality non-inferiority criterion. ## Roadmap -MARGINAL `v0.1.0` established the dependency-free reference allocator. Development is now -focused on **v0.2 — Universal Agent Foundation**: one shared protocol and local runtime for -Codex, Claude Code, GitHub Copilot, OpenCode, and future compatible development agents. - -The next measured milestone is a paired Codex evaluation comparing the same model, tasks, -tools, limits, and verifier with and without MARGINAL, using real token telemetry and -quality-preservation criteria. +The project remains one product and one repository. Future Codex, OpenCode, Claude Code, GitHub Copilot, and other integrations will be thin adapters over the same protocol and core. [View the full product roadmap →](ROADMAP.md) @@ -420,31 +564,30 @@ quality-preservation criteria. - [Concepts](docs/concepts.md) - [Architecture](docs/architecture.md) - [API reference](docs/api.md) +- [Learning loop](docs/learning-loop.md) +- [Universal runtime](docs/universal-runtime.md) +- [Privacy profiles](docs/privacy.md) - [Integrations](docs/integrations.md) -- [Killer demo](demos/killer-demo/RESULTS.md) - [Benchmarking](docs/benchmarking.md) +- [Public benchmark protocol](docs/public-benchmarks.md) - [Research and prior art](docs/research.md) - [FAQ](docs/faq.md) - [Governance](docs/governance.md) - [Contributing](CONTRIBUTING.md) - [Security](SECURITY.md) +## Research lineage + +MARGINAL is an independent open-source reference implementation inspired by the research direction described in Siqi Zhu’s position paper, “Agentic AI Systems Should Be Designed as Marginal Token Allocators.” The paper proposes an economic framing; this repository focuses on a usable runtime contract, accounting, evidence, tests, integrations, and honest validation. + ## Contributing -MARGINAL is deliberately small at the core and open at the edges. Contributions are -welcome for adapters, estimators, benchmark scenarios, documentation, and independent -validation. Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. +Contributions are welcome for adapters, estimator implementations, benchmark scenarios, schemas, documentation, and independent validation. Read [`CONTRIBUTING.md`](CONTRIBUTING.md) before opening a pull request. ## Citation -Academic and technical work can cite the repository using [CITATION.cff](CITATION.cff). +Research and technical work can cite the repository using [`CITATION.cff`](CITATION.cff). ## License -Apache License 2.0. See [LICENSE](LICENSE). - ---- - -## Public benchmarks - -See [the public benchmark protocol](docs/public-benchmarks.md). +Apache License 2.0. See [`LICENSE`](LICENSE). diff --git a/ROADMAP.md b/ROADMAP.md index cce39ae..8819925 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ MARGINAL's North Star is simple: > **Install MARGINAL once, keep using your AI development agent normally, and reduce avoidable token consumption without sacrificing verified quality.** -MARGINAL is being developed as one universal, local compute-governance product for the main AI development agents. A single core, protocol, installer, policy system, and reporting experience will support Codex, Claude Code, GitHub Copilot, OpenCode, and future compatible runtimes through thin engine adapters. +MARGINAL is being developed as one universal, local compute-governance product for the main AI development agents. A single core, protocol, installer, policy system, learning loop, and reporting experience will support Codex, Claude Code, GitHub Copilot, OpenCode, and future compatible runtimes through thin engine adapters. This roadmap is milestone-driven rather than date-driven. It communicates product direction and measurable outcomes. GitHub Issues and pull requests should track implementation-level work. @@ -14,10 +14,11 @@ This roadmap is milestone-driven rather than date-driven. It communicates produc 2. **Thin adapters:** engine-specific behavior stays outside the decision core. 3. **Quality first:** token reduction is valuable only when verified quality is preserved. 4. **Measured claims:** public savings claims must use real runtime or provider telemetry. -5. **Local first:** prompts and source code are not uploaded or logged by default. -6. **Simple installation:** supported agents should require no manual code changes. -7. **Transparent capabilities:** observe-only and enforcement integrations must be clearly distinguished. -8. **Small core:** the provider-neutral core keeps zero mandatory runtime dependencies. +5. **Learning without overclaiming:** observational associations are not described as causal value. +6. **Local first:** prompts and source code are not uploaded or logged by default. +7. **Simple installation:** supported agents should require no manual code changes. +8. **Transparent capabilities:** observe-only and enforcement integrations must be clearly distinguished. +9. **Small core:** the provider-neutral core keeps zero mandatory runtime dependencies. ## Status legend @@ -25,7 +26,7 @@ This roadmap is milestone-driven rather than date-driven. It communicates produc |---|---| | **Planned** | Scope is defined, but implementation has not started. | | **In progress** | Implementation is actively underway. | -| **Validation** | Implementation is complete, but benchmark or compatibility evidence is still pending. | +| **Validation** | Implementation exists, but final CI, release, integration, or benchmark evidence is pending. | | **Complete** | All exit criteria have been met and supporting evidence is available. | ## Milestones at a glance @@ -33,11 +34,11 @@ This roadmap is milestone-driven rather than date-driven. It communicates produc | Milestone | Status | Primary outcome | |---|---|---| | **v0.1 — Reference Allocator Foundation** | Complete | Provider-neutral allocation, accounting, tracing, demos, and first release | -| **v0.2 — Universal Agent Foundation** | In progress | Shared protocol and runtime for every supported development agent | +| **v0.2 — Learning Loop Foundation** | Validation | Universal protocol, non-blocking observation, versioned evidence, outcomes, replay, and estimators | | **v0.3 — Codex Reference Integration** | Planned | Real Codex integration and measured paired benchmark | | **v0.4 — Multi-Engine Developer Preview** | Planned | Codex, OpenCode, Claude Code, and GitHub Copilot compatibility | | **v0.5 — One-Command Universal Installation** | Planned | Automatic detection, installation, diagnostics, and rollback | -| **v0.6 — Adaptive Allocation** | Planned | Context-aware value estimation and dynamic compute pricing | +| **v0.6 — Adaptive and Causal Allocation** | Planned | Calibrated learning, context-carry economics, exploration, and regret measurement | | **v0.7 — Ecosystem and Operational Scale** | Planned | Additional engines, persistent runtimes, observability, and team controls | --- @@ -74,34 +75,54 @@ Evidence: [`CHANGELOG.md`](CHANGELOG.md), [`docs/architecture.md`](docs/architec --- -## v0.2 — Universal Agent Foundation - -**Status:** In progress - -**Objective:** Create the shared architecture that lets every supported development agent use the same MARGINAL decision engine, telemetry model, and safety guarantees. - -### Deliverables - -- [ ] Publish MARGINAL Universal Agent Protocol v1 -- [ ] Define versioned normalized event, decision, capability, and outcome schemas -- [ ] Add capability negotiation for observe, modify, deny, stop, and verification control -- [ ] Add token usage v2 with input, cached input, output, reasoning, and total token fields -- [ ] Add trace schema v2 with run, task, trajectory, action, policy, estimator, and model identity -- [ ] Add state-aware fingerprints and configurable deduplication scopes -- [ ] Add `shadow`, `recommend`, and `enforce` operating modes -- [ ] Add explicit settlement for failed, partial, unknown, and measured usage -- [ ] Add conservative built-in policy profiles: Quality First, Balanced, Token Saver, and Strict Budget -- [ ] Add a local session runtime and state store -- [ ] Define the shared adapter SDK and conformance test suite -- [ ] Preserve a dependency-free provider-neutral core +## v0.2 — Learning Loop Foundation + +**Status:** Validation + +**Objective:** Create the shared, versioned learning-loop foundation that lets every supported development agent use the same MARGINAL decisions, evidence model, accounting, and safety guarantees. + +### Delivered in the release candidate + +- [x] Publish MARGINAL Universal Agent Protocol v1 +- [x] Define normalized event, decision, capability, outcome, token-usage, and ledger schemas +- [x] Add capability negotiation for observe, modify, deny, stop, and verification control +- [x] Add additive token usage v2 for uncached input, cached input, output, reasoning, and total tokens +- [x] Add Decision Ledger v2 with run, task, trajectory, action, policy, estimator, engine, and model identity +- [x] Classify evidence fields as safe-by-default, pseudonymous, or potentially sensitive +- [x] Add `LOCAL_FULL` and keyed `SAFE_TELEMETRY` operational ledger profiles +- [x] Add separate grouped `AGGREGATE_EXPORT` output with no identifiers or timestamps +- [x] Suppress aggregate groups smaller than five records by default with a configurable threshold +- [x] Publish recursively strict JSON Schemas for safe event-level telemetry and aggregate exports +- [x] Add local 256-bit key generation, restrictive permission checks, race-safe exports, and safe export CLI +- [x] Add strict ledger parsing, monotonic sequence validation, and task/outcome correlation checks +- [x] Add state-aware fingerprints and configurable deduplication scopes +- [x] Add `shadow`, `recommend`, and `enforce` operating modes +- [x] Preserve concurrent Shadow Mode observations with separate reservation identities +- [x] Add explicit failed-action settlement for measured, estimated, and unavailable usage +- [x] Keep failed actions retryable while accounting for consumed resources +- [x] Add conservative fallback settlement when failure usage extraction itself fails +- [x] Add Quality First, Balanced, Token Saver, and Strict Budget reference profiles +- [x] Add a provider-neutral local `UniversalRuntime` +- [x] Add explicit task outcomes and separate action-level realized-gain observations +- [x] Add versioned estimator identities, uncertainty, confidence, sample size, provenance, and registry +- [x] Add deterministic training-data fingerprints for online observations +- [x] Add non-causal policy replay and CLI ledger validation/reporting +- [x] Add protocol and schema conformance tests, executable examples, and aligned documentation +- [x] Preserve the dependency-free provider-neutral runtime core ### Exit criteria -- The protocol and schemas are versioned and documented. -- A reference simulated adapter passes all conformance tests. -- Existing v0.1 behavior remains backward compatible or has an explicit migration path. -- Shadow mode can observe a complete agent session without changing its behavior. -- The core still has zero mandatory runtime dependencies. +- [x] Protocols and schemas are versioned and documented. +- [x] Privacy profiles, field classification, key handling, small-group suppression, export boundaries, and limitations are documented. +- [x] The provider-neutral reference runtime passes focused protocol and lifecycle tests. +- [x] Shadow Mode can observe complete action lifecycles without blocking caller behavior. +- [x] Existing v0.1 constructors and enforced execution paths remain covered by regression tests. +- [x] The package metadata and public documentation describe the implemented v0.2 behavior consistently. +- [x] The runtime core still has zero mandatory dependencies. +- [ ] Ruff, mypy strict, the full repository test suite, package build, and Twine validation pass in the canonical GitHub checkout and CI. +- [ ] `v0.2.0` is tagged and released from the canonical repository. + +The release remains in **Validation** until the final two exit criteria are satisfied. Vendor-specific adapters and measured production savings are intentionally not part of v0.2. --- @@ -146,7 +167,7 @@ Evidence: [`CHANGELOG.md`](CHANGELOG.md), [`docs/architecture.md`](docs/architec - [ ] Build an OpenCode adapter - [ ] Build a Claude Code adapter - [ ] Build a GitHub Copilot CLI or coding-agent adapter where official control surfaces permit it -- [ ] Reuse the same protocol, policy profiles, telemetry, and reports across all adapters +- [ ] Reuse the same protocol, policy profiles, telemetry, ledger, and reports across all adapters - [ ] Publish an engine capability matrix - [ ] Add adapter-specific compatibility and end-to-end tests - [ ] Clearly label each integration as Observe, Tool Enforcement, or Full Compute Enforcement @@ -158,7 +179,7 @@ Evidence: [`CHANGELOG.md`](CHANGELOG.md), [`docs/architecture.md`](docs/architec - At least four development-agent environments, including Codex, pass protocol conformance tests. - No adapter contains duplicated economic decision logic. - Every supported engine has documented capabilities and limitations. -- The same policy profile produces comparable decision traces across engines. +- The same policy profile produces comparable decision records across engines. - At least two engines support real action enforcement. --- @@ -175,7 +196,7 @@ Evidence: [`CHANGELOG.md`](CHANGELOG.md), [`docs/architecture.md`](docs/architec - [ ] Automatically detect supported agents and their versions - [ ] Install only the required adapters - [ ] Create safe backups before changing agent configuration -- [ ] Enable Quality First and shadow mode by default +- [ ] Enable Quality First and Shadow Mode by default - [ ] Add `marginal status` - [ ] Add `marginal doctor` - [ ] Add `marginal profile` @@ -195,31 +216,33 @@ Evidence: [`CHANGELOG.md`](CHANGELOG.md), [`docs/architecture.md`](docs/architec --- -## v0.6 — Adaptive Allocation +## v0.6 — Adaptive and Causal Allocation **Status:** Planned -**Objective:** Replace static expected-gain assumptions with contextual, uncertainty-aware estimates learned from real agent trajectories. +**Objective:** Learn calibrated action value from real trajectories while distinguishing prediction, association, and causal evidence. ### Deliverables -- [ ] Add contextual value estimation by task, phase, engine, model, evidence, and repository state -- [ ] Attach uncertainty, confidence, sample size, and provenance to value estimates -- [ ] Add a calibrated task belief state updated by tests, errors, reviews, and verifier evidence +- [ ] Train and validate contextual estimators on real engine trajectories +- [ ] Add a calibrated task belief state updated by deterministic evidence - [ ] Estimate context-carry cost across future model turns - [ ] Add dynamic token shadow pricing based on scarcity and projected remaining work -- [ ] Add controlled, budgeted exploration for counterfactual learning +- [ ] Add controlled, budgeted exploration with propensity logging - [ ] Prevent exploration for unsafe or irreversible actions -- [ ] Add estimator calibration and drift reports -- [ ] Compare adaptive policies against fixed reference policies +- [ ] Add off-policy evaluation appropriate to logged propensities +- [ ] Add estimator calibration, drift, and regret reports +- [ ] Compare adaptive policies against fixed reference policies on held-out runs +- [ ] Define explicit evidence standards before making causal marginal-value claims - [ ] Preserve deterministic policy modes for reproducibility and regulated use cases ### Exit criteria -- Predicted value and observed outcomes have published calibration evidence. +- Predicted action value and observed outcomes have published calibration evidence. - Adaptive allocation improves token cost per verified task over the fixed reference policy on held-out runs. - Quality remains within the predefined non-inferiority margin. - Exploration behavior is bounded, reproducible, and separately accounted. +- Any causal claim includes a documented identification strategy and assumptions. --- @@ -276,6 +299,7 @@ MARGINAL will be evaluated on the combined outcome, not token savings alone: - regressions and recoveries; - tool and sub-agent calls; - latency and direct cost; +- estimator calibration and decision regret; - variance across repeated runs; - installation success and rollback reliability; - capability coverage by engine. diff --git a/SECURITY.md b/SECURITY.md index 9505d64..da9fa94 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,27 +2,78 @@ ## Supported versions -| Version | Supported | -|---|---| -| 0.1.x | Yes | +Security fixes are provided for the latest released minor version. Users should upgrade to the newest `0.x` release because the public API may still evolve before `1.0.0`. ## Reporting a vulnerability -Do not open a public issue for vulnerabilities that could expose credentials, prompts, -private traces, budget bypasses, arbitrary execution, or supply-chain compromise. +Report vulnerabilities privately through GitHub Security Advisories for `SignalLayerLabs/Marginal`. Do not open a public issue for credential exposure, arbitrary code execution, path traversal, ledger tampering, unsafe installer behavior, or sensitive-data disclosure. -Use GitHub private vulnerability reporting for this repository. Include: +Include the affected version, platform, minimal reproduction, impact, and suggested mitigation. Do not include real credentials, private prompts, proprietary source code, or personal data. -- affected version and platform; -- minimal reproduction; -- impact and attack preconditions; -- suggested mitigation, when known. +## Data handling and privacy profiles -The maintainers will acknowledge valid reports, assess severity, coordinate a fix, and -publish credit unless the reporter asks to remain anonymous. +MARGINAL is local-first, has no mandatory cloud account, and the core does not transmit data. +Callers control trace paths, ledger paths, identifiers, metadata, and retention. Local storage is +still a security boundary: access controls, backups, synchronization tools, and developer +workstations can expose a ledger. -## Security model +The primary privacy risk is not limited to prompts. Quasi-identifiers and free text can reveal a +customer, repository, task, model, or incident even when prompt and output fields are absent. +Prefer opaque values from `generate_local_identifier(...)` when external identity is not needed. +Examples include task IDs, action names, model identity, repository metadata, verifier names, +tool arguments, exception text, and exact timestamps. -MARGINAL controls whether application-provided callables are invoked. It is not a sandbox, -credential vault, content filter, or authorization system. Applications remain responsible -for tool permissions, network controls, secret handling, and safe execution environments. +MARGINAL provides three profiles: + +- `LOCAL_FULL` preserves the complete operational record and is the backward-compatible default; +- `SAFE_TELEMETRY` removes free text and metadata, pseudonymizes identifiers using a local + HMAC-SHA-256 key, generalizes exact timestamps, and retains only allowlisted structured fields; +- `AGGREGATE_EXPORT` groups generalized decision and outcome rows, contains no identifiers or + timestamps, and suppresses groups smaller than five records by default. It is an export format, + not an operational ledger mode. + +Pseudonymization is not anonymization. Stable pseudonyms can be linked within one key domain, +rare patterns can still identify a workload. Aggregate export applies a configurable minimum +group size of five by default, but this threshold is not a proof of anonymity or formal +k-anonymity. The profiles do not provide differential privacy, encryption at rest, compliance +certification, or protection against a party that has both the source values and local key. + +Generated privacy keys contain 256 random bits, are created with owner-only permissions on POSIX +systems, and are never written into ledger records. Symbolic-link key paths and overly permissive +existing key files are rejected. Keep keys out of source control and do not distribute an +operational key with an exported dataset. The default ignore rules exclude `*.privacy.key` and +`.marginal/`. + +Applications can still write sensitive data to `JsonlTraceSink`, `LOCAL_FULL` ledgers, custom +files, logs outside MARGINAL, or downstream systems. `SAFE_TELEMETRY` is a strict allowlist at the +Decision Ledger boundary; it does not sanitize arbitrary external logs. Review +[`docs/privacy.md`](docs/privacy.md) before sharing evidence. + +Protocol metadata used for automatic fingerprints must be deterministically JSON serializable. +This rejects ambiguous custom-object representations, but it does not make the source metadata +safe. Fingerprints are identifiers, not secrets; the strict profile re-pseudonymizes them with a +keyed construction because low-entropy hashes can be guessed. + +## Adapter trust boundary + +Engine adapters can observe or control agent actions. Install adapters only from trusted sources, review configuration changes, and use least privilege. An adapter must disclose whether it can block actions, modify actions, stop the agent, observe model usage, or access source code. + +Capability negotiation prevents an adapter from claiming unsupported controls accidentally, but it is not a sandbox or an authorization system. The host application remains responsible for validating and applying adapter decisions safely. + +## Ledger integrity and concurrency + +`JsonlDecisionLedger` is append-only at the application level and uses a process-local lock. It is not a cryptographic audit log and does not provide multi-process or distributed atomicity. Filesystem users can modify it, and two independent processes must not append to the same ledger without an external coordinator. + +`CompositeTraceSink` invokes sinks sequentially. It does not provide a distributed transaction across sinks. A later sink can fail after an earlier sink has accepted an event. + +Environments requiring tamper evidence, cross-process coordination, or compliance-grade retention should add immutable storage, external locking, signing, or hash chaining before treating the ledger as authoritative evidence. + +## Failure behavior + +Shadow and Recommend modes are intentionally non-blocking. Enforce Mode can deny actions. Integrations must document what happens if the runtime, ledger, or trace sink is unavailable. + +Failed provider or tool calls may still consume resources. A failure usage extractor should return measured or best-known usage when available. If extraction itself fails, the built-in wrappers conservatively settle the reserved estimate, preserve the original execution exception as primary, and chain the extraction failure for diagnosis. Failed actions are accounted but are not marked as successfully completed duplicates, so a legitimate retry remains possible. + +## Replay and scientific claims + +Policy replay evaluates recorded actions under another policy. It does not execute the omitted counterfactual trajectory, establish causality, or prove quality preservation. Security, compliance, or safety decisions must not treat replay output as causal evidence. diff --git a/apply_roadmap_update.py b/apply_roadmap_update.py deleted file mode 100644 index 8f98cab..0000000 --- a/apply_roadmap_update.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -"""Apply the approved MARGINAL roadmap update to a repository checkout.""" - -from __future__ import annotations - -from pathlib import Path -import shutil -import sys - - -README_MARKER = """The next validation milestone is a public benchmark across real agent frameworks and task -sets, measuring cost per verified outcome rather than cost alone. - -## Documentation -""" - -README_REPLACEMENT = """The next validation milestone is a public benchmark across real agent frameworks and task -sets, measuring cost per verified outcome rather than cost alone. - -## Roadmap - -MARGINAL `v0.1.0` established the dependency-free reference allocator. Development is now -focused on **v0.2 — Universal Agent Foundation**: one shared protocol and local runtime for -Codex, Claude Code, GitHub Copilot, OpenCode, and future compatible development agents. - -The next measured milestone is a paired Codex evaluation comparing the same model, tasks, -tools, limits, and verifier with and without MARGINAL, using real token telemetry and -quality-preservation criteria. - -[View the full product roadmap →](ROADMAP.md) - -## Documentation -""" - - -def main() -> int: - repo = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() - readme = repo / "README.md" - source_roadmap = Path(__file__).with_name("ROADMAP.md") - target_roadmap = repo / "ROADMAP.md" - - if not readme.is_file(): - raise SystemExit(f"README.md not found in {repo}") - if not source_roadmap.is_file(): - raise SystemExit(f"ROADMAP.md not found beside {Path(__file__).name}") - - current = readme.read_text(encoding="utf-8") - if "[View the full product roadmap →](ROADMAP.md)" in current: - raise SystemExit("README.md already contains the roadmap section") - if README_MARKER not in current: - raise SystemExit("README insertion marker not found; review README changes manually") - - updated = current.replace(README_MARKER, README_REPLACEMENT, 1) - readme.write_text(updated, encoding="utf-8") - if source_roadmap.resolve() != target_roadmap.resolve(): - shutil.copyfile(source_roadmap, target_roadmap) - roadmap_message = f"Created {target_roadmap}" - else: - roadmap_message = f"Using existing {target_roadmap}" - - print(f"Updated {readme}") - print(roadmap_message) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/assets/marginal-project-mark.png b/assets/marginal-project-mark.png deleted file mode 100644 index 9ca9e108356f1fad58bd0f90c7bc5485b2bec1ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 144758 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 diff --git a/assets/marginal-social-preview.png b/assets/marginal-social-preview.png deleted file mode 100644 index b5a2aeea90846a23858e09b6a961e5d4684442f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 347894 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 z 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

xc0ni5OCX71%Lm?Y$-5HTS>VP^&&jU!(? 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

Same fix. Different capital discipline.

Baseline

9 actions -
  1. 1inspect the failing assertionDiagnose · 1,200 tokens$0.006Justified✓
  2. 2scan the entire repositoryDiagnose · 9,000 tokens$0.045Executed×
  3. 3ask two parallel reviewersDiagnose · 14,000 tokens$0.120Executed×
  4. 4apply the targeted one-line patchFix · 2,400 tokens$0.018Justified✓
  5. 5rewrite the complete pricing moduleFix · 12,000 tokens$0.110Executed×
  6. 6ask a frontier model for an alternative patchFix · 18,000 tokens$0.280Executed×
  7. 7run the targeted verifierVerify · 700 tokens$0.002Justified✓
  8. 8run the full test suiteVerify · 4,500 tokens$0.012Executed×
  9. 9request a premium model auditVerify · 11,000 tokens$0.170Executed×
+
  1. 1inspect the failing assertionDiagnose · 1,200 tokens$0.006Justified✓
  2. 2scan the entire repositoryDiagnose · 9,000 tokens$0.045Executedx
  3. 3ask two parallel reviewersDiagnose · 14,000 tokens$0.120Executedx
  4. 4apply the targeted one-line patchFix · 2,400 tokens$0.018Justified✓
  5. 5rewrite the complete pricing moduleFix · 12,000 tokens$0.110Executedx
  6. 6ask a frontier model for an alternative patchFix · 18,000 tokens$0.280Executedx
  7. 7run the targeted verifierVerify · 700 tokens$0.002Justified✓
  8. 8run the full test suiteVerify · 4,500 tokens$0.012Executedx
  9. 9request a premium model auditVerify · 11,000 tokens$0.170Executedx
Total estimated cost $0.763 @@ -1576,7 +1576,7 @@

What this demo proves

The deterministic defect

Fix a percentage-discount bug in a deterministic Python repository

- − return total - rate + - return total - rate + return total * (1 - rate)
diff --git a/demos/killer-demo/trace.jsonl b/demos/killer-demo/trace.jsonl index 0933593..febbf20 100644 --- a/demos/killer-demo/trace.jsonl +++ b/demos/killer-demo/trace.jsonl @@ -1,9 +1,9 @@ -{"candidates": [{"action": {"cost": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "current_success_probability": 0.0, "expected_gain": 0.22, "fingerprint": "9249f7c0ea26013b8bf6b1aea2ae903c1a389d4891d3d1b3e27587cc90153028", "is_verification": false, "kind": "research", "metadata": {"stage": "diagnose"}, "name": "inspect the failing assertion"}, "decision": {"allowed": true, "estimated_cost_value": 0.030699999999999998, "expected_gain": 0.22, "reason": "approved: marginal ROI 7.166", "score": 0.1893}}, {"action": {"cost": {"latency_ms": 2200, "risk": 0.0, "tokens": 9000, "usd": 0.045}, "current_success_probability": 0.0, "expected_gain": 0.05, "fingerprint": "5c1e3d25d6c0ea662b38fc76c3adb01e2916aab6808cbff750b38ae368d9d712", "is_verification": false, "kind": "research", "metadata": {"stage": "diagnose"}, "name": "scan the entire repository"}, "decision": {"allowed": false, "estimated_cost_value": 0.22939999999999997, "expected_gain": 0.05, "reason": "rejected: marginal ROI 0.218 below 1.000", "score": -0.17939999999999995}}, {"action": {"cost": {"latency_ms": 4500, "risk": 0.0, "tokens": 14000, "usd": 0.12}, "current_success_probability": 0.0, "expected_gain": 0.04, "fingerprint": "cca32271d74f3dab1bc141d5918b12eb406090a194bdf9b436fe76c036836eb2", "is_verification": false, "kind": "review", "metadata": {"stage": "diagnose"}, "name": "ask two parallel reviewers"}, "decision": {"allowed": false, "estimated_cost_value": 0.40900000000000003, "expected_gain": 0.04, "reason": "rejected: marginal ROI 0.098 below 1.000", "score": -0.36900000000000005}}], "event": "candidate_ranking", "treasury": "killer-demo"} -{"action": {"cost": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "current_success_probability": 0.0, "expected_gain": 0.22, "fingerprint": "9249f7c0ea26013b8bf6b1aea2ae903c1a389d4891d3d1b3e27587cc90153028", "is_verification": false, "kind": "research", "metadata": {"stage": "diagnose"}, "name": "inspect the failing assertion"}, "decision": {"allowed": true, "estimated_cost_value": 0.030699999999999998, "expected_gain": 0.22, "reason": "approved: marginal ROI 7.166", "score": 0.1893}, "event": "authorization", "reserved": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "treasury": "killer-demo", "usage": {"latency_ms": 0, "risk": 0.0, "tokens": 0, "usd": 0.0}} -{"action": {"cost": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "current_success_probability": 0.0, "expected_gain": 0.22, "fingerprint": "9249f7c0ea26013b8bf6b1aea2ae903c1a389d4891d3d1b3e27587cc90153028", "is_verification": false, "kind": "research", "metadata": {"stage": "diagnose"}, "name": "inspect the failing assertion"}, "budget_overrun": false, "event": "commit", "treasury": "killer-demo", "usage": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "violations": []} -{"candidates": [{"action": {"cost": {"latency_ms": 700, "risk": 0.0, "tokens": 2400, "usd": 0.018}, "current_success_probability": 0.0, "expected_gain": 0.5, "fingerprint": "6709d8e40d23cdc7b42020f54ed11dfa3e9141f5117e1f8d85642c12708e0a8a", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "apply the targeted one-line patch"}, "decision": {"allowed": true, "estimated_cost_value": 0.06739999999999999, "expected_gain": 0.5, "reason": "approved: marginal ROI 7.418", "score": 0.4326}}, {"action": {"cost": {"latency_ms": 3200, "risk": 0.0, "tokens": 12000, "usd": 0.11}, "current_success_probability": 0.0, "expected_gain": 0.2, "fingerprint": "d96d4754a8ac012a625b0ccc6e2944f64dcac291577089a2ab33e44c59156adc", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "rewrite the complete pricing module"}, "decision": {"allowed": false, "estimated_cost_value": 0.3564, "expected_gain": 0.2, "reason": "rejected: marginal ROI 0.561 below 1.000", "score": -0.15639999999999998}}, {"action": {"cost": {"latency_ms": 5500, "risk": 0.0, "tokens": 18000, "usd": 0.28}, "current_success_probability": 0.0, "expected_gain": 0.15, "fingerprint": "1125e93f1af77ac4124f84f538886e9f97b17dd935ad5266f09d064da312d841", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "ask a frontier model for an alternative patch"}, "decision": {"allowed": false, "estimated_cost_value": 0.651, "expected_gain": 0.15, "reason": "rejected: marginal ROI 0.230 below 1.000", "score": -0.501}}], "event": "candidate_ranking", "treasury": "killer-demo"} -{"action": {"cost": {"latency_ms": 700, "risk": 0.0, "tokens": 2400, "usd": 0.018}, "current_success_probability": 0.0, "expected_gain": 0.5, "fingerprint": "6709d8e40d23cdc7b42020f54ed11dfa3e9141f5117e1f8d85642c12708e0a8a", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "apply the targeted one-line patch"}, "decision": {"allowed": true, "estimated_cost_value": 0.06739999999999999, "expected_gain": 0.5, "reason": "approved: marginal ROI 7.418", "score": 0.4326}, "event": "authorization", "reserved": {"latency_ms": 700, "risk": 0.0, "tokens": 2400, "usd": 0.018}, "treasury": "killer-demo", "usage": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}} -{"action": {"cost": {"latency_ms": 700, "risk": 0.0, "tokens": 2400, "usd": 0.018}, "current_success_probability": 0.0, "expected_gain": 0.5, "fingerprint": "6709d8e40d23cdc7b42020f54ed11dfa3e9141f5117e1f8d85642c12708e0a8a", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "apply the targeted one-line patch"}, "budget_overrun": false, "event": "commit", "treasury": "killer-demo", "usage": {"latency_ms": 1050, "risk": 0.0, "tokens": 3600, "usd": 0.024}, "violations": []} -{"candidates": [{"action": {"cost": {"latency_ms": 180, "risk": 0.0, "tokens": 700, "usd": 0.002}, "current_success_probability": 0.0, "expected_gain": 0.35, "fingerprint": "313d64e7457c58d692b5f2ec902a279c32ca88807380c35dc464afaed301d70f", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "run the targeted verifier"}, "decision": {"allowed": true, "estimated_cost_value": 0.01636, "expected_gain": 0.35, "reason": "approved: marginal ROI 21.394", "score": 0.33364}}, {"action": {"cost": {"latency_ms": 1400, "risk": 0.0, "tokens": 4500, "usd": 0.012}, "current_success_probability": 0.0, "expected_gain": 0.08, "fingerprint": "f83033a110b1105e98b597c52e17027bceab726f06b6f3438ff26d61deca992d", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "run the full test suite"}, "decision": {"allowed": false, "estimated_cost_value": 0.10479999999999999, "expected_gain": 0.08, "reason": "rejected: marginal ROI 0.763 below 1.000", "score": -0.02479999999999999}}, {"action": {"cost": {"latency_ms": 4000, "risk": 0.0, "tokens": 11000, "usd": 0.17}, "current_success_probability": 0.0, "expected_gain": 0.05, "fingerprint": "c444a2361858cb3bcc7b1a4e2596a0ef1248b3aa5978f63a07b40ad62cf10db2", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "request a premium model audit"}, "decision": {"allowed": false, "estimated_cost_value": 0.398, "expected_gain": 0.05, "reason": "rejected: marginal ROI 0.126 below 1.000", "score": -0.34800000000000003}}], "event": "candidate_ranking", "treasury": "killer-demo"} -{"action": {"cost": {"latency_ms": 180, "risk": 0.0, "tokens": 700, "usd": 0.002}, "current_success_probability": 0.0, "expected_gain": 0.35, "fingerprint": "313d64e7457c58d692b5f2ec902a279c32ca88807380c35dc464afaed301d70f", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "run the targeted verifier"}, "decision": {"allowed": true, "estimated_cost_value": 0.01636, "expected_gain": 0.35, "reason": "approved: marginal ROI 21.394", "score": 0.33364}, "event": "authorization", "reserved": {"latency_ms": 180, "risk": 0.0, "tokens": 700, "usd": 0.002}, "treasury": "killer-demo", "usage": {"latency_ms": 1050, "risk": 0.0, "tokens": 3600, "usd": 0.024}} -{"action": {"cost": {"latency_ms": 180, "risk": 0.0, "tokens": 700, "usd": 0.002}, "current_success_probability": 0.0, "expected_gain": 0.35, "fingerprint": "313d64e7457c58d692b5f2ec902a279c32ca88807380c35dc464afaed301d70f", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "run the targeted verifier"}, "budget_overrun": false, "event": "commit", "treasury": "killer-demo", "usage": {"latency_ms": 1230, "risk": 0.0, "tokens": 4300, "usd": 0.026000000000000002}, "violations": []} +{"candidates": [{"action": {"cost": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "current_success_probability": 0.0, "expected_gain": 0.22, "fingerprint": "9249f7c0ea26013b8bf6b1aea2ae903c1a389d4891d3d1b3e27587cc90153028", "is_verification": false, "kind": "research", "metadata": {"stage": "diagnose"}, "name": "inspect the failing assertion"}, "decision": {"allowed": true, "confidence": 1.0, "estimated_cost_value": 0.030699999999999998, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.22, "mode": "enforce", "reason": "approved: marginal ROI 7.166", "reason_code": "APPROVED", "recommendation_reason": "approved: marginal ROI 7.166", "recommendation_reason_code": "APPROVED", "recommended": true, "score": 0.1893, "uncertainty": 0.0}}, {"action": {"cost": {"latency_ms": 2200, "risk": 0.0, "tokens": 9000, "usd": 0.045}, "current_success_probability": 0.0, "expected_gain": 0.05, "fingerprint": "5c1e3d25d6c0ea662b38fc76c3adb01e2916aab6808cbff750b38ae368d9d712", "is_verification": false, "kind": "research", "metadata": {"stage": "diagnose"}, "name": "scan the entire repository"}, "decision": {"allowed": false, "confidence": 1.0, "estimated_cost_value": 0.22939999999999997, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.05, "mode": "enforce", "reason": "rejected: marginal ROI 0.218 below 1.000", "reason_code": "MARGINAL_ROI_REJECTED", "recommendation_reason": "rejected: marginal ROI 0.218 below 1.000", "recommendation_reason_code": "MARGINAL_ROI_REJECTED", "recommended": false, "score": -0.17939999999999995, "uncertainty": 0.0}}, {"action": {"cost": {"latency_ms": 4500, "risk": 0.0, "tokens": 14000, "usd": 0.12}, "current_success_probability": 0.0, "expected_gain": 0.04, "fingerprint": "cca32271d74f3dab1bc141d5918b12eb406090a194bdf9b436fe76c036836eb2", "is_verification": false, "kind": "review", "metadata": {"stage": "diagnose"}, "name": "ask two parallel reviewers"}, "decision": {"allowed": false, "confidence": 1.0, "estimated_cost_value": 0.40900000000000003, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.04, "mode": "enforce", "reason": "rejected: marginal ROI 0.098 below 1.000", "reason_code": "MARGINAL_ROI_REJECTED", "recommendation_reason": "rejected: marginal ROI 0.098 below 1.000", "recommendation_reason_code": "MARGINAL_ROI_REJECTED", "recommended": false, "score": -0.36900000000000005, "uncertainty": 0.0}}], "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "candidate_ranking", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "treasury": "killer-demo"} +{"action": {"cost": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "current_success_probability": 0.0, "expected_gain": 0.22, "fingerprint": "9249f7c0ea26013b8bf6b1aea2ae903c1a389d4891d3d1b3e27587cc90153028", "is_verification": false, "kind": "research", "metadata": {"stage": "diagnose"}, "name": "inspect the failing assertion"}, "decision": {"allowed": true, "confidence": 1.0, "estimated_cost_value": 0.030699999999999998, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.22, "mode": "enforce", "reason": "approved: marginal ROI 7.166", "reason_code": "APPROVED", "recommendation_reason": "approved: marginal ROI 7.166", "recommendation_reason_code": "APPROVED", "recommended": true, "score": 0.1893, "uncertainty": 0.0}, "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "authorization", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "reserved": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "treasury": "killer-demo", "usage": {"latency_ms": 0, "risk": 0.0, "tokens": 0, "usd": 0.0}} +{"action": {"cost": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "current_success_probability": 0.0, "expected_gain": 0.22, "fingerprint": "9249f7c0ea26013b8bf6b1aea2ae903c1a389d4891d3d1b3e27587cc90153028", "is_verification": false, "kind": "research", "metadata": {"stage": "diagnose"}, "name": "inspect the failing assertion"}, "budget_overrun": false, "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "commit", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "treasury": "killer-demo", "usage": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}, "violations": []} +{"candidates": [{"action": {"cost": {"latency_ms": 700, "risk": 0.0, "tokens": 2400, "usd": 0.018}, "current_success_probability": 0.0, "expected_gain": 0.5, "fingerprint": "6709d8e40d23cdc7b42020f54ed11dfa3e9141f5117e1f8d85642c12708e0a8a", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "apply the targeted one-line patch"}, "decision": {"allowed": true, "confidence": 1.0, "estimated_cost_value": 0.06739999999999999, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.5, "mode": "enforce", "reason": "approved: marginal ROI 7.418", "reason_code": "APPROVED", "recommendation_reason": "approved: marginal ROI 7.418", "recommendation_reason_code": "APPROVED", "recommended": true, "score": 0.4326, "uncertainty": 0.0}}, {"action": {"cost": {"latency_ms": 3200, "risk": 0.0, "tokens": 12000, "usd": 0.11}, "current_success_probability": 0.0, "expected_gain": 0.2, "fingerprint": "d96d4754a8ac012a625b0ccc6e2944f64dcac291577089a2ab33e44c59156adc", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "rewrite the complete pricing module"}, "decision": {"allowed": false, "confidence": 1.0, "estimated_cost_value": 0.3564, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.2, "mode": "enforce", "reason": "rejected: marginal ROI 0.561 below 1.000", "reason_code": "MARGINAL_ROI_REJECTED", "recommendation_reason": "rejected: marginal ROI 0.561 below 1.000", "recommendation_reason_code": "MARGINAL_ROI_REJECTED", "recommended": false, "score": -0.15639999999999998, "uncertainty": 0.0}}, {"action": {"cost": {"latency_ms": 5500, "risk": 0.0, "tokens": 18000, "usd": 0.28}, "current_success_probability": 0.0, "expected_gain": 0.15, "fingerprint": "1125e93f1af77ac4124f84f538886e9f97b17dd935ad5266f09d064da312d841", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "ask a frontier model for an alternative patch"}, "decision": {"allowed": false, "confidence": 1.0, "estimated_cost_value": 0.651, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.15, "mode": "enforce", "reason": "rejected: marginal ROI 0.230 below 1.000", "reason_code": "MARGINAL_ROI_REJECTED", "recommendation_reason": "rejected: marginal ROI 0.230 below 1.000", "recommendation_reason_code": "MARGINAL_ROI_REJECTED", "recommended": false, "score": -0.501, "uncertainty": 0.0}}], "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "candidate_ranking", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "treasury": "killer-demo"} +{"action": {"cost": {"latency_ms": 700, "risk": 0.0, "tokens": 2400, "usd": 0.018}, "current_success_probability": 0.0, "expected_gain": 0.5, "fingerprint": "6709d8e40d23cdc7b42020f54ed11dfa3e9141f5117e1f8d85642c12708e0a8a", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "apply the targeted one-line patch"}, "decision": {"allowed": true, "confidence": 1.0, "estimated_cost_value": 0.06739999999999999, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.5, "mode": "enforce", "reason": "approved: marginal ROI 7.418", "reason_code": "APPROVED", "recommendation_reason": "approved: marginal ROI 7.418", "recommendation_reason_code": "APPROVED", "recommended": true, "score": 0.4326, "uncertainty": 0.0}, "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "authorization", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "reserved": {"latency_ms": 700, "risk": 0.0, "tokens": 2400, "usd": 0.018}, "treasury": "killer-demo", "usage": {"latency_ms": 350, "risk": 0.0, "tokens": 1200, "usd": 0.006}} +{"action": {"cost": {"latency_ms": 700, "risk": 0.0, "tokens": 2400, "usd": 0.018}, "current_success_probability": 0.0, "expected_gain": 0.5, "fingerprint": "6709d8e40d23cdc7b42020f54ed11dfa3e9141f5117e1f8d85642c12708e0a8a", "is_verification": false, "kind": "generation", "metadata": {"stage": "fix"}, "name": "apply the targeted one-line patch"}, "budget_overrun": false, "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "commit", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "treasury": "killer-demo", "usage": {"latency_ms": 1050, "risk": 0.0, "tokens": 3600, "usd": 0.024}, "violations": []} +{"candidates": [{"action": {"cost": {"latency_ms": 180, "risk": 0.0, "tokens": 700, "usd": 0.002}, "current_success_probability": 0.0, "expected_gain": 0.35, "fingerprint": "313d64e7457c58d692b5f2ec902a279c32ca88807380c35dc464afaed301d70f", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "run the targeted verifier"}, "decision": {"allowed": true, "confidence": 1.0, "estimated_cost_value": 0.01636, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.35, "mode": "enforce", "reason": "approved: marginal ROI 21.394", "reason_code": "APPROVED", "recommendation_reason": "approved: marginal ROI 21.394", "recommendation_reason_code": "APPROVED", "recommended": true, "score": 0.33364, "uncertainty": 0.0}}, {"action": {"cost": {"latency_ms": 1400, "risk": 0.0, "tokens": 4500, "usd": 0.012}, "current_success_probability": 0.0, "expected_gain": 0.08, "fingerprint": "f83033a110b1105e98b597c52e17027bceab726f06b6f3438ff26d61deca992d", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "run the full test suite"}, "decision": {"allowed": false, "confidence": 1.0, "estimated_cost_value": 0.10479999999999999, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.08, "mode": "enforce", "reason": "rejected: marginal ROI 0.763 below 1.000", "reason_code": "MARGINAL_ROI_REJECTED", "recommendation_reason": "rejected: marginal ROI 0.763 below 1.000", "recommendation_reason_code": "MARGINAL_ROI_REJECTED", "recommended": false, "score": -0.02479999999999999, "uncertainty": 0.0}}, {"action": {"cost": {"latency_ms": 4000, "risk": 0.0, "tokens": 11000, "usd": 0.17}, "current_success_probability": 0.0, "expected_gain": 0.05, "fingerprint": "c444a2361858cb3bcc7b1a4e2596a0ef1248b3aa5978f63a07b40ad62cf10db2", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "request a premium model audit"}, "decision": {"allowed": false, "confidence": 1.0, "estimated_cost_value": 0.398, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.05, "mode": "enforce", "reason": "rejected: marginal ROI 0.126 below 1.000", "reason_code": "MARGINAL_ROI_REJECTED", "recommendation_reason": "rejected: marginal ROI 0.126 below 1.000", "recommendation_reason_code": "MARGINAL_ROI_REJECTED", "recommended": false, "score": -0.34800000000000003, "uncertainty": 0.0}}], "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "candidate_ranking", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "treasury": "killer-demo"} +{"action": {"cost": {"latency_ms": 180, "risk": 0.0, "tokens": 700, "usd": 0.002}, "current_success_probability": 0.0, "expected_gain": 0.35, "fingerprint": "313d64e7457c58d692b5f2ec902a279c32ca88807380c35dc464afaed301d70f", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "run the targeted verifier"}, "decision": {"allowed": true, "confidence": 1.0, "estimated_cost_value": 0.01636, "estimator_name": "historical-mean", "estimator_version": "2.0.0", "expected_gain": 0.35, "mode": "enforce", "reason": "approved: marginal ROI 21.394", "reason_code": "APPROVED", "recommendation_reason": "approved: marginal ROI 21.394", "recommendation_reason_code": "APPROVED", "recommended": true, "score": 0.33364, "uncertainty": 0.0}, "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "authorization", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "reserved": {"latency_ms": 180, "risk": 0.0, "tokens": 700, "usd": 0.002}, "treasury": "killer-demo", "usage": {"latency_ms": 1050, "risk": 0.0, "tokens": 3600, "usd": 0.024}} +{"action": {"cost": {"latency_ms": 180, "risk": 0.0, "tokens": 700, "usd": 0.002}, "current_success_probability": 0.0, "expected_gain": 0.35, "fingerprint": "313d64e7457c58d692b5f2ec902a279c32ca88807380c35dc464afaed301d70f", "is_verification": true, "kind": "verification", "metadata": {"stage": "verify"}, "name": "run the targeted verifier"}, "budget_overrun": false, "estimator": {"config_hash": "8bd2358da0bef14a0514ddbdc9505cac5a645a024d8a722af2f1793a2ea8ab92", "name": "historical-mean", "training_data_fingerprint": null, "version": "2.0.0"}, "event": "commit", "mode": "enforce", "policy": {"config_hash": "06ff44c755d4d09cdad5efbf5feb55af755e760db18699d07077db84ae4bb8d0", "name": "marginal-reference", "version": "2.0.0"}, "treasury": "killer-demo", "usage": {"latency_ms": 1230, "risk": 0.0, "tokens": 4300, "usd": 0.026000000000000002}, "violations": []} diff --git a/docs/api.md b/docs/api.md index bde9121..1ea027d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,102 +1,219 @@ # API reference -## `Cost` +## Core values ```python Cost(tokens=0, usd=0.0, latency_ms=0, risk=0.0) +TokenUsage( + input_tokens=0, + cached_input_tokens=0, + output_tokens=0, + reasoning_tokens=0, +) +Action(name="run tests", kind="verification", cost=Cost(tokens=500)) +Decision(allowed=True, reason="approved") ``` -All values must be finite and non-negative. Token and latency counters must be integers. +`TokenUsage` components are additive. `input_tokens` means uncached input. When a provider reports reasoning as a subset of total output, the common extractor normalizes `output_tokens` to non-reasoning output. + +`Decision.allowed` is the behavior applied by the current mode. `Decision.recommended` is the policy recommendation before a Shadow or Recommend override. The original v0.1 fields and constructors remain supported. -## `Action` +## Execution modes ```python -Action( - name="run tests", - kind="verification", - cost=Cost(tokens=500), - expected_gain=0.15, - current_success_probability=0.70, - is_verification=True, - fingerprint=None, - metadata={"suite": "unit"}, -) +ExecutionMode.SHADOW +ExecutionMode.RECOMMEND +ExecutionMode.ENFORCE ``` -Expected gain and current probability are bounded between zero and one. +- `shadow`: execute proposed work while recording recommendations; +- `recommend`: execute proposed work and surface advisory decisions; +- `enforce`: apply policy and hard-budget denials. + +`fund_best` is an explicit allocation operation and remains selective in every mode. -## `PolicyConfig` +## Estimation ```python -PolicyConfig( - outcome_value_usd=1.0, - token_shadow_price_per_million_usd=0.0, - latency_shadow_price_per_second_usd=0.0, - risk_shadow_price_usd=1.0, - minimum_roi=1.0, - minimum_expected_gain=0.0, - target_success_probability=1.0, +ValueEstimator( + default_gain=0.05, + name="historical-mean", + version="2.0.0", + context_fields=("engine", "phase", "task_type", "language", "model"), ) ``` -## `BudgetLimits` +Primary methods: -```python -BudgetLimits( - max_tokens=None, - max_usd=None, - max_latency_ms=None, - max_risk=None, - verification_reserve_tokens=0, - verification_reserve_usd=0.0, -) -``` +- `estimate(action) -> float`; +- `estimate_detail(action) -> ValueEstimate`; +- `observe(kind, realized_gain)` for v0.1 compatibility; +- `observe_action(action, realized_gain)` for contextual observations. + +Every action observation updates both its contextual bucket and the action-kind fallback. The estimator identity includes a stable configuration hash and a training-data fingerprint that changes when online observations change. + +`EstimatorRegistry.register(estimator)` and `resolve(name, version)` provide explicit name/version resolution. The registry key remains stable while `identity.training_data_fingerprint` identifies current learned state. + +## Policy -A non-zero verification reserve requires the corresponding token or USD maximum. +`MarginalPolicy` accepts `PolicyConfig`, an estimator, and optional policy name/version. `identity` contains a stable configuration hash. `build_policy(profile)` creates transparent reference policies: -## `Treasury` +- `quality-first`; +- `balanced`; +- `token-saver`; +- `strict-budget`. + +Profiles are reference defaults, not universal calibrations. + +## Treasury + +```python +Treasury(limits, policy=..., trace_sink=..., mode="shadow") +``` Primary methods: +- `evaluate(action) -> Decision` without reservation; - `authorize(action) -> Decision`; -- `propose(action) -> Decision` alias; - `fund_best(actions) -> Allocation | None`; -- `is_authorized(action) -> bool`; - `commit(action) -> BudgetUsage`; -- `abort(action, reason=...) -> None`; +- `settle_failure(action, actual_cost, reason=...) -> BudgetUsage`; +- `abort(action, reason=...)`; +- `observe_value(action, realized_gain)`; +- `record_outcome(outcome)`; - `child(name, limits) -> Treasury`; - `summary() -> dict`. -## Exceptions +Failed settlements record spend but do not mark an action as a completed duplicate, allowing a legitimate retry. Non-blocking modes can hold multiple concurrent reservations for the same semantic fingerprint without dropping accounting. -- `ActionDenied`: wrapper refused execution; -- `AuthorizationRequired`: commit or abort without approval; -- `BudgetExceeded`: direct ledger operation exceeded a hard limit; -- `BudgetOverrun`: actual settled usage exceeded its reservation or hard limit. - -## Usage extractors +## Decision Ledger ```python -def extractor(result: object, estimated_cost: Cost) -> Cost: - ... +JsonlDecisionLedger( + path, + context=DecisionLedgerContext(run_id="..."), + privacy_profile="safe_telemetry", + privacy_key_path=".marginal/privacy.key", +) +read_decision_ledger(path) +summarize_decision_ledger(records) +export_decision_ledger( + source, + destination, + privacy_profile="aggregate_export", + minimum_group_size=5, +) +``` + +Ledger v2 requires a valid schema version, event ID, monotonically increasing sequence, timestamp, run ID, and event name. Reserved envelope fields cannot be overridden by caller events. When the ledger context contains a task ID, outcome records must match it. + +## Privacy API + +Public privacy values and functions: + +- `PrivacyProfile`: `LOCAL_FULL`, `SAFE_TELEMETRY`, and `AGGREGATE_EXPORT`; +- `PrivacyClass`: safe-by-default, pseudonymous, and potentially sensitive; +- `FIELD_CLASSIFICATION`: published classification for representative ledger fields; +- `LocalPseudonymizer(key)`: field-separated HMAC-SHA-256 pseudonyms; +- `generate_local_identifier(namespace)`: opaque random local correlation IDs; +- `load_or_create_privacy_key(path)`: local 256-bit key management; +- `sanitize_ledger_record(record, profile=..., pseudonymizer=...)`; +- `validate_safe_telemetry_record(record)`: reject malformed pseudonyms, unreviewed fields, free text, and noncanonical strict records; +- `aggregate_ledger_records(records, minimum_group_size=5)`; +- `export_decision_ledger(source, destination, privacy_profile=..., minimum_group_size=5)`. + Aggregate groups below the threshold are suppressed. Destinations are created exclusively and + are never overwritten. + +`JsonlDecisionLedger` accepts `privacy_profile`, `privacy_key`, and `privacy_key_path`. +`aggregate_export` is rejected as an operational profile and must use the export API. Export +destinations are not overwritten. See [`privacy.md`](privacy.md) for field behavior and threat +model. + +## Universal protocol + +Public protocol values: + +- `AgentAction`; +- `AgentEvent` and `AgentEventType`; +- `AgentDecision`; +- `AgentDirective`; +- `AgentCapabilities`; +- `DeduplicationScope`; +- `UniversalRuntime`. + +`AgentAction`, `AgentEvent`, `AgentDecision`, and `AgentCapabilities` provide strict dictionary serialization and parsing where applicable. Protocol metadata used for fingerprinting must be JSON serializable. + +Protocol v1 directives are: + +```text +allow · deny · modify · defer · reuse · stop · force_verify ``` -The extractor must return the complete actual or best-known cost. +The v0.2 reference runtime maps core decisions to `allow` or `deny`. The richer directives and replacement payload are stable adapter-extension contracts; MARGINAL does not claim the reference policy automatically generates them. + +`UniversalRuntime` in Enforce Mode requires `AgentCapabilities(block_actions=True)`. Observe-only adapters cannot be represented as enforced integrations. + +## Wrappers + +`budgeted_call`, `async_budgeted_call`, `funded_call`, and `async_funded_call` accept: -## Execution helpers +- `usage_extractor(result, estimated_cost) -> Cost`; +- `failure_usage_extractor(error, estimated_cost) -> Cost | None`. -- `budgeted_call`: authorize, execute, and settle one synchronous callable; -- `async_budgeted_call`: asynchronous equivalent; -- `funded_call`: execute and settle an `Allocation` reserved by `fund_best`; -- `async_funded_call`: asynchronous funded-allocation equivalent. +A failure extractor returning `None` means no spend was observed and releases the reservation. Returning `Cost` settles measured or best-known failed spend. If the extractor itself fails, MARGINAL conservatively settles the reserved estimate and keeps the original execution exception primary, with the extraction error chained as its cause. -## Killer demo +## Usage extraction + +- `extract_common_llm_usage(result, estimate) -> Cost` preserves v0.1 total-token accounting; +- `extract_common_token_usage(result) -> TokenUsage` returns an additive breakdown. + +Provider schemas differ. Test the exact SDK response shape used by an integration and preserve raw provider evidence outside the core when detailed auditability is required. + +## Packaged schemas ```python -from marginal import run_killer_demo +from marginal import available_schemas, load_schema + +for name in available_schemas(): + schema = load_schema(name) +``` + +The public schema API reads immutable JSON resources bundled in the installed wheel. Names are restricted to known basenames; path traversal and unknown resources are rejected. The same source contracts remain available under [`schemas/`](../schemas/). + +Privacy-specific contracts include `safe-telemetry-v1.json`, which recursively rejects unreviewed fields from strict event-level exports, and `aggregate-export-v1.json`, which accepts only grouped generalized rows. -result = run_killer_demo("killer-demo-output") +## Public benchmark comparison + +```python +compare_runs( + baseline, + marginal, + bootstrap_samples=2_000, + seed=42, + confidence_level=0.95, + quality_margin_pp=1.0, +) +``` + +The comparator requires matched task IDs, rejects type-coerced booleans and numbers, reports configurable task-level bootstrap uncertainty, applies the caller-provided non-inferiority margin, and computes tokens and USD per resolved task. The margin must be chosen before inspecting final results. + +## Trace sinks + +- `JsonlTraceSink`: legacy v0.1-compatible JSONL; +- `JsonlDecisionLedger`: strict v0.2 evidence; +- `CompositeTraceSink`: deterministic fan-out to multiple sinks. + +Composite fan-out is sequential, not an atomic distributed write across sinks. Choose one authoritative ledger when cross-sink atomicity is required. + +## Outcomes + +`Outcome` stores task-level reward, optional resolved status, verifier identity, trajectory identity, metrics, and evidence. It does not assign causal credit to individual actions. + +## Replay + +```python +result = replay_ledger("ledger.jsonl", policy, limits) +report = render_replay_report(result) ``` -The function returns the complete structured result and optionally writes Markdown, HTML, -SVG, JSON, and JSONL trace artifacts. +Replay is an off-policy recommendation diagnostic over recorded actions and estimated costs. It does not simulate unobserved trajectories, infer preserved quality, or establish causal savings. diff --git a/docs/architecture.md b/docs/architecture.md index ac4b99c..8cce259 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,83 +1,76 @@ # Architecture -MARGINAL separates description, ranking, reservation, execution, and settlement. - ```text -Candidate actions - │ - ├─► hard child and parent budgets - ├─► pending reservations - ├─► duplicate and stopping checks - ├─► ValueEstimator.estimate - └─► MarginalPolicy.evaluate - │ - rank by marginal score - │ - reserve best candidate - │ - execute callable - ┌───┴────┐ - success failure - │ │ - settle actual abort - │ │ - committed released - └────┬─────┘ - append trace +AI development agent + │ native hook/event + ▼ +thin engine adapter + │ AgentAction / AgentEvent + ▼ +UniversalRuntime + │ Action + ▼ +Treasury ──► MarginalPolicy ──► ValueEstimator + │ │ │ + │ applied │ recommendation │ versioned estimate + state fingerprint + ▼ ▼ ▼ +reserve → execute → settle / abort / failure settlement + │ + └──► JsonlDecisionLedger ──► privacy profile ──► outcome / replay / export ``` -## Modules +## Module boundaries - `models.py`: immutable provider-neutral values; -- `budget.py`: hard constraints, reservations, settlement, and accounting; -- `estimator.py`: transparent expected-gain estimates; -- `policy.py`: economic scoring and explanations; -- `fingerprint.py`: deterministic action and call identity; -- `treasury.py`: lifecycle, ranking, hierarchy, and atomic coordination; -- `adapters.py`: guarded sync and async Python or SDK calls; -- `trace.py`: append-only JSONL evidence; -- `cli.py`: trace validation, reporting, and demonstration; -- `benchmark.py`: deterministic synthetic scenarios. +- `modes.py`: Shadow, Recommend, and Enforce semantics; +- `budget.py`: hard limits, reservations, settlement, and accounting; +- `estimator.py`: transparent versioned value estimates and learned-state identity; +- `registry.py`: estimator name/version resolution; +- `policy.py`: deterministic economic scoring and reason codes; +- `profiles.py`: transparent reference policy configurations; +- `fingerprint.py`: deterministic core action and call identity; +- `treasury.py`: mode-aware lifecycle, hierarchy, atomic coordination, and evidence; +- `adapters.py`: guarded sync/async execution and failure usage settlement; +- `outcomes.py`: verified task outcome contract; +- `ledger.py`: strict schema-versioned evidence and privacy-aware export orchestration; +- `privacy.py`: field classification, keyed pseudonymization, strict sanitization, and aggregate grouping; +- `protocol.py`: universal adapter contract, directives, and capability negotiation; +- `runtime.py`: normalized local engine-session lifecycle; +- `replay.py`: non-causal off-policy decision replay; +- `trace.py`: legacy trace sinks and deterministic fan-out; +- `cli.py`: trace, ledger, privacy export, replay, benchmark, and demo commands. + +## Shadow authorization -## Authorization lifecycle +Shadow Mode still creates reservations, including unchecked reservations for would-deny actions. This lets later recommendations observe pending demand while the external agent continues unchanged. -`authorize` evaluates an action and reserves its estimated cost across the child ledger and -every ancestor. The root and its children share one re-entrant lock, preventing concurrent -fan-out from oversubscribing a parent budget. Pending fingerprints also retain their owning -treasury, preventing cross-sibling settlement or cancellation. +Concurrent semantic duplicates receive unique internal reservation identities. The semantic fingerprint remains in evidence and duplicate recommendations, while each actual execution is separately reserved and settled. -Committed usage remains separate from reserved usage. `Treasury.summary()` exposes both. +Settlement records actual usage and violations without raising a caller-visible overrun. -## Settlement lifecycle +## Enforced authorization -`commit` replaces every reservation in the hierarchy with actual usage. All levels are -updated consistently. If actual usage exceeds one or more limits, a `BudgetOverrun` is -raised only after the spend is recorded and traced. +Enforce Mode preserves v0.1 behavior: affordability and policy denial prevent execution; reservations are transactional; actual overruns are recorded before `BudgetOverrun` is raised. -`abort` releases every reservation without recording spend. The sync and async wrappers call -it automatically when the guarded callable raises. +## Failure boundary -Trace writes participate in the authorization transaction: a failed authorization trace rolls -back reservations and counters. Abort releases state before tracing; if tracing also fails, the -guarded callable's original exception is re-raised with the trace failure chained as its cause. -Settlement remains fail-truthful: once external work has occurred, accounting is never rolled -back merely because a trace sink fails. +No observed spend releases a reservation. Measured failed spend is committed without marking the action as successfully completed. If failure usage extraction fails, the reserved estimate is settled conservatively and the original execution exception remains primary. -## Candidate ranking +## Evidence boundary -`fund_best` evaluates all supplied candidates under the same locked state. Allowed candidates -are ordered by: +`record_outcome` records task evidence. `observe_value` records explicit action-level realized gain. The separation prevents causal credit from being assigned merely because an action appeared in a successful trajectory. -1. marginal score; -2. capped expected gain; -3. lower estimated cost value; -4. deterministic fingerprint tie-break. +Decision Ledger writes are strict JSON and sequence-safe within one process. Composite sink fan-out and multiple processes are not an atomic distributed transaction; deployments needing that property must provide an external transactional sink. -Only the selected candidate is reserved. The full candidate evaluation is emitted as a -`candidate_ranking` trace event. +## Privacy boundary -## Extension boundaries +`JsonlDecisionLedger` constructs the complete event, validates task/outcome consistency, then +applies the configured profile before serialization. `local_full` preserves the event. +`safe_telemetry` uses a strict allowlist, field-separated HMAC pseudonyms, and UTC-day timestamp +generalization. Unknown custom fields are dropped. `aggregate_export` is not accepted by the +operational sink; it reads a completed ledger, suppresses groups below a configurable threshold +of five by default, and writes grouped generalized rows through a separate file with overwrite +protection. -Applications can replace the estimator or policy without changing actions, budgets, -wrappers, or traces. Framework adapters should translate native events into `Action` and -`Cost` rather than adding provider logic to the core. +Keys remain local and are not part of a trace transaction. Losing a key prevents future stable +correlation but does not make existing pseudonyms anonymous. diff --git a/docs/benchmarking.md b/docs/benchmarking.md index aef67e2..f875ea3 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -1,48 +1,48 @@ # Benchmarking -## Killer demo +## Demonstrations versus measured benchmarks -`marginal killer-demo --output killer-demo-output` runs an end-to-end coding workflow -against a generated buggy repository. It compares a run-everything baseline with MARGINAL's -funded action in each diagnose, fix, and verification stage. Both paths must pass the same -deterministic verifier. The generated HTML, Markdown, SVG, JSON, and JSONL trace make every -decision inspectable. +The Killer Demo and bundled synthetic benchmark test allocator behavior with declared costs. They do not measure a provider and are not universal savings claims. -The committed result is available in [`demos/killer-demo`](../demos/killer-demo/RESULTS.md). -Its token, USD, and latency values are declared action-cost estimates used to exercise the -allocator; they are not provider telemetry. +## Required paired protocol -It demonstrates the mechanism, but it is not a production benchmark or a claim that all -workloads will save 94.09%. +A real benchmark should keep constant: -## Bundled synthetic benchmark +- task and dataset version; +- model and provider version; +- system and user prompt; +- tools and permissions; +- repository state; +- time and token limits; +- verifier; +- task order and retry policy. -`marginal demo` runs five deterministic task scenarios. Each contains three actions needed -for a verified outcome and two low-value redundant actions. The baseline executes every -action. MARGINAL uses the reference policy and executes only actions that clear the economic -threshold. +Compare baseline and MARGINAL on matched task IDs. Do not drop failures or impute missing runs. Benchmark rows are parsed strictly: boolean and numeric strings are rejected rather than coerced. -The benchmark tests accounting, policy behavior, reserves, and reproducibility. It is not -evidence that every real agent will save the same percentage. +## Required metrics -## Required production metrics - -Real evaluations should report: - -- task and verifier definition; -- model and provider versions; -- success rate with confidence intervals; -- input, output, cached, and reasoning tokens where available; +- resolution rate and confidence interval; +- quality non-inferiority margin defined before results; +- uncached input, cached input, non-reasoning output, reasoning, and total tokens where available; - direct cost and latency; - tool and sub-agent calls; -- denied-action reasons; -- cost per verified successful outcome; -- quality difference against an uncontrolled baseline. +- regressions and recoveries; +- cost per verified successful task; +- policy and estimator identities, including learned-state fingerprint; +- denied and recommended reason distribution; +- raw paired result files. + + +The bundled comparator exposes `--confidence-level`, `--quality-margin-pp`, `--bootstrap-samples`, and `--seed`. Record these values with the raw inputs. Its efficiency section reports tokens and USD per resolved task; a zero-resolved condition is reported as unavailable rather than divided by zero. + +## Shadow evaluation + +Shadow Mode is ideal for integration safety, estimator calibration, and false-denial analysis, but does not itself produce realized token savings because all actions still execute. + +## Replay -Savings without preserved quality are not optimization. +Replay is useful for policy sensitivity analysis. It cannot model state changes from actions a different policy would have denied. Replay output must remain labeled estimated and non-causal. Malformed ledger authorization records are rejected. -## Benchmark contribution rules +## Causal evaluation -A contributed benchmark must be runnable, pinned to a dataset version, free from hidden -manual steps, and explicit about synthetic or simulated values. Raw result files should be -included or reproducibly generated. +Causal marginal-value work requires an identification strategy, such as controlled randomization, paired trajectories, valid propensity logging, or another justified design. Historical success association alone is insufficient. diff --git a/docs/concepts.md b/docs/concepts.md index cbe2bf5..fb0db5e 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -1,80 +1,69 @@ # Concepts -## Action +## Action, Cost, and TokenUsage -An `Action` is a proposed unit of work. It has a stable name and kind, an estimated `Cost`, -an optional expected success gain, and caller-defined metadata. +An `Action` is proposed work. `Cost` records total tokens, direct USD, latency, and application-defined risk. `TokenUsage` records additive uncached input, cached input, non-reasoning output, reasoning, and total tokens. -## Cost +The provider-neutral hard ledger continues to use `Cost.tokens`. The detailed token breakdown is evidence for analysis, pricing, and benchmarks. -`Cost` normalizes four non-negative dimensions: +## Applied decision versus recommendation -- token count; -- direct USD spend; -- latency in milliseconds; -- application-defined risk. +`Decision.allowed` is what the execution mode applies. `Decision.recommended` is what the policy recommended before a non-blocking override. -`PolicyConfig` converts them to a common value unit using optional shadow prices. Hard -budgets always inspect the original dimensions directly. +- Enforce: applied equals recommended. +- Shadow: every proposed action is applied, but the recommendation is preserved. +- Recommend: non-blocking behavior intended for visible advisory integrations. -## Expected gain +Stable reason codes support analytics without forcing integrations to parse human-readable strings. -Expected gain is the estimated increase in the probability of a verified successful -outcome. The policy caps it by the remaining distance to the configured success target, so -an action cannot claim more probability improvement than remains possible. +## Directives -Applications may provide expected gain directly or use a `ValueEstimator`. +The universal protocol represents adapter instructions as `AgentDirective` values: allow, deny, modify, defer, reuse, stop, and force-verify. Core v0.2 decisions currently produce allow or deny. Other directives are explicit extension points for adapters and future policies. -## Shadow price +## Capability negotiation -A shadow price represents the opportunity cost of consuming a scarce resource. It is -separate from direct provider billing: +`AgentCapabilities` states what an adapter can observe or control. Capability level is derived rather than trusted from input. Enforce Mode requires real action-blocking capability; a prompt convention or advisory skill is not enforcement. -- `Cost.usd` is direct spend; -- `token_shadow_price_per_million_usd` prices scarce tokens; -- `latency_shadow_price_per_second_usd` prices waiting time; -- `risk_shadow_price_usd` prices the configured risk quantity. +## Reservation and settlement -## Treasury +Authorization reserves estimated resources. Commit replaces the reservation with actual usage. Abort releases the reservation when no external spend occurred. Failure settlement records measured or conservatively estimated spend from a failed external action. -A `Treasury` owns a policy, committed usage, pending reservations, duplicate state, and an -optional parent treasury. One root treasury should normally represent one task or workflow. +A failed action is accounted but not marked as a successfully completed duplicate. Concurrent semantic duplicates in non-blocking modes receive separate internal reservation identities so Shadow Mode does not alter agent behavior. -## Reservation +## Verification reserve -Approval reserves estimated resources immediately but does not add them to committed usage. -Reservations prevent parallel or sequential authorizations from promising the same budget -twice. Each reservation records its owning treasury, so siblings cannot settle or abort one -another's work. Reservations are converted to actual usage on `commit` or released on -`abort`. +A verification reserve protects tokens or USD that only verification actions may consume. It prevents generation from exhausting the entire budget before tests or checks can run. -## Verification reserve +## Decision Ledger + +The Decision Ledger is a schema-versioned JSONL evidence stream. It correlates actions, decisions, identities, costs, failures, observations, and outcomes. Avoiding prompt and output fields is not sufficient by itself because quasi-identifiers and free text can still reveal sensitive information. + +Every new ledger declares `local_full` or `safe_telemetry`. `local_full` preserves the complete operational event. `safe_telemetry` uses a strict allowlist, removes potentially sensitive content, pseudonymizes identifiers with a local key, and generalizes exact timestamps. `aggregate_export` is a separate grouped export with no identifiers or timestamps and suppresses +groups smaller than five records by default. + +It is append-only at the application level, not cryptographically tamper-proof and not multi-process transactional. Pseudonymization is not anonymization. + +## Outcome + +An `Outcome` describes a verified task-level result. It does not automatically assign causal credit to preceding actions. Outcome task identity must match the runtime or ledger context when one is declared. -Agents often spend an entire budget generating an answer and leave nothing for validation. -A verification reserve protects tokens or USD that only actions marked `is_verification` -may consume. A reserve requires the corresponding `max_tokens` or `max_usd` hard limit. -Verification already spent does not reduce the regular allocation unnecessarily. +## Value estimate and estimator state -## Allocation +`ValueEstimate` includes expected gain, uncertainty, confidence, sample size, provenance, and estimator identity. Explicit estimates remain supported. Historical estimates are observational. -`fund_best` evaluates a set of candidates against the same state and reserves the affordable -candidate with the highest marginal score. The returned `Allocation` contains the prepared -action and its explainable decision. Use `funded_call` or `async_funded_call` to execute and -settle it safely. +Contextual observations also update the action-kind fallback. Online observations update `training_data_fingerprint`, so otherwise identical estimator versions with different learned state can be distinguished. -## Duplicate action +## Fingerprints and deduplication -Direct treasury actions are fingerprinted from their declared action fields. Guarded -callables additionally include callable identity and arguments. MARGINAL performs exact, -deterministic deduplication; it does not claim semantic-similarity detection. +Core guarded calls use deterministic semantic fingerprints. Universal actions add state-aware scopes: -## Settlement and overrun +- exact; +- once per state; +- once per phase; +- retry-number aware. -Settlement replaces a reservation with actual usage. If actual usage exceeds a limit, the -spend is still recorded because execution has already occurred, then `BudgetOverrun` is -raised. This preserves truthful accounting. +Protocol fingerprint metadata must be JSON serializable. Arbitrary object `repr` values are not accepted because they may be process-dependent. -## Decision +## Replay -Every `Decision` contains `allowed`, `reason`, `score`, `expected_gain`, and the estimated -cost value. Policies remain inspectable and auditable. +Replay asks what another policy would have recommended for recorded proposed actions. It does not know what unexecuted trajectories would have produced and is not causal proof. diff --git a/docs/faq.md b/docs/faq.md index b5e8712..ad55046 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,55 +1,49 @@ -# Frequently asked questions +# FAQ -## Is MARGINAL a prompt compressor? +## Does MARGINAL guarantee fewer tokens on every request? -No. Prompt compressors reduce the size of a call. MARGINAL can decide not to make a -low-value call at all. The two approaches can be combined. +No. Some requests are already minimal, and some require additional verification. The target is lower avoidable compute per verified successful task across representative sessions. -## Is MARGINAL a model router? +## Does Shadow Mode save tokens? -No. A router chooses a model. MARGINAL can evaluate multiple model calls as candidate -actions and fund the best one. +No action is blocked in Shadow Mode. It creates the evidence needed to estimate which future enforcement decisions may be safe. -## Is this only a hard token budget? +## Can Shadow Mode observe repeated concurrent actions? -No. Hard budgets are enforced, but the policy also compares expected success gain with -direct cost and configured shadow prices. +Yes. Semantic duplicates are still recommended as duplicates, but non-blocking modes keep separate internal reservations so every execution is accounted without changing the agent's behavior. -## Does MARGINAL estimate expected gain automatically? +## Is replay proof that denied actions were unnecessary? -The reference estimator supports explicit values, defaults, and transparent observed -averages by action kind. Causal estimation and counterfactual replay are future validation -work, not current claims. +No. Replay only reclassifies recorded proposed actions. It does not simulate the trajectory that would follow a denial. -## Does authorization consume budget? +## Does MARGINAL learn automatically from every successful task? -It creates a reservation. Committed usage remains unchanged until settlement, but other -actions cannot spend the reserved capacity. +It records outcomes, but it does not assign causal credit automatically. Applications must provide defensible action-level realized gain or later use a validated estimator. -## What happens when a call fails? +## How is estimator state versioned? -The guarded sync and async wrappers abort and release the reservation, then re-raise the -original exception. +The estimator identity includes name, version, configuration hash, and training-data fingerprint. Online observations update the training-data fingerprint. -## What happens when actual usage exceeds the estimate? +## What happens if a failed-call usage extractor also fails? -The actual spend is recorded and traced, then `BudgetOverrun` is raised. Future decisions -see the real overrun. +MARGINAL conservatively settles the reserved estimate, releases the reservation, and re-raises the original execution failure with the extraction error chained as its cause. -## What happens when the trace sink fails? +## Are Codex, Claude Code, Copilot, and OpenCode already supported? -An authorization trace failure rolls back the new reservation and approval counter. If a -guarded callable fails and abort tracing also fails, MARGINAL releases the reservation and -re-raises the callable's original exception with the trace error chained as its cause. After -external work succeeds, committed accounting is not rolled back if settlement tracing fails. +Version `0.2.0` provides the shared protocol, schemas, and local runtime. Vendor-specific adapters remain roadmap milestones and are not claimed complete. -## Does MARGINAL store prompts? +## Does the protocol already generate modify, defer, reuse, stop, and force-verify actions? -Not by default. Automatic call fingerprints hash inputs and traces store the digest. Action -metadata is included in traces, so applications must not put secrets there. +The protocol defines those directives so adapters share one contract. The v0.2 reference policy and runtime currently generate allow and deny. Richer automatic directives remain future policy and adapter work. -## Is it thread-safe? +## Does MARGINAL upload code or prompts? -A root treasury and all its children share one re-entrant lock for authorization, -reservation, settlement, and abort operations. Reservations are owner-bound, and the JSONL -sink is thread-safe within one process. +The core has no mandatory network service and does not upload data. Prompts and outputs are not added automatically, but task IDs, action names, model identity, metadata, verifier details, error text, and exact timestamps can still be sensitive. Use `SAFE_TELEMETRY` to remove free text and pseudonymize identifiers, or `AGGREGATE_EXPORT` for grouped sharing. `LOCAL_FULL` preserves caller content. Pseudonymization is not anonymization. + +## Where is the pseudonymization key stored? + +Supply `privacy_key_path` explicitly or let a strict ledger create a hidden owner-only key beside the ledger. Keep the key outside version control and do not share an operational key with the dataset it protects. Existing group-readable or world-readable key files are rejected on POSIX systems. + +## Is the Decision Ledger tamper-proof? + +No. It is append-only at the application level, not cryptographically immutable. Use external signing or immutable storage when tamper evidence is required. diff --git a/docs/governance.md b/docs/governance.md index 56ca493..c1de5f9 100644 --- a/docs/governance.md +++ b/docs/governance.md @@ -6,9 +6,10 @@ MARGINAL begins as a SignalLayer Labs-led open-source project. - routine fixes and documentation changes use normal pull-request review; - public API changes require rationale, compatibility notes, and tests; -- policy or trace format changes require a design discussion before implementation; +- policy, ledger, protocol, schema, or privacy-profile changes require a design discussion before implementation; - benchmark claims require reproducible evidence and independent review when practical; -- security-sensitive fixes may be developed privately before coordinated disclosure. +- security-sensitive fixes may be developed privately before coordinated disclosure; +- shareable telemetry changes require an explicit field-classification and quasi-identifier review. ## Compatibility @@ -21,3 +22,9 @@ major release after `1.0.0`. Maintainers protect technical integrity, transparent claims, contributor safety, and a small dependency-free core. Project influence follows sustained, reviewed contribution rather than employer or commercial status. + +## Privacy governance + +The operational Decision Ledger and shareable telemetry are separate products with separate contracts. `LOCAL_FULL` may retain caller-controlled local evidence; `SAFE_TELEMETRY` is a strict allowlist with keyed pseudonyms; `AGGREGATE_EXPORT` contains generalized grouped rows only. Unknown fields are treated as potentially sensitive. + +A change may not weaken a privacy profile silently. Any newly retained field requires tests, documentation, schema updates where applicable, and a migration or compatibility note. Pseudonymized data must never be described as anonymous. diff --git a/docs/index.md b/docs/index.md index d19fccb..599b316 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,18 +1,21 @@ # MARGINAL documentation -MARGINAL is a provider-neutral compute capital allocator for AI agents. Start with the -[quickstart](quickstart.md), then read the [concepts](concepts.md) and -[architecture](architecture.md). +MARGINAL is a provider-neutral decision, accounting, and evidence layer for economically disciplined AI agents. Version `0.2.0` adds the Learning Loop Foundation: non-blocking Shadow Mode, a versioned Decision Ledger, explicit privacy profiles, strict shareable-telemetry schemas, outcome contracts, versioned estimators, replay, packaged protocol schemas, and a universal adapter runtime. + +Start with the [quickstart](quickstart.md), then read the [concepts](concepts.md), [learning loop](learning-loop.md), and [architecture](architecture.md). ## Guides - [Quickstart](quickstart.md) - [Concepts](concepts.md) +- [Learning Loop Foundation](learning-loop.md) +- [Universal runtime](universal-runtime.md) +- [Privacy profiles](privacy.md) - [Architecture](architecture.md) - [API reference](api.md) - [Integrations](integrations.md) -- [Killer demo](../demos/killer-demo/RESULTS.md) - [Benchmarking](benchmarking.md) +- [Public benchmark protocol](public-benchmarks.md) - [Research and prior art](research.md) - [FAQ](faq.md) - [Governance](governance.md) diff --git a/docs/integrations.md b/docs/integrations.md index a00ea36..eff4d2e 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -1,70 +1,73 @@ # Integrations -## Generic synchronous callable +## Generic callable integration -`budgeted_call` works with any synchronous Python callable. It fingerprints the action, -callable, positional arguments, and keyword arguments; reserves the estimate; executes only -on approval; and settles actual usage. +Use `budgeted_call` or `async_budgeted_call` around an existing Python or SDK callable. Use `funded_call` or `async_funded_call` after `Treasury.fund_best`. -## Funded candidate execution +The wrappers authorize before execution, reserve estimated resources, settle actual usage, and release reservations when no spend occurred. -`funded_call` and `async_funded_call` execute an `Allocation` already reserved by -`Treasury.fund_best`. They verify that the reservation still exists before invoking the -callable, then commit actual usage or abort and release the reservation on failure. +## Usage extraction -## Generic asynchronous callable +`extract_common_llm_usage` returns total `Cost.tokens`. `extract_common_token_usage` returns normalized uncached input, cached input, non-reasoning output, reasoning, and total tokens. -`async_budgeted_call` provides the same lifecycle for `async` SDK methods and framework -steps. +Provider schemas differ in whether reasoning is included inside output. The extractor uses a declared total where available and recognizes reasoning reported through output-detail objects. Production adapters must test their exact SDK version. -```python -response = await async_budgeted_call( - treasury, - client.responses.create, - action=action, - usage_extractor=extract_common_llm_usage, - **request, -) -``` +## Failure accounting -## OpenAI-, Anthropic-, and LiteLLM-like SDKs +A failed external call may still be billed. Provide `failure_usage_extractor`: -Pass the existing SDK method as the callable. `extract_common_llm_usage` reads common usage -attributes or mappings: +- return `Cost` when spend is measured or best-known; +- return `None` only when no external spend occurred; +- if extraction fails, MARGINAL settles the reserved estimate conservatively. -- `total_tokens`; -- `input_tokens` and `output_tokens`; -- `prompt_tokens` and `completion_tokens`. +The original execution exception remains primary. Measured failure does not mark the action as a completed duplicate, so a valid retry remains possible and is charged independently. -Provider responses do not consistently expose billed cost, wall-clock latency, or -application risk. The built-in extractor therefore replaces token usage while preserving -those estimated dimensions. +## Universal engine adapters -A custom extractor receives `(result, estimated_cost)` and returns a complete `Cost`. +Use `AgentAction`, `AgentCapabilities`, `AgentDecision`, `AgentDirective`, and `UniversalRuntime`. Do not embed economic policy in an adapter. -## Framework adapter lifecycle +A thin adapter should: -A framework adapter should: +1. declare capabilities; +2. normalize a native proposed action; +3. call `before_action`; +4. apply or surface the returned directive; +5. call `after_action` with actual cost, or `fail_action` after failure; +6. record verifier outcomes; +7. classify identifiers, free text, verifier details, and errors before persistence; +8. select a Decision Ledger privacy profile appropriate to the trust boundary. -1. create one or more candidate `Action` objects; -2. use `fund_best` or `authorize`; -3. skip native execution on denial; -4. use `funded_call` after `fund_best`, or execute after direct authorization; -5. settle actual usage on success; -6. release the reservation on execution failure; -7. surface `BudgetOverrun` after settlement; -8. expose decision reasons in framework telemetry. +Enforce Mode requires `block_actions=True`. `UniversalRuntime` rejects an observe-only adapter configured as enforced. -Adapters should treat authorization trace failure as a denied transaction because MARGINAL has -already rolled back the reservation. Execution exceptions must remain the primary error when -abort telemetry fails; the built-in wrappers preserve this ordering automatically. +## Protocol directives -## Fingerprint inputs +Protocol v1 supports allow, deny, modify, defer, reuse, stop, and force-verify. The reference v0.2 runtime currently emits allow and deny based on core decisions. An adapter may transport the broader directive contract, but documentation must not imply those actions are generated automatically until a policy implements them. -The automatic call fingerprint supports deterministic JSON-like values, bytes, paths, -enums, sequences, sets, and mappings with string keys. For custom objects, provide an -explicit `Action.fingerprint` generated by the application. -The normal trace stores the digest, not the raw call arguments. A digest is not a secret; -low-entropy values may still be guessable, so sensitive identifiers should be salted or -replaced by application-controlled opaque IDs. +## Privacy for adapters + +Adapter-native logs are outside the Decision Ledger privacy boundary. Do not assume that using +`SAFE_TELEMETRY` sanitizes vendor logs, shell history, IDE telemetry, or custom callbacks. Keep +action kinds generic, use opaque local IDs, avoid embedding source paths in error messages, and +route shareable evidence through `ledger-export`. + +- use `LOCAL_FULL` only on a trusted local filesystem; +- use `SAFE_TELEMETRY` for structured event-level evidence with keyed pseudonyms; +- use `AGGREGATE_EXPORT` for grouped datasets intended to cross trust boundaries. + +Pseudonymization is not anonymization. Organizations should add retention limits, minimum-group +rules, access controls, and legal review appropriate to their data. + +## Integration labels + +Documentation must distinguish: + +- **Observe:** telemetry and non-blocking recommendations; +- **Tool Enforcement:** supported tool actions can be blocked or changed; +- **Full Compute Enforcement:** model turns, tools, retries, and stop behavior are controllable and measured. + +A prompt instruction, skill, or advisory middleware is not equivalent to enforced interception. + +## Current status + +Version `0.2.0` implements the universal adapter foundation, schemas, runtime, and conformance tests. Vendor-specific Codex, OpenCode, Claude Code, and GitHub Copilot adapters are roadmap work and must not be advertised as complete until tested against official control surfaces. diff --git a/docs/learning-loop.md b/docs/learning-loop.md new file mode 100644 index 0000000..62e9a94 --- /dev/null +++ b/docs/learning-loop.md @@ -0,0 +1,70 @@ +# Learning Loop Foundation + +MARGINAL's defensible direction is not a static ROI formula. It is a disciplined evidence loop: + +```text +observe proposed actions +→ record recommendations and applied behavior +→ measure actual cost and verified outcome +→ estimate action value with uncertainty +→ replay and compare policies +→ validate before stronger enforcement +``` + +## Why Shadow Mode comes first + +A policy that immediately denies actions observes only what it chose to execute. This creates selection bias. Shadow Mode records would-deny decisions while the underlying agent continues unchanged, producing evidence about the action and trajectory. + +Shadow data still does not prove causal value. Actions occur in sequences, outcomes are delayed, and successful tasks may contain unnecessary actions. Future causal work requires paired runs, controlled exploration, propensity logging, deterministic verifiers, and careful off-policy evaluation. + +## Evidence types + +### Decision evidence + +What the policy knew, estimated, recommended, and applied. + +### Usage evidence + +Estimated and actual tokens, direct cost, latency, and risk, including failed calls and conservative fallback accounting when usage extraction fails. + +### Outcome evidence + +Task-level verifier result, reward, metrics, and supporting evidence. + +### Action-level realized gain + +Explicit application-provided evidence that one action changed success probability. This is never inferred automatically from a task outcome. + + +## Privacy boundary + +Learning evidence can contain quasi-identifiers even without prompts or outputs. A task ID, +action name, model name, repository label, verifier, exception, or exact timestamp may identify +a customer or project. `LOCAL_FULL` keeps the complete trusted operational record. +`SAFE_TELEMETRY` removes potentially sensitive content, pseudonymizes identifiers with a local +key, and retains structured learning fields. `AGGREGATE_EXPORT` groups generalized rows and +removes identifiers and timestamps. + +Strict privacy profiles preserve policy and estimator versions, decisions, reason codes, cost, +confidence, uncertainty, and structured outcomes while dropping provenance and free text. This +lets calibration and policy analysis continue without treating caller metadata as shareable. +Pseudonymization is not anonymization; small or unusual groups can remain identifiable. + +## Estimator versioning and learned state + +Every useful learning record needs policy and estimator identity. Estimator identity contains: + +- implementation name; +- semantic version; +- configuration hash; +- training-data fingerprint. + +Online action observations update the training-data fingerprint. This separates two estimator instances that use the same code and configuration but have learned from different evidence. + +Contextual observations also update the action-kind fallback, allowing new contexts to benefit from broader evidence while still preferring exact contextual history when available. + +## Replay limits + +Replay can estimate how a policy would classify recorded proposed actions and their recorded costs. It cannot know whether denied actions would have changed later state or quality. Reports therefore use “estimated selected/avoided cost,” never “causal savings.” + +Malformed authorization evidence is rejected rather than coerced or silently skipped. diff --git a/docs/privacy.md b/docs/privacy.md new file mode 100644 index 0000000..53665cf --- /dev/null +++ b/docs/privacy.md @@ -0,0 +1,191 @@ +# Privacy profiles + +MARGINAL is local-first and has no mandatory network service, but locality alone does not +make telemetry safe to share. Identifiers, action names, model names, repository labels, +error text, verifier details, and caller metadata can reveal sensitive information even when +prompts and model outputs are absent. + +MARGINAL therefore classifies evidence fields and provides three explicit privacy profiles. + +## Field classes + +### Safe by default + +These fields are structured and retained by `safe_telemetry`: + +- generic event and action kind; +- estimated and actual cost; +- token breakdown and latency; +- applied and recommended decisions; +- stable reason codes; +- structured task reward and resolved status; +- policy and estimator versions; +- confidence, uncertainty, score, and schema version. + +Arbitrary strings are normalized or replaced with a generic value. Numeric sub-objects use +an allowlist, so custom metric names are not copied accidentally. + +### Pseudonymous + +These fields are transformed with field-separated HMAC-SHA-256 under a local key: + +- event, run, task, trajectory, and action identifiers; +- action fingerprints and state hashes; +- other explicit engine-instance identifiers when adapters expose them. + +Exact timestamps are generalized to UTC day boundaries. Pseudonyms are stable only for the +same key and field name. Different keys produce unlinkable identifiers. + +When external correlation is unnecessary, prefer opaque random IDs from +`generate_local_identifier("run")`, `generate_local_identifier("task")`, or another simple +namespace. Random local IDs avoid embedding customer or project names before sanitization. + +### Potentially sensitive + +The strict profile excludes: + +- free-form action names; +- complete model identity; +- metadata, tags, tool arguments, and replacement payloads; +- error, exception, abort, and failure text; +- human-readable policy reasons; +- verifier identity, evidence, and custom outcome metrics; +- treasury names and estimator training-data fingerprints. + +## Profiles + +### `LOCAL_FULL` + +`local_full` is the backward-compatible default. It preserves the complete operational +Decision Ledger record. Use it only where the ledger path and filesystem access are trusted. +Caller-provided metadata remains caller responsibility. + +```python +ledger = JsonlDecisionLedger( + "ledger.jsonl", + context=DecisionLedgerContext(run_id="local-run"), + privacy_profile="local_full", +) +``` + +### `SAFE_TELEMETRY` + +`safe_telemetry` removes free text and metadata, pseudonymizes identifiers, generalizes exact +timestamps, and keeps only allowlisted structured fields. Every strict record is validated when read through `read_decision_ledger(...)` and can also be +checked explicitly with `validate_safe_telemetry_record(...)`. The packaged +`safe-telemetry-v1.json` schema rejects unknown fields recursively. + +```python +ledger = JsonlDecisionLedger( + "safe-ledger.jsonl", + context=DecisionLedgerContext( + run_id="customer-acme-contract-2026", + task_id="customer-acme-contract-2026", + engine="codex", + model="internal-legal-model", + ), + privacy_profile="safe_telemetry", + privacy_key_path=".marginal/privacy.key", +) +``` + +New ledger and export files are created with owner-only permissions on POSIX systems. Existing +ledger append targets must be regular files, must not be symbolic links, and must not be accessible +by group or other users. When no key or key path is supplied, the ledger creates an owner-only +hidden key beside the ledger. +Generated keys contain 256 random bits and are never written into ledger records. +Keep the key outside version control and backups intended for sharing. + +An existing key file must be a regular file and, on POSIX systems, must not be readable by +group or other users. Symbolic-link key paths are rejected. + +### `AGGREGATE_EXPORT` + +`aggregate_export` is deliberately separate from operational ledger persistence. It groups +generalized decision and outcome rows, removes all identifiers and timestamps, and suppresses +any group containing fewer than five source records by default. The threshold is configurable +and is recorded in every emitted row. + +```bash +marginal ledger-export ledger.jsonl aggregate.jsonl \ + --privacy-profile aggregate_export --minimum-group-size 5 +``` + +A grouped decision row contains only fields such as: + +```json +{ + "schema_version": "1.0", + "privacy_profile": "aggregate_export", + "record_type": "decision", + "action_kind": "verification", + "cost_bucket": "low", + "gain_bucket": "medium", + "recommendation": "deny", + "applied_decision": "allow", + "reason_code": "SHADOW_OVERRIDE", + "outcome_class": "not_applicable", + "count": 12, + "minimum_group_size": 5 +} +``` + +Small groups are omitted entirely. Raising `--minimum-group-size` reduces disclosure risk but +can remove more data. Lowering it below five is intended only for controlled local analysis and +should not be treated as anonymous sharing. Default buckets are deterministic: + +- cost: `low` up to 2,000 tokens, USD 0.02, and 1 second; `medium` up to 10,000 +tokens, USD 0.20, and 10 seconds; otherwise `high`; +- expected gain: `low` below 0.10, `medium` below 0.30, otherwise `high`. + +## Exporting an existing ledger + +Create a new unlinkable safe export by using a dedicated export key: + +```bash +marginal ledger-export ledger.jsonl safe-export.jsonl \ + --privacy-profile safe_telemetry \ + --privacy-key-file .marginal/export.key +``` + +The API equivalent is: + +```python +from marginal import export_decision_ledger + +export_decision_ledger( + "ledger.jsonl", + "safe-export.jsonl", + privacy_profile="safe_telemetry", + privacy_key_path=".marginal/export.key", +) +``` + +Exports create the destination with an exclusive filesystem operation and never overwrite an +existing path, including when another process creates the destination after the initial check. This +prevents accidental replacement of an authoritative ledger or a previously reviewed dataset. + +## Threat model and limitations + +Pseudonymization is not anonymization. Stable pseudonyms can still be linkable within one +export, rare action patterns can identify a workload, and small aggregate groups may permit +inference. The profiles do not provide differential privacy, k-anonymity, encryption at rest, +cryptographic tamper evidence, multi-process locking, or compliance certification. + +Before sharing data: + +1. prefer `aggregate_export` over event-level telemetry; +2. inspect the generated file; +3. use a new export key rather than an operational key; +4. raise the default minimum group size when the dataset or population is small; +5. avoid combining exports with external datasets that restore identity; +6. treat source ledgers and pseudonymization keys as sensitive assets. + +The public classification map is available as `FIELD_CLASSIFICATION`. `classify_field(...)` +inherits reviewed classifications for nested fields such as `action.cost.tokens`, while unknown +paths default to potentially sensitive. Applications may use the map for UI explanations or +additional validation, but custom event fields are excluded by the strict profile unless MARGINAL +explicitly allowlists them. + +Use `load_schema("safe-telemetry-v1.json")` and +`load_schema("aggregate-export-v1.json")` to validate shareable outputs from an installed wheel. diff --git a/docs/public-benchmarks.md b/docs/public-benchmarks.md index 104fe2f..6159e11 100644 --- a/docs/public-benchmarks.md +++ b/docs/public-benchmarks.md @@ -1,67 +1,77 @@ # Public benchmark protocol -MARGINAL is evaluated as a runtime intervention, not as a model. The correct experiment runs the **same agent, model, prompt, tools, task order, and verifier** twice: +MARGINAL is evaluated as a runtime intervention, not as a model. The correct experiment +runs the **same agent, model, prompt, tools, task order, runtime limits, and verifier** under +matched conditions: -1. baseline runtime; -2. baseline runtime with MARGINAL authorization enabled. +1. the unmodified baseline runtime; +2. the same runtime with MARGINAL enabled. -The first supported public suites are: - -- **Claw-SWE-Bench Lite-80**: 80 tasks, ten per language across Java, Go, Rust, JS/TS, C/C++, Ruby, PHP, and Python. -- **SWE-bench Verified**: 500 human-validated Python issue-resolution tasks. -- Any benchmark that exports one matched JSONL row per task. +The release includes a provider-neutral comparator. It does not bundle provider credentials +or claim that a vendor-specific benchmark runner is already complete. ## Required row schema +Each JSONL file contains one object per matched task: + ```json {"instance_id":"django__django-11790","resolved":true,"tokens":48210,"usd":0.84,"latency_ms":182000,"tool_calls":27} ``` -Only `instance_id`, `resolved`, and `tokens` are required. Missing optional metrics default to zero. Baseline and MARGINAL files must contain exactly the same instance IDs; the evaluator refuses unmatched samples. +`instance_id`, `resolved`, and `tokens` are required. Optional metrics default to zero. +`resolved` must be a real JSON boolean; strings such as `"false"` are rejected. Baseline +and MARGINAL files must contain exactly the same instance IDs. ## Run the comparison ```bash -marginal public-eval baseline.jsonl marginal.jsonl > PUBLIC_BENCHMARK.md -marginal public-eval baseline.jsonl marginal.jsonl --json > public-benchmark.json +marginal public-eval baseline.jsonl marginal.jsonl \ + --confidence-level 0.95 --quality-margin-pp 1.0 \ + > PUBLIC_BENCHMARK.md +marginal public-eval baseline.jsonl marginal.jsonl \ + --confidence-level 0.95 --quality-margin-pp 1.0 --json \ + > public-benchmark.json ``` -The report includes: +The generated comparison reports: - resolve rate and percentage-point delta; - total token, USD, latency, and tool-call savings; - regressions and recoveries; -- a 95% task-level bootstrap interval for token savings; -- a quality-preservation flag requiring no more than one percentage point of resolve-rate loss. +- a configurable task-level bootstrap interval for token savings; +- tokens and USD per resolved task; +- whether the preregistered non-inferiority criterion is met. ## Fairness requirements -- Freeze model version, agent code, prompt, temperature, tools, runtime limits, and task order. -- Do not drop failed, timed-out, or expensive tasks. +- Freeze the model version, agent code, prompt, temperature, tools, limits, and task order. +- Do not drop failed, timed-out, prematurely stopped, or expensive tasks. - Count premature MARGINAL stops as unresolved. -- Export actual provider usage rather than character-count estimates when available. -- Publish both JSONL inputs and the generated report. -- Use at least three runs per task when the agent is stochastic, then report paired means. +- Export actual runtime or provider usage rather than character-count estimates. +- Publish both JSONL inputs, environment metadata, and the generated report. +- Use repeated paired runs when the agent is stochastic. +- Preregister the quality non-inferiority margin before inspecting the final result. +- Keep synthetic demonstrations separate from measured runtime claims. -## Claw-SWE-Bench Lite-80 +## Token telemetry -The public Lite subset contains 80 tasks selected from a 350-task multilingual set using a cost-aware, rank-aware calibration procedure. Install and load it with: +Where the runtime exposes it, collect and publish: -```bash -pip install datasets -python - <<'PY' -from datasets import load_dataset -rows = load_dataset("TokenRhythm/Claw-SWE-Bench", "lite", split="test") -print(len(rows)) -PY -``` +- uncached input tokens; +- cached input tokens; +- output tokens; +- reasoning tokens; +- total tokens. -The benchmark runner is intentionally not bundled with provider credentials. Connect your agent's final verifier result and usage counters to the JSONL schema above, then run `marginal public-eval`. +The current public comparator consumes the total token field. Decision Ledger v2 and +`TokenUsage` preserve the richer breakdown for engine-specific runners and future reports. ## Interpretation -A publishable MARGINAL claim must report both axes together, for example: +A publishable claim must report cost and quality together, for example: -> 38.4% fewer tokens (95% CI 34.1–42.7%) with a -0.4 percentage-point resolve-rate delta on Claw-SWE-Bench Lite-80. +> 38.4% fewer tokens with a -0.4 percentage-point resolve-rate delta under the preregistered evaluation protocol. -A token reduction without preserved verified outcomes is not considered a successful result. +A token reduction without preserved verified outcomes is not considered a successful +MARGINAL result. Policy replay is not a substitute for paired execution: replay cannot +simulate state changes or outcomes from actions that another policy would have skipped. diff --git a/docs/quickstart.md b/docs/quickstart.md index 7e06229..8cfb809 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -3,148 +3,73 @@ ## Install ```bash -pip install "marginal-ai @ git+https://github.com/SignalLayerLabs/Marginal.git@v0.1.0" +python -m pip install -e ".[dev]" ``` -MARGINAL supports Python 3.10–3.13 and has no mandatory runtime dependencies. - -## Guard one action +## Shadow first ```python from marginal import ( Action, - ActionDenied, BudgetLimits, Cost, - MarginalPolicy, - PolicyConfig, + DecisionLedgerContext, + JsonlDecisionLedger, Treasury, budgeted_call, - funded_call, -) - -policy = MarginalPolicy( - PolicyConfig( - outcome_value_usd=10.0, - token_shadow_price_per_million_usd=12.0, - minimum_roi=1.25, - target_success_probability=0.95, - ) + build_policy, + generate_local_identifier, ) -treasury = Treasury( - BudgetLimits( - max_tokens=25_000, - max_usd=1.00, - verification_reserve_tokens=3_000, +ledger = JsonlDecisionLedger( + "ledger.jsonl", + context=DecisionLedgerContext( + run_id=generate_local_identifier("run"), + task_id=generate_local_identifier("task"), + engine="generic", ), - policy=policy, + privacy_profile="safe_telemetry", + privacy_key_path=".marginal/privacy.key", ) -try: - result = budgeted_call( - treasury, - expensive_operation, - "authentication", - action=Action( - name="inspect documentation", - kind="research", - cost=Cost(tokens=2_000, usd=0.02), - expected_gain=0.10, - ), - ) -except ActionDenied as exc: - result = None - print(exc.decision.reason) -``` - -A denial happens before `expensive_operation` is called. An approval reserves the estimate -until it is committed or aborted. - -## Choose among candidates - -```python -allocation = treasury.fund_best( - [ - Action( - name="ask another model", - kind="review", - cost=Cost(tokens=5_000, usd=0.08), - expected_gain=0.03, - ), - Action( - name="run tests", - kind="verification", - cost=Cost(tokens=500, usd=0.001), - expected_gain=0.16, - is_verification=True, - ), - ] -) - -if allocation is not None: - result = funded_call(treasury, allocation, execute, allocation.action) -``` - -## Configure economic assumptions - -```python -from marginal import MarginalPolicy, PolicyConfig - -policy = MarginalPolicy( - PolicyConfig( - outcome_value_usd=10.0, - token_shadow_price_per_million_usd=12.0, - latency_shadow_price_per_second_usd=0.002, - risk_shadow_price_usd=2.0, - minimum_roi=1.25, - minimum_expected_gain=0.01, - target_success_probability=0.95, - ) +treasury = Treasury( + BudgetLimits(max_tokens=20_000, verification_reserve_tokens=2_000), + policy=build_policy("quality-first"), + trace_sink=ledger, + mode="shadow", ) -``` - -`outcome_value_usd` is the application-defined value of moving a task from zero to certain -success. Shadow prices express the opportunity cost of scarce tokens, latency, and risk. -`Cost.usd` remains the direct estimated or measured spend used by hard USD budgets. -## Record actual provider usage - -```python -from marginal import extract_common_llm_usage - -response = budgeted_call( +result = budgeted_call( treasury, - client.responses.create, - action=action, - usage_extractor=extract_common_llm_usage, - model="YOUR_MODEL", - input="Analyze the evidence.", + lambda: "done", + action=Action( + name="draft answer", + kind="generation", + cost=Cost(tokens=2_000), + expected_gain=0.10, + ), ) ``` -A custom extractor uses this contract: +Inspect the evidence: -```python -def extract_usage(result, estimated_cost): - return Cost( - tokens=result.usage.total_tokens, - usd=estimated_cost.usd, - latency_ms=estimated_cost.latency_ms, - risk=estimated_cost.risk, - ) +```bash +marginal ledger-validate ledger.jsonl +marginal ledger-report ledger.jsonl +marginal replay ledger.jsonl --profile balanced +marginal ledger-export ledger.jsonl aggregate.jsonl --privacy-profile aggregate_export \ + --minimum-group-size 5 ``` -## Persist evidence +`safe_telemetry` excludes free text and pseudonymizes identifiers. Use `local_full` only for a +trusted operational ledger. Use `aggregate_export` when preparing grouped data for sharing; groups +smaller than five records are suppressed by default. +Pseudonymization is not anonymization; read [`privacy.md`](privacy.md) before export. -```python -from marginal import JsonlTraceSink +Move to `recommend` when recommendations are surfaced to a user or agent. Move to `enforce` only after representative validation shows acceptable quality. -trace = JsonlTraceSink("run.jsonl") -treasury = Treasury(BudgetLimits(max_tokens=25_000), trace_sink=trace) -``` +## Engine adapters -```bash -marginal validate run.jsonl -marginal report run.jsonl -``` +Use `UniversalRuntime` when integrating a development agent. Enforce Mode requires an adapter that declares real action-blocking capability. The reference runtime currently maps core decisions to allow or deny; other protocol directives are extension points. + +See [`universal-runtime.md`](universal-runtime.md) and the executable examples in [`examples`](../examples). diff --git a/docs/superpowers/plans/2026-08-06-learning-loop-foundation.md b/docs/superpowers/plans/2026-08-06-learning-loop-foundation.md new file mode 100644 index 0000000..3265abe --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-learning-loop-foundation.md @@ -0,0 +1,186 @@ +# MARGINAL v0.2 Learning Loop Foundation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. + +**Goal:** Build a dependency-free, versioned learning-loop foundation with non-blocking shadow evaluation, decision evidence, outcome recording, policy replay, and a universal adapter runtime. + +**Architecture:** Preserve the v0.1 core contracts and extend them through focused modules. `Treasury` remains the transactional authority, `JsonlDecisionLedger` enriches trace events, `UniversalRuntime` translates protocol actions into core actions, and replay consumes the same ledger format without claiming causal effects. + +**Tech Stack:** Python 3.10-3.13, standard library only at runtime, dataclasses, JSONL, pytest, Ruff, mypy strict, setuptools. + +## Global Constraints + +- Keep zero mandatory runtime dependencies. +- Preserve v0.1 public APIs unless a new optional field or method is additive. +- Shadow and recommend modes must not prevent execution. +- Enforce mode must preserve transactional reservation and overrun behavior. +- Never infer causal action value from a task outcome alone. +- Do not record prompts or outputs by default. +- All public identities and schemas must be explicitly versioned. +- All documentation must describe implemented behavior and limitations consistently. + +--- + +## Execution status + +Tasks 1–9 have been implemented and covered by the repository test suite. Task 10 has passed all verification available in the isolated environment. Ruff, mypy strict, and Twine remain explicit Visual Studio/CI gates because their executables could not be installed without network access. + +### Task 1: Versioned decision and token primitives + +**Files:** +- Modify: `src/marginal/models.py` +- Create: `src/marginal/modes.py` +- Test: `tests/test_models_v2.py` + +**Produces:** `TokenUsage`, enriched `Decision`, and `ExecutionMode`. + +- [x] Write failing validation and compatibility tests. +- [x] Run the focused tests and confirm missing imports/fields fail. +- [x] Implement immutable validated primitives with backward-compatible defaults. +- [x] Run focused and existing model tests. + +### Task 2: Versioned estimator and registry + +**Files:** +- Modify: `src/marginal/estimator.py` +- Create: `src/marginal/registry.py` +- Test: `tests/test_estimator_v2.py` + +**Produces:** `EstimatorIdentity`, `ValueEstimate`, enhanced `ValueEstimator`, `EstimatorRegistry`. + +- [x] Write failing tests for explicit, historical, contextual, uncertainty, identity, and registry behavior. +- [x] Confirm RED failures. +- [x] Implement transparent estimates and deterministic hashes. +- [x] Run focused and legacy policy tests. + +### Task 3: Versioned policy identities and profiles + +**Files:** +- Modify: `src/marginal/policy.py` +- Create: `src/marginal/profiles.py` +- Test: `tests/test_policy_v2.py` + +**Produces:** `PolicyIdentity`, structured reason codes, reference profiles. + +- [x] Write failing tests for identity stability, estimator metadata, reason codes, and profiles. +- [x] Confirm RED failures. +- [x] Implement additive policy behavior. +- [x] Run focused and legacy policy tests. + +### Task 4: Shadow-safe transactional accounting + +**Files:** +- Modify: `src/marginal/budget.py` +- Modify: `src/marginal/treasury.py` +- Test: `tests/test_shadow_mode.py` + +**Produces:** non-blocking shadow/recommend authorization, unchecked reservations for observation, explicit outcome/value hooks. + +- [x] Write failing tests for policy denial override, hard-budget override, pending accounting, overrun measurement, enforce preservation, and learning hooks. +- [x] Confirm RED failures. +- [x] Implement mode-aware authorization and settlement. +- [x] Run focused and legacy treasury tests. + +### Task 5: Failed-action usage settlement + +**Files:** +- Modify: `src/marginal/adapters.py` +- Test: `tests/test_failure_settlement.py` + +**Produces:** optional failure usage extraction and primary-exception preservation. + +- [x] Write failing sync and async tests. +- [x] Confirm RED failures. +- [x] Implement failure settlement without replacing the original callable error. +- [x] Run focused and legacy adapter tests. + +### Task 6: Outcome contract and decision ledger v2 + +**Files:** +- Create: `src/marginal/outcomes.py` +- Create: `src/marginal/ledger.py` +- Modify: `src/marginal/trace.py` +- Test: `tests/test_decision_ledger.py` + +**Produces:** `Outcome`, `DecisionLedgerContext`, `JsonlDecisionLedger`, ledger readers and summaries. + +- [x] Write failing schema, sequence, validation, outcome, and privacy tests. +- [x] Confirm RED failures. +- [x] Implement append-only thread-safe evidence. +- [x] Run focused trace and ledger tests. + +### Task 7: Universal Agent Protocol and local runtime + +**Files:** +- Create: `src/marginal/protocol.py` +- Create: `src/marginal/runtime.py` +- Test: `tests/test_protocol.py` +- Test: `tests/test_runtime.py` + +**Produces:** protocol values, capability negotiation, and an engine-neutral lifecycle runtime. + +- [x] Write failing round-trip, fingerprint-scope, lifecycle, failure, and outcome tests. +- [x] Confirm RED failures. +- [x] Implement protocol serialization and runtime mapping. +- [x] Run focused tests. + +### Task 8: Policy replay and CLI + +**Files:** +- Create: `src/marginal/replay.py` +- Modify: `src/marginal/cli.py` +- Test: `tests/test_replay.py` +- Test: `tests/test_cli_v2.py` + +**Produces:** replay summaries, Markdown rendering, ledger validate/report and replay commands. + +- [x] Write failing replay and CLI tests. +- [x] Confirm RED failures. +- [x] Implement replay with explicit non-causal language. +- [x] Run focused and legacy CLI tests. + +### Task 9: Public API, schemas, examples, and complete documentation alignment + +**Files:** +- Modify: `src/marginal/__init__.py` +- Modify: `pyproject.toml` +- Modify: `README.md` +- Modify: `CHANGELOG.md` +- Modify: `CONTRIBUTING.md` +- Modify: `SECURITY.md` +- Modify: `docs/api.md` +- Modify: `docs/architecture.md` +- Modify: `docs/concepts.md` +- Modify: `docs/integrations.md` +- Modify: `docs/benchmarking.md` +- Modify: `docs/quickstart.md` +- Modify: `docs/faq.md` +- Create: `docs/learning-loop.md` +- Create: `docs/universal-runtime.md` +- Create: `ROADMAP.md` +- Create: `schemas/agent-event-v1.json` +- Create: `schemas/agent-decision-v1.json` +- Create: `schemas/decision-ledger-v2.json` +- Create: `schemas/outcome-v1.json` +- Create: `examples/shadow_mode.py` +- Create: `examples/universal_runtime.py` +- Test: `tests/test_public_api_v2.py` + +**Produces:** one consistent v0.2 product surface and documentation set. + +- [x] Write failing public-export, version, schema, and documentation consistency tests. +- [x] Confirm RED failures. +- [x] Update every public reference and example together. +- [x] Run focused documentation consistency tests. + +### Task 10: Full verification and delivery + +**Files:** all changed files plus delivery prompt and manifest. + +- [ ] Run Ruff format and lint. +- [ ] Run mypy strict. +- [x] Run the complete pytest suite. +- [x] Build wheel and sdist. +- [ ] Run twine checks. +- [x] Run security-oriented scans for accidental secrets, prompt logging, TODOs, and inconsistent versions. +- [x] Generate a clean repository-overlay ZIP, SHA-256 manifest, change summary, and Visual Studio commit/push prompt. diff --git a/docs/superpowers/plans/2026-08-06-privacy-profiles.md b/docs/superpowers/plans/2026-08-06-privacy-profiles.md new file mode 100644 index 0000000..b9eaf44 --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-privacy-profiles.md @@ -0,0 +1,123 @@ +# Privacy Profiles Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. + +**Goal:** Add explicit privacy profiles that protect quasi-identifiers and free-text fields in Decision Ledger telemetry, while providing a separate aggregate-only export suitable for sharing. + +**Architecture:** Introduce a provider-neutral privacy module with field classifications, keyed local pseudonymization, strict safe-telemetry sanitization, and aggregate record generation. Integrate profiles at the Decision Ledger boundary so all Treasury events are protected consistently. Keep operational local ledgers and aggregate exports separate, expose both through the public API and CLI, and package versioned JSON schemas. + +**Tech Stack:** Python 3.10+, standard library only (`enum`, `dataclasses`, `hashlib`, `hmac`, `secrets`, `json`, `pathlib`), pytest, existing JSON Schema test suite. + +## Global Constraints + +- Preserve zero mandatory runtime dependencies. +- Default behavior remains backward-compatible `local_full`. +- `safe_telemetry` must remove free text and metadata, pseudonymize identifiers with HMAC-SHA-256, and generalize exact timestamps. +- Pseudonymization keys are local, never written into ledger records, and created with restrictive permissions when generated automatically. +- `aggregate_export` must not be usable as an operational Decision Ledger profile; it is a separate export path with grouped generalized rows, no identifiers or timestamps, and default suppression for groups smaller than five records. +- Public schemas, CLI help, API docs, security documentation, roadmap, changelog, examples, and README must agree with runtime behavior. +- No claim that pseudonymization is anonymization. + +--- + +## Execution status + +Tasks 1–5 have been implemented with test-first coverage. Task 6 is complete for all checks available in the isolated build environment; Ruff, mypy, and Twine remain mandatory pre-commit/CI gates because their executables could not be installed without network access. + +### Task 1: Define privacy contracts and sanitization + +**Files:** +- Create: `src/marginal/privacy.py` +- Create: `tests/test_privacy.py` + +**Interfaces:** +- Produces: `PrivacyProfile`, `PrivacyClass`, `PrivacyConfig`, `LocalPseudonymizer`, `sanitize_ledger_record`, `aggregate_ledger_records`, `load_or_create_privacy_key`. + +- [x] Write failing tests for profile parsing, deterministic keyed pseudonymization, key separation, strict sensitive-field removal, timestamp generalization, and aggregate grouping. +- [x] Run `python -m pytest tests/test_privacy.py -q` and verify failures are caused by the missing module/API. +- [x] Implement the minimal privacy module. +- [x] Run the privacy tests and refactor while green. + +### Task 2: Integrate privacy at the Decision Ledger boundary + +**Files:** +- Modify: `src/marginal/ledger.py` +- Modify: `tests/test_decision_ledger.py` + +**Interfaces:** +- `JsonlDecisionLedger(..., privacy_profile=..., privacy_key=..., privacy_key_path=...)` +- Every record includes `privacy_profile`. +- `safe_telemetry` applies before JSON serialization. + +- [x] Write failing ledger tests for local-full preservation, safe telemetry sanitization, consistent context/outcome pseudonyms, key-file creation, reserved-field protection, and aggregate-profile rejection. +- [x] Run focused tests and verify RED. +- [x] Implement profile integration and reader validation. +- [x] Run focused tests and full regression tests. + +### Task 3: Add aggregate export API and CLI + +**Files:** +- Modify: `src/marginal/ledger.py` +- Modify: `src/marginal/cli.py` +- Modify: `src/marginal/__init__.py` +- Create or modify: `tests/test_cli_v2.py`, `tests/test_public_api_v2.py` + +**Interfaces:** +- `export_decision_ledger(source, destination, *, privacy_profile, privacy_key=None, privacy_key_path=None)` +- CLI: `marginal ledger-export SOURCE DESTINATION --privacy-profile safe_telemetry|aggregate_export [--privacy-key-file PATH] [--minimum-group-size N]` + +- [x] Write failing API and CLI tests. +- [x] Verify RED. +- [x] Implement export and CLI behavior with overwrite protection. +- [x] Verify GREEN and regression compatibility. + +### Task 4: Publish schemas and package resources + +**Files:** +- Modify: `schemas/decision-ledger-v2.json` +- Modify: `src/marginal/schemas/decision-ledger-v2.json` +- Create: `schemas/aggregate-export-v1.json` +- Create: `src/marginal/schemas/aggregate-export-v1.json` +- Modify: schema tests. + +**Interfaces:** +- Decision ledger schema documents `privacy_profile`. +- Aggregate export schema validates grouped generalized records. + +- [x] Write failing packaged-schema and conformance tests. +- [x] Verify RED. +- [x] Add synchronized schemas. +- [x] Verify package API and JSON Schema validation. + +### Task 5: Align the full project ecosystem + +**Files:** +- Modify: `README.md`, `SECURITY.md`, `CHANGELOG.md`, `ROADMAP.md` +- Modify: `docs/api.md`, `docs/concepts.md`, `docs/learning-loop.md`, `docs/universal-runtime.md`, `docs/architecture.md`, `docs/quickstart.md`, `docs/faq.md`, `docs/index.md` +- Create: `docs/privacy.md` +- Create: `examples/privacy_profiles.py` +- Modify: repository consistency tests. + +**Interfaces:** +- One consistent description of field classes and three profiles. +- Clear statement that pseudonymization does not equal anonymization. +- Operational ledger and aggregate export are explicitly separate. + +- [x] Add failing consistency assertions for privacy documentation, public API references, and schema presence. +- [x] Verify RED. +- [x] Update all documentation and examples. +- [x] Verify GREEN and scan for contradictory privacy claims. + +### Task 6: Final verification and delivery + +**Files:** +- Verify entire repository. +- Create clean ZIP, SHA-256 file, verification report, and Visual Studio commit/push prompt. + +- [x] Run pytest, compileall, lint/type/build tools when available, wheel/sdist build, clean-venv install, packaged schema checks, examples, CLI smoke tests, link checks, secret scans, and archive integrity checks. + + Local note: pytest, compileall, build, clean-wheel install, schema, example, CLI, link, secret, and archive checks passed. Ruff, mypy, and Twine are still required in Visual Studio/CI because those executables were unavailable in the offline container. +- [x] Remove caches, build products, temporary ledgers, and local key files from the deliverable. +- [x] Generate `VERIFICATION_REPORT.md` with exact commands and results. +- [x] Generate `VISUAL_STUDIO_COMMIT_PROMPT.md` with review, test, commit, and push instructions. +- [x] Create a single clean ZIP and SHA-256 checksum. diff --git a/docs/superpowers/specs/2026-08-06-learning-loop-foundation-design.md b/docs/superpowers/specs/2026-08-06-learning-loop-foundation-design.md new file mode 100644 index 0000000..08299b4 --- /dev/null +++ b/docs/superpowers/specs/2026-08-06-learning-loop-foundation-design.md @@ -0,0 +1,77 @@ +# MARGINAL v0.2 Learning Loop Foundation Design + +## Goal + +Transform MARGINAL from a static marginal-value policy engine into a versioned, observable learning-loop foundation while preserving its deterministic, dependency-free core and backward-compatible v0.1 execution APIs. + +## Product boundary + +This release implements the universal foundation used by future Codex, Claude Code, GitHub Copilot, OpenCode, and other adapters. It does not claim that vendor-specific adapters or causal value estimation are complete. It creates the runtime, protocol, evidence, shadow evaluation, replay, and estimator interfaces required to build and validate those integrations honestly. + +## Architecture + +The existing `Action -> Policy -> Treasury -> Trace` flow remains intact. New modules add: + +- `ExecutionMode` for shadow, recommend, and enforce behavior; +- `TokenUsage` for measured token breakdowns while `Cost.tokens` remains backward compatible; +- enriched `Decision` fields separating recommendations from applied behavior; +- a versioned `ValueEstimator` and `EstimatorRegistry`; +- `Outcome` as a provider-neutral verifier result; +- `JsonlDecisionLedger` as a schema-versioned trace sink; +- `UniversalRuntime` and protocol values for thin engine adapters; +- policy replay over ledger evidence without making causal claims; +- explicit failed-action settlement when external work consumed resources. + +## Mode semantics + +- **enforce:** policy and hard-budget decisions control execution; overruns remain errors. +- **shadow:** every proposed action executes, while the would-allow/would-deny recommendation is recorded. Budget violations are measured but do not change the caller's behavior. +- **recommend:** identical non-blocking accounting semantics to shadow mode, but intended for integrations that surface recommendations to the user or agent. + +Candidate selection through `fund_best` remains an active allocation API. Shadow mode applies to guarded proposed actions, not to inventing a baseline candidate choice when no external choice exists. + +## Decision ledger + +Every ledger event receives: + +- schema version; +- event ID and monotonic sequence; +- timestamp; +- run, task, trajectory, engine, and model identity; +- execution mode; +- policy and estimator identity; +- normalized action, decision, usage, and outcome payloads. + +The ledger is append-only JSONL, local-first, thread-safe, and does not record prompts or model outputs unless callers explicitly place data in metadata. + +## Estimation and learning + +The estimator remains transparent. Explicit action estimates take priority. Historical observations are keyed by action kind and selected context fields, return uncertainty/confidence metadata, and expose a stable identity containing name, semantic version, configuration hash, and optional training-data fingerprint. + +`Treasury.observe_value(action, realized_gain)` is the explicit learning hook. Outcomes and action-level realized gains are deliberately separate because a successful task does not prove that every preceding action caused the success. + +## Replay + +Replay re-evaluates historical authorization events under a selected policy and reports counterfactual policy decisions and estimated selected/avoided cost. It is an off-policy diagnostic, not causal proof and not a task-quality simulation. + +## Compatibility + +Existing constructors and methods continue to work. New dataclass fields have defaults. Existing `JsonlTraceSink`, wrappers, demos, public evaluator, and CLI commands remain supported. The package version advances to `0.2.0`. + +## Security and privacy + +No mandatory network service is introduced. Ledger paths are caller-controlled. Metadata remains caller-controlled and must be treated as potentially sensitive. Fingerprints are identifiers, not secrets. Failure accounting preserves the original execution exception as the primary error. + +## Validation + +The release requires: + +- backward-compatibility tests for v0.1 policy, treasury, and adapters; +- red-green tests for every new public primitive; +- shadow-mode no-block behavior and measured overrun tests; +- ledger schema, sequence, and privacy tests; +- protocol round-trip and runtime lifecycle tests; +- estimator versioning, contextual observations, and registry tests; +- failure settlement tests; +- replay tests with explicit non-causal labeling; +- Ruff, mypy strict, pytest, build, and twine validation. diff --git a/docs/superpowers/specs/2026-08-06-privacy-profiles-design.md b/docs/superpowers/specs/2026-08-06-privacy-profiles-design.md new file mode 100644 index 0000000..109693f --- /dev/null +++ b/docs/superpowers/specs/2026-08-06-privacy-profiles-design.md @@ -0,0 +1,130 @@ +# Privacy Profiles Design + +## Objective + +Protect MARGINAL Decision Ledger evidence from quasi-identifier and free-text disclosure +without weakening the local operational record or adding a mandatory runtime dependency. + +The privacy boundary is the Decision Ledger serializer and export path. It does not claim to +sanitize arbitrary application logs, provider SDK traces, or caller-owned files. + +## Threat model + +A record can disclose sensitive context without containing prompts or model outputs. Examples +include customer-derived task IDs, repository names, action descriptions, model identities, +verifier details, tool arguments, exception text, and exact timestamps. + +The design assumes: + +- the host process and local filesystem account are trusted; +- pseudonymization keys remain local and separate from exported datasets; +- exported files may be shared with parties that must not receive the source identifiers; +- unknown fields are sensitive until reviewed explicitly; +- pseudonymization reduces direct disclosure but does not provide anonymity. + +## Field classification + +Every supported field belongs to one of three classes: + +- **Safe by default:** structured economic, decision, outcome, and version fields; +- **Pseudonymous:** correlation identifiers transformed with a local keyed construction; +- **Potentially sensitive:** free text, arbitrary metadata, complete model identity, verifier + details, tool arguments, error text, and other caller-defined content. + +`FIELD_CLASSIFICATION` is immutable. `classify_field(...)` inherits classifications from +reviewed parent paths and treats every unknown path as potentially sensitive. + +## Privacy profiles + +### LOCAL_FULL + +Preserves the complete operational record. It is the backward-compatible default and may +contain sensitive caller data. New files use owner-only permissions on POSIX systems. + +### SAFE_TELEMETRY + +Uses an explicit allowlist. It: + +- removes free-form and arbitrary metadata fields; +- pseudonymizes event, run, task, trajectory, action, fingerprint, state, and engine-instance + identifiers with field-separated HMAC-SHA-256; +- generalizes timestamps to UTC day boundaries; +- normalizes engine, event, action-kind, mode, directive, and reason-code labels; +- retains only allowlisted finite numeric fields and validates their ranges; +- retains only version-like policy and estimator identities; +- validates every record on read against the same canonical transformation; +- publishes a recursively strict JSON Schema with no unknown fields. + +Pseudonyms use 128 bits of the HMAC digest and are stable only for the same key and field +name. Different keys produce unlinkable export domains. + +### AGGREGATE_EXPORT + +Is not an operational ledger mode. It groups decisions and outcomes into generalized +categories and emits no identifiers, timestamps, free text, metadata, model identity, or +verifier details. The output remains vulnerable to inference from rare groups, so consumers +must apply organizational minimum-group and retention rules before publication. + +## Key and filesystem handling + +Generated keys contain 256 random bits. On supported POSIX filesystems they are created with +mode `0600`. Existing key files must be regular, non-symlink files and must not be accessible +by group or other users. Existing keys are read from a validated descriptor to prevent path +replacement between validation and read. + +Decision Ledger append targets reject symbolic links, non-regular files, and permissive POSIX +modes. Export destinations are created exclusively and are never overwritten, including if a +competing process creates the destination after an earlier existence check. + +## Data flow + +```text +Treasury event + │ + ▼ +Decision Ledger envelope + │ + ├─ LOCAL_FULL ───────► complete local JSONL + │ + └─ SAFE_TELEMETRY ───► allowlist → pseudonymize → generalize → validate → JSONL + +Existing operational ledger + │ + └─ export_decision_ledger + ├─ SAFE_TELEMETRY ─► unlinkable event-level export + └─ AGGREGATE_EXPORT ► grouped shareable rows +``` + +## Public contracts + +- `PrivacyProfile` +- `PrivacyClass` +- `PrivacyConfig` +- `LocalPseudonymizer` +- `FIELD_CLASSIFICATION` +- `classify_field(...)` +- `generate_local_identifier(...)` +- `load_or_create_privacy_key(...)` +- `sanitize_ledger_record(...)` +- `validate_safe_telemetry_record(...)` +- `aggregate_ledger_records(...)` +- `export_decision_ledger(...)` +- `safe-telemetry-v1.json` +- `aggregate-export-v1.json` + +## Non-goals + +The profiles do not provide encryption at rest, differential privacy, k-anonymity, +cryptographic tamper evidence, distributed locking, regulatory certification, or protection +against a party that possesses both the source values and pseudonymization key. + +## Acceptance criteria + +- Sensitive fixture strings never appear in strict or aggregate exports. +- Pseudonyms are deterministic per field/key and unlinkable across keys. +- Unknown fields are removed from strict telemetry and rejected on read. +- Strict records conform to the packaged schema. +- Aggregate rows conform to their packaged schema and contain no identifiers. +- Key, ledger, and export filesystem hardening has regression tests. +- Public API, CLI, README, security guide, privacy guide, roadmap, and changelog describe the + same implemented behavior. diff --git a/docs/universal-runtime.md b/docs/universal-runtime.md new file mode 100644 index 0000000..4723a7f --- /dev/null +++ b/docs/universal-runtime.md @@ -0,0 +1,71 @@ +# Universal Agent Runtime + +MARGINAL is one product with one decision core. Engine integrations are thin adapters over Universal Agent Protocol v1. + +## Adapter responsibilities + +1. declare engine capabilities; +2. translate a native proposed action into `AgentAction`; +3. call `UniversalRuntime.before_action`; +4. apply or surface `AgentDecision` according to mode and capability; +5. execute the native action when applied; +6. call `after_action` with actual cost, or `fail_action` with measured cost when available; +7. record verifier outcomes with the matching task ID; +8. classify identifiers and free text before persistence; +9. use `SAFE_TELEMETRY` or `AGGREGATE_EXPORT` before data leaves a trusted boundary. + +The runtime adds engine, session, and task identity to the core action metadata. Action IDs remain pending until successful settlement, measured failure settlement, or abort. + +The runtime does not itself sanitize adapter inputs. Privacy is enforced at the Decision Ledger +boundary. `SAFE_TELEMETRY` removes action names, model identity, metadata, verifier details, and +error text while pseudonymizing correlation identifiers. An adapter should still minimize +sensitive data before it reaches any log outside MARGINAL. + +## Capability levels + +- `observe`: telemetry but no action control; +- `control`: at least one blocking, modification, stop, or model-turn control surface; +- `full`: all currently modeled capabilities. + +Capability level is derived from booleans and validated during dictionary parsing. Labels describe technical control, not benchmark quality. + +Enforce Mode requires `block_actions=True`; construction fails otherwise. Shadow and Recommend modes support observe-only adapters. + +## Directives + +Protocol v1 defines: + +```text +allow · deny · modify · defer · reuse · stop · force_verify +``` + +The reference runtime converts current core decisions to allow or deny. Replacement payloads and richer directives are stable protocol extension points, not automatic v0.2 policy behavior. + +## Deduplication scopes + +- `exact`: same semantic action payload; +- `once_per_state`: reruns are valid after workspace state changes; +- `once_per_phase`: one execution in a named task phase; +- `allow_retry`: retry number participates in identity. + +Protocol fingerprint metadata must be JSON serializable. Non-blocking modes maintain separate internal reservations for concurrent semantic duplicates, preserving Shadow Mode behavior and complete accounting. + +## Settlement safety + +`after_action` and `fail_action` validate actual cost before removing the runtime action ID. Invalid settlement data therefore does not orphan a Treasury reservation. + +Failed work with measured cost is settled but not marked as successfully completed, so a retry may be authorized and charged. + +## Published schemas + +- `schemas/agent-event-v1.json`; +- `schemas/agent-decision-v1.json`; +- `schemas/agent-capabilities-v1.json`; +- `schemas/token-usage-v2.json`; +- `schemas/outcome-v1.json`; +- `schemas/decision-ledger-v2.json`; +- `schemas/aggregate-export-v1.json`. + +## Vendor adapters + +Codex, OpenCode, Claude Code, and GitHub Copilot adapters are separate milestones. This release provides their shared contract and local runtime but does not claim those vendor integrations are complete. diff --git a/examples/privacy_profiles.py b/examples/privacy_profiles.py new file mode 100644 index 0000000..ba6f5c6 --- /dev/null +++ b/examples/privacy_profiles.py @@ -0,0 +1,68 @@ +"""Create a strict local ledger and a shareable aggregate export.""" + +from pathlib import Path + +from marginal import ( + Action, + BudgetLimits, + Cost, + DecisionLedgerContext, + JsonlDecisionLedger, + Outcome, + Treasury, + export_decision_ledger, +) + +output = Path("privacy-example") +output.mkdir(exist_ok=True) + +ledger_path = output / "safe-ledger.jsonl" +ledger = JsonlDecisionLedger( + ledger_path, + context=DecisionLedgerContext( + run_id="customer-acme-contract-2026", + task_id="customer-acme-contract-2026", + engine="codex", + model="internal-legal-model", + ), + privacy_profile="safe_telemetry", + privacy_key_path=output / "privacy.key", +) + +treasury = Treasury( + BudgetLimits(max_tokens=10_000), + trace_sink=ledger, + mode="shadow", +) +for index in range(5): + action = Action( + name=f"review termination clause {index}", + kind="verification", + cost=Cost(tokens=1_200, usd=0.01, latency_ms=200), + expected_gain=0.2, + is_verification=True, + metadata={ + "repository": "secret-merger-project", + "document_index": index, + }, + ) + treasury.authorize(action) + treasury.commit(action) +treasury.record_outcome( + Outcome( + task_id="customer-acme-contract-2026", + reward=1.0, + resolved=True, + verifier="internal legal verifier", + evidence={"repository": "secret-merger-project"}, + ) +) + +export_decision_ledger( + ledger_path, + output / "aggregate.jsonl", + privacy_profile="aggregate_export", + minimum_group_size=5, +) +print(f"Safe operational ledger: {ledger_path}") +print(f"Aggregate shareable export: {output / 'aggregate.jsonl'}") diff --git a/examples/public_eval/baseline.jsonl b/examples/public_eval/baseline.jsonl deleted file mode 100644 index a3f9af0..0000000 --- a/examples/public_eval/baseline.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"instance_id":"public-task-1","resolved":true,"tokens":12000,"usd":0.18,"latency_ms":42000,"tool_calls":12} -{"instance_id":"public-task-2","resolved":false,"tokens":18000,"usd":0.27,"latency_ms":61000,"tool_calls":18} diff --git a/examples/public_eval/marginal.jsonl b/examples/public_eval/marginal.jsonl deleted file mode 100644 index 7d54bbd..0000000 --- a/examples/public_eval/marginal.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"instance_id":"public-task-1","resolved":true,"tokens":7200,"usd":0.11,"latency_ms":31000,"tool_calls":8} -{"instance_id":"public-task-2","resolved":false,"tokens":9000,"usd":0.14,"latency_ms":39000,"tool_calls":10} diff --git a/examples/shadow_mode.py b/examples/shadow_mode.py new file mode 100644 index 0000000..76545e3 --- /dev/null +++ b/examples/shadow_mode.py @@ -0,0 +1,41 @@ +"""Observe recommendations without changing application behavior.""" + +from marginal import ( + Action, + BudgetLimits, + Cost, + DecisionLedgerContext, + JsonlDecisionLedger, + Outcome, + Treasury, + budgeted_call, + build_policy, +) + +ledger = JsonlDecisionLedger( + "shadow-ledger.jsonl", + context=DecisionLedgerContext(run_id="example-run", task_id="example-task"), +) +treasury = Treasury( + BudgetLimits(max_tokens=1_000, verification_reserve_tokens=100), + policy=build_policy("quality-first"), + trace_sink=ledger, + mode="shadow", +) + +result = budgeted_call( + treasury, + lambda: "executed even if the policy recommends deny", + action=Action( + name="optional reviewer", + kind="review", + cost=Cost(tokens=2_000), + expected_gain=0.01, + ), +) +print(result) + +treasury.record_outcome( + Outcome(task_id="example-task", reward=1.0, resolved=True, verifier="example") +) +print(treasury.summary()) diff --git a/examples/universal_runtime.py b/examples/universal_runtime.py new file mode 100644 index 0000000..da95650 --- /dev/null +++ b/examples/universal_runtime.py @@ -0,0 +1,36 @@ +"""Minimal engine-neutral adapter lifecycle.""" + +from marginal import ( + AgentAction, + AgentCapabilities, + BudgetLimits, + Cost, + Treasury, + UniversalRuntime, + build_policy, +) + +treasury = Treasury(BudgetLimits(max_tokens=10_000), policy=build_policy("balanced"), mode="shadow") +runtime = UniversalRuntime( + treasury, + engine="example-engine", + session_id="session-1", + task_id="task-1", + capabilities=AgentCapabilities(block_actions=True, record_outcomes=True), +) + +action = AgentAction( + action_id="read-1", + name="read a large file", + kind="file_read", + estimated_cost=Cost(tokens=4_000), + expected_gain=0.04, + state_hash="workspace-v1", + phase="diagnose", + deduplication_scope="once_per_state", +) + +decision = runtime.before_action(action) +print(decision.to_dict()) +if decision.allowed: + runtime.after_action(action.action_id, actual_cost=Cost(tokens=3_600)) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index f17f46c..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1314 +0,0 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. - -[[package]] -name = "ast-serialize" -version = "0.6.0" -description = "Python bindings for mypy AST serialization" -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24"}, - {file = "ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14"}, - {file = "ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596"}, - {file = "ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad"}, - {file = "ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a"}, - {file = "ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081"}, - {file = "ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77"}, - {file = "ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790"}, - {file = "ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7"}, - {file = "ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b"}, - {file = "ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b"}, - {file = "ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32"}, - {file = "ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b"}, - {file = "ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a"}, - {file = "ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e"}, - {file = "ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b"}, - {file = "ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1"}, - {file = "ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e"}, - {file = "ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e"}, - {file = "ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe"}, -] - -[[package]] -name = "backports-tarfile" -version = "1.2.0" -description = "Backport of CPython tarfile module" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" -files = [ - {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, - {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] - -[[package]] -name = "build" -version = "1.5.0" -description = "A simple, correct Python build frontend" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f"}, - {file = "build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "os_name == \"nt\""} -importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} -packaging = ">=24.0" -pyproject_hooks = "*" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -keyring = ["keyring"] -uv = ["uv (>=0.1.18)"] -virtualenv = ["virtualenv (>=20.17) ; python_version >= \"3.10\" and python_version < \"3.14\"", "virtualenv (>=20.31) ; python_version >= \"3.14\""] - -[[package]] -name = "certifi" -version = "2026.7.22" -description = "Python package for providing Mozilla's CA Bundle." -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, - {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, -] - -[[package]] -name = "cffi" -version = "2.1.1" -description = "Foreign Function Interface for Python calling C code." -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, - {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, - {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, - {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, - {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, - {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, - {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, - {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, - {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, - {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, - {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, - {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, - {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, - {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, - {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, - {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, - {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, - {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, - {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, - {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, - {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, - {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, - {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, - {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, - {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, - {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, - {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, - {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, - {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, - {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, - {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, - {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, - {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, - {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, - {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, - {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, - {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, - {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, - {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, - {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, - {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, - {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, - {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, - {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, - {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, - {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, - {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, - {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, - {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, - {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, - {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, - {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, - {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, - {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, - {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, - {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, - {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, - {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, - {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, - {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, - {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, - {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, - {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, - {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, - {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, - {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, - {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, - {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, - {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, - {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, - {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, - {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, - {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, - {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, - {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, - {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, - {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, - {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, - {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, - {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, - {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, - {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, - {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, - {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, - {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, - {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, - {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, - {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, - {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, - {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, - {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, - {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, - {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, - {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, - {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, - {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, - {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, - {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, - {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, - {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, -] - -[package.dependencies] -pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} - -[[package]] -name = "charset-normalizer" -version = "3.4.9" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"}, - {file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"}, - {file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"}, - {file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"}, - {file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"}, - {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"}, - {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"}, - {file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"}, - {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"}, - {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = true -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] -markers = "extra == \"dev\" and (os_name == \"nt\" or sys_platform == \"win32\")" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "cryptography" -version = "50.0.0" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = true -python-versions = "!=3.9.0,!=3.9.1,>=3.9" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" -files = [ - {file = "cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169"}, - {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f"}, - {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105"}, - {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef"}, - {file = "cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30"}, - {file = "cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d"}, - {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c"}, - {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c"}, - {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95"}, - {file = "cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269"}, - {file = "cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708"}, - {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47"}, - {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9"}, - {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7"}, - {file = "cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba"}, - {file = "cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c"}, - {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a"}, - {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e"}, - {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d"}, - {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437"}, - {file = "cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9"}, - {file = "cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} -typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} - -[package.extras] -ssh = ["bcrypt (>=3.1.5)"] - -[[package]] -name = "docutils" -version = "0.23" -description = "Docutils -- Python Documentation Utilities" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea"}, - {file = "docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\" and python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "id" -version = "1.6.1" -description = "A tool for generating OIDC identities" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca"}, - {file = "id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069"}, -] - -[package.dependencies] -urllib3 = ">=2,<3" - -[package.extras] -dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] -lint = ["bandit", "interrogate", "mypy", "ruff (<0.14.15)"] -test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] - -[[package]] -name = "idna" -version = "3.18" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, - {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, -] - -[package.extras] -all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "9.0.0" -description = "Read metadata from Python packages" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\" and (python_full_version < \"3.10.2\" or platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version < \"3.12\"" -files = [ - {file = "importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7"}, - {file = "importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "jaraco-classes" -version = "3.4.0" -description = "Utility functions for Python class constructs" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, - {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, -] - -[package.dependencies] -more-itertools = "*" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "jaraco-context" -version = "6.1.2" -description = "Useful decorators and context managers" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535"}, - {file = "jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3"}, -] - -[package.dependencies] -"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] -type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] - -[[package]] -name = "jaraco-functools" -version = "4.6.0" -description = "Functools like those found in stdlib" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30"}, - {file = "jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280"}, -] - -[package.dependencies] -more_itertools = "*" - -[package.extras] -check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] -type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] - -[[package]] -name = "jeepney" -version = "0.9.0" -description = "Low-level, pure Python DBus protocol wrapper." -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" -files = [ - {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, - {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, -] - -[package.extras] -test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] -trio = ["trio"] - -[[package]] -name = "keyring" -version = "25.7.0" -description = "Store and access your passwords safely." -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, - {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, -] - -[package.dependencies] -importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} -"jaraco.classes" = "*" -"jaraco.context" = "*" -"jaraco.functools" = "*" -jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} -pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} -SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -completion = ["shtab (>=1.1.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] -type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] - -[[package]] -name = "librt" -version = "0.13.0" -description = "Mypyc runtime library" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5"}, - {file = "librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547"}, - {file = "librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2"}, - {file = "librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929"}, - {file = "librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a"}, - {file = "librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac"}, - {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7"}, - {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40"}, - {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a"}, - {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde"}, - {file = "librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8"}, - {file = "librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc"}, - {file = "librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082"}, - {file = "librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14"}, - {file = "librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79"}, - {file = "librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176"}, - {file = "librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89"}, - {file = "librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f"}, - {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d"}, - {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd"}, - {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588"}, - {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1"}, - {file = "librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21"}, - {file = "librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b"}, - {file = "librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c"}, - {file = "librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0"}, - {file = "librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5"}, - {file = "librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9"}, - {file = "librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b"}, - {file = "librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03"}, - {file = "librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e"}, - {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd"}, - {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348"}, - {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7"}, - {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82"}, - {file = "librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3"}, - {file = "librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa"}, - {file = "librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1"}, - {file = "librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3"}, - {file = "librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c"}, - {file = "librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c"}, - {file = "librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b"}, - {file = "librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9"}, - {file = "librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db"}, - {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6"}, - {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7"}, - {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1"}, - {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a"}, - {file = "librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628"}, - {file = "librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927"}, - {file = "librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650"}, - {file = "librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566"}, - {file = "librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71"}, - {file = "librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6"}, - {file = "librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f"}, - {file = "librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180"}, - {file = "librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6"}, - {file = "librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9"}, - {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007"}, - {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0"}, - {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1"}, - {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d"}, - {file = "librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16"}, - {file = "librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37"}, - {file = "librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39"}, - {file = "librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6"}, - {file = "librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5"}, - {file = "librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46"}, - {file = "librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e"}, - {file = "librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a"}, - {file = "librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22"}, - {file = "librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c"}, - {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0"}, - {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04"}, - {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61"}, - {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9"}, - {file = "librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18"}, - {file = "librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259"}, - {file = "librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99"}, - {file = "librt-0.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa"}, - {file = "librt-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390"}, - {file = "librt-0.13.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c"}, - {file = "librt-0.13.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4"}, - {file = "librt-0.13.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005"}, - {file = "librt-0.13.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94"}, - {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360"}, - {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b"}, - {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1"}, - {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97"}, - {file = "librt-0.13.0-cp39-cp39-win32.whl", hash = "sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a"}, - {file = "librt-0.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f"}, - {file = "librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781"}, -] - -[[package]] -name = "markdown-it-py" -version = "4.2.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, - {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins (>=0.5.0)"] -profiling = ["gprof2dot"] -rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "more-itertools" -version = "11.1.0" -description = "More routines for operating on iterables, beyond itertools" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" -files = [ - {file = "more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192"}, - {file = "more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d"}, -] - -[[package]] -name = "mypy" -version = "2.3.0" -description = "Optional static typing for Python" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "mypy-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc"}, - {file = "mypy-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3"}, - {file = "mypy-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805"}, - {file = "mypy-2.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117"}, - {file = "mypy-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5"}, - {file = "mypy-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494"}, - {file = "mypy-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee"}, - {file = "mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329"}, - {file = "mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f"}, - {file = "mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a"}, - {file = "mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36"}, - {file = "mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461"}, - {file = "mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd"}, - {file = "mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568"}, - {file = "mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5"}, - {file = "mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c"}, - {file = "mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491"}, - {file = "mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7"}, - {file = "mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3"}, - {file = "mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c"}, - {file = "mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595"}, - {file = "mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97"}, - {file = "mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac"}, - {file = "mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6"}, - {file = "mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b"}, - {file = "mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2"}, - {file = "mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757"}, - {file = "mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1"}, - {file = "mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db"}, - {file = "mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762"}, - {file = "mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef"}, - {file = "mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b"}, - {file = "mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff"}, - {file = "mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4"}, - {file = "mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f"}, - {file = "mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7"}, - {file = "mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373"}, - {file = "mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556"}, - {file = "mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88"}, - {file = "mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe"}, - {file = "mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60"}, - {file = "mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654"}, - {file = "mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5"}, - {file = "mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88"}, - {file = "mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e"}, -] - -[package.dependencies] -ast-serialize = ">=0.6.0,<1.0.0" -librt = {version = ">=0.13.0", markers = "platform_python_implementation != \"PyPy\""} -mypy_extensions = ">=1.0.0" -pathspec = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = [ - {version = ">=4.6.0", markers = "python_version < \"3.15\""}, - {version = ">=4.14.0", markers = "python_version >= \"3.15\""}, -] - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "nh3" -version = "0.3.6" -description = "Python binding to Ammonia HTML sanitizer Rust crate" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2"}, - {file = "nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d"}, - {file = "nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e"}, - {file = "nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917"}, - {file = "nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4"}, - {file = "nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9"}, - {file = "nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6"}, - {file = "nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796"}, - {file = "nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6"}, - {file = "nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41"}, - {file = "nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25"}, - {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69"}, - {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e"}, - {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae"}, - {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d"}, - {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf"}, - {file = "nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e"}, - {file = "nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec"}, - {file = "nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0"}, - {file = "nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78"}, - {file = "nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438"}, - {file = "nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56"}, - {file = "nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f"}, - {file = "nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da"}, - {file = "nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10"}, - {file = "nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21"}, - {file = "nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7"}, -] - -[[package]] -name = "packaging" -version = "26.3" -description = "Core utilities for Python packages" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, - {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, -] - -[[package]] -name = "pathspec" -version = "1.1.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, - {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, -] - -[package.extras] -hyperscan = ["hyperscan (>=0.7)"] -optional = ["typing-extensions (>=4)"] -re2 = ["google-re2 (>=1.1)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pycparser" -version = "3.0" -description = "C parser in Python" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, - {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, -] - -[[package]] -name = "pygments" -version = "2.20.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -description = "Wrappers to call pyproject.toml-based build backend hooks." -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, - {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, -] - -[[package]] -name = "pytest" -version = "9.1.1" -description = "pytest: simple powerful testing with Python" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, - {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -description = "A (partial) reimplementation of pywin32 using ctypes/cffi" -optional = true -python-versions = ">=3.6" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"win32\"" -files = [ - {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, - {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, -] - -[[package]] -name = "readme-renderer" -version = "45.0" -description = "readme_renderer is a library for rendering readme descriptions for Warehouse" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f"}, - {file = "readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1"}, -] - -[package.dependencies] -docutils = ">=0.21.2" -nh3 = ">=0.2.14" -Pygments = ">=2.5.1" - -[package.extras] -md = ["comrak (>=0.0.11)"] - -[[package]] -name = "requests" -version = "2.34.2" -description = "Python HTTP for Humans." -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, - {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, -] - -[package.dependencies] -certifi = ">=2023.5.7" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.26,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -description = "A utility belt for advanced users of python-requests" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, - {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, -] - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - -[[package]] -name = "rfc3986" -version = "2.0.0" -description = "Validating URI References per RFC 3986" -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, - {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, -] - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "rich" -version = "15.0.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = true -python-versions = ">=3.9.0" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, - {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "ruff" -version = "0.15.22" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8"}, - {file = "ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697"}, - {file = "ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb"}, - {file = "ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74"}, - {file = "ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c"}, - {file = "ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296"}, - {file = "ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262"}, - {file = "ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64"}, - {file = "ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf"}, - {file = "ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde"}, - {file = "ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661"}, - {file = "ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809"}, -] - -[[package]] -name = "secretstorage" -version = "3.5.0" -description = "Python bindings to FreeDesktop.org Secret Service API" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" -files = [ - {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, - {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, -] - -[package.dependencies] -cryptography = ">=2.0" -jeepney = ">=0.6" - -[[package]] -name = "tomli" -version = "2.4.1" -description = "A lil' TOML parser" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"dev\" and python_version == \"3.10\"" -files = [ - {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, - {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, - {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, - {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, - {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, - {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, - {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, - {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, - {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, - {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, - {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, - {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, - {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, - {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, - {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, - {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, - {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, - {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, - {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, -] - -[[package]] -name = "twine" -version = "7.0.0" -description = "Collection of utilities for publishing packages on PyPI" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "twine-7.0.0-py3-none-any.whl", hash = "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7"}, - {file = "twine-7.0.0.tar.gz", hash = "sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177"}, -] - -[package.dependencies] -id = "*" -keyring = {version = ">=21.2.0", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} -packaging = ">=26.1" -readme-renderer = ">=35.0" -requests = ">=2.20" -requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" -rfc3986 = ">=1.4.0" -rich = ">=14.3.3" -urllib3 = ">=1.26.0" - -[package.extras] -keyring = ["keyring (>=21.2.0)"] - -[[package]] -name = "typing-extensions" -version = "4.16.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, - {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\"" -files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "zipp" -version = "4.1.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"dev\" and (python_full_version < \"3.10.2\" or platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version < \"3.12\"" -files = [ - {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, - {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] - -[extras] -dev = ["build", "mypy", "pytest", "ruff", "twine"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.10" -content-hash = "0d1ecfbfab514d31d524ab08fbc4b22a290f36afce11254d03f09e9ed5e8d8bc" diff --git a/pyproject.toml b/pyproject.toml index 28472e6..dfa79ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "marginal-ai" -version = "0.1.0" -description = "Open-source compute capital allocator for AI agents: fund only actions whose expected marginal value justifies their token, cost, latency, and risk." +version = "0.2.0" +description = "Universal learning-loop and compute-governance foundation for economically disciplined AI agents." readme = "README.md" requires-python = ">=3.10" license = "Apache-2.0" @@ -32,6 +32,11 @@ keywords = [ "ai-finops", "compute-allocation", "llm-cost-optimization", + "decision-ledger", + "agent-governance", + "shadow-mode", + "privacy-preserving-telemetry", + "pseudonymization", ] classifiers = [ "Development Status :: 3 - Alpha", @@ -54,6 +59,7 @@ dev = [ "build>=1.2.2", "mypy>=1.17", "pytest>=8.3", + "jsonschema>=4.23", "ruff==0.15.22", "twine>=6.1", ] @@ -76,7 +82,7 @@ include-package-data = true where = ["src"] [tool.setuptools.package-data] -marginal = ["py.typed"] +marginal = ["py.typed", "schemas/*.json"] [tool.pytest.ini_options] addopts = "-ra" diff --git a/schemas/agent-capabilities-v1.json b/schemas/agent-capabilities-v1.json new file mode 100644 index 0000000..9a8268c --- /dev/null +++ b/schemas/agent-capabilities-v1.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/agent-capabilities-v1.json", + "title": "MARGINAL Agent Capabilities v1", + "type": "object", + "required": [ + "observe_model_usage", + "block_actions", + "modify_actions", + "stop_agent", + "control_model_turns", + "record_outcomes" + ], + "properties": { + "observe_model_usage": {"type": "boolean"}, + "block_actions": {"type": "boolean"}, + "modify_actions": {"type": "boolean"}, + "stop_agent": {"type": "boolean"}, + "control_model_turns": {"type": "boolean"}, + "record_outcomes": {"type": "boolean"}, + "level": {"enum": ["observe", "control", "full"]} + }, + "additionalProperties": false +} diff --git a/schemas/agent-decision-v1.json b/schemas/agent-decision-v1.json new file mode 100644 index 0000000..67cf9b1 --- /dev/null +++ b/schemas/agent-decision-v1.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/agent-decision-v1.json", + "title": "MARGINAL Agent Decision v1", + "type": "object", + "required": [ + "action_id", + "allowed", + "recommended", + "reason", + "reason_code", + "recommendation_reason", + "recommendation_reason_code", + "mode", + "directive", + "recommended_directive", + "replacement", + "score", + "expected_gain", + "estimated_cost_value", + "uncertainty", + "confidence" + ], + "properties": { + "action_id": {"type": "string", "minLength": 1}, + "allowed": {"type": "boolean"}, + "recommended": {"type": "boolean"}, + "reason": {"type": "string", "minLength": 1}, + "reason_code": {"type": "string", "minLength": 1}, + "recommendation_reason": {"type": "string", "minLength": 1}, + "recommendation_reason_code": {"type": "string", "minLength": 1}, + "mode": {"enum": ["shadow", "recommend", "enforce"]}, + "directive": { + "enum": ["allow", "deny", "modify", "defer", "reuse", "stop", "force_verify"] + }, + "recommended_directive": { + "enum": ["allow", "deny", "modify", "defer", "reuse", "stop", "force_verify"] + }, + "replacement": {"type": "object"}, + "score": {"type": "number"}, + "expected_gain": {"type": "number", "minimum": 0, "maximum": 1}, + "estimated_cost_value": {"type": "number"}, + "uncertainty": {"type": "number", "minimum": 0}, + "confidence": {"type": "number", "minimum": 0, "maximum": 1} + }, + "additionalProperties": false +} diff --git a/schemas/agent-event-v1.json b/schemas/agent-event-v1.json new file mode 100644 index 0000000..a9830d9 --- /dev/null +++ b/schemas/agent-event-v1.json @@ -0,0 +1,111 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/agent-event-v1.json", + "title": "MARGINAL Agent Event v1", + "type": "object", + "required": ["protocol_version", "engine", "session_id", "task_id", "event_type"], + "properties": { + "protocol_version": {"const": "1.0"}, + "engine": {"type": "string", "minLength": 1}, + "session_id": {"type": "string", "minLength": 1}, + "task_id": {"type": "string", "minLength": 1}, + "event_type": { + "enum": [ + "session.start", + "session.end", + "action.before", + "action.after", + "action.failed", + "outcome" + ] + }, + "action": { + "oneOf": [ + {"$ref": "#/$defs/agent_action"}, + {"type": "null"} + ] + }, + "state": {"type": "object"}, + "metadata": {"type": "object"} + }, + "$defs": { + "cost": { + "type": "object", + "required": ["tokens", "usd", "latency_ms", "risk"], + "properties": { + "tokens": {"type": "integer", "minimum": 0}, + "usd": {"type": "number", "minimum": 0}, + "latency_ms": {"type": "integer", "minimum": 0}, + "risk": {"type": "number", "minimum": 0} + }, + "additionalProperties": false + }, + "token_usage": { + "type": "object", + "required": [ + "input_tokens", + "cached_input_tokens", + "output_tokens", + "reasoning_tokens", + "total_tokens" + ], + "properties": { + "input_tokens": {"type": "integer", "minimum": 0}, + "cached_input_tokens": {"type": "integer", "minimum": 0}, + "output_tokens": {"type": "integer", "minimum": 0}, + "reasoning_tokens": {"type": "integer", "minimum": 0}, + "total_tokens": {"type": "integer", "minimum": 0} + }, + "additionalProperties": false + }, + "agent_action": { + "type": "object", + "required": [ + "action_id", + "name", + "kind", + "estimated_cost", + "current_success_probability", + "is_verification", + "state_hash", + "phase", + "retry_number", + "deduplication_scope", + "metadata" + ], + "properties": { + "action_id": {"type": "string", "minLength": 1}, + "name": {"type": "string", "minLength": 1}, + "kind": {"type": "string", "minLength": 1}, + "estimated_cost": {"$ref": "#/$defs/cost"}, + "token_usage": { + "oneOf": [ + {"$ref": "#/$defs/token_usage"}, + {"type": "null"} + ] + }, + "expected_gain": { + "oneOf": [ + {"type": "number", "minimum": 0, "maximum": 1}, + {"type": "null"} + ] + }, + "current_success_probability": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "is_verification": {"type": "boolean"}, + "state_hash": {"type": "string"}, + "phase": {"type": "string"}, + "retry_number": {"type": "integer", "minimum": 0}, + "deduplication_scope": { + "enum": ["exact", "once_per_state", "once_per_phase", "allow_retry"] + }, + "metadata": {"type": "object"} + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schemas/aggregate-export-v1.json b/schemas/aggregate-export-v1.json new file mode 100644 index 0000000..7c53dfb --- /dev/null +++ b/schemas/aggregate-export-v1.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/aggregate-export-v1.json", + "title": "MARGINAL Aggregate Privacy Export Record v1", + "description": "Grouped, generalized decision or outcome data with no identifiers, timestamps, free text, metadata, model identity, verifier details, or tool arguments.", + "type": "object", + "required": [ + "schema_version", + "privacy_profile", + "record_type", + "action_kind", + "cost_bucket", + "gain_bucket", + "recommendation", + "applied_decision", + "reason_code", + "outcome_class", + "count", + "minimum_group_size" + ], + "properties": { + "schema_version": { + "const": "1.0" + }, + "privacy_profile": { + "const": "aggregate_export" + }, + "record_type": { + "enum": [ + "decision", + "outcome" + ] + }, + "action_kind": { + "type": "string", + "pattern": "^[a-z0-9_.-]{1,64}$" + }, + "cost_bucket": { + "enum": [ + "low", + "medium", + "high", + "unknown" + ] + }, + "gain_bucket": { + "enum": [ + "low", + "medium", + "high", + "unknown" + ] + }, + "recommendation": { + "enum": [ + "allow", + "deny", + "unknown", + "not_applicable" + ] + }, + "applied_decision": { + "enum": [ + "allow", + "deny", + "unknown", + "not_applicable" + ] + }, + "reason_code": { + "type": "string", + "pattern": "^[A-Z0-9_]{1,64}$|^not_applicable$" + }, + "outcome_class": { + "enum": [ + "verified_success", + "verified_failure", + "positive_reward", + "non_positive_reward", + "unknown", + "not_applicable" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "minimum_group_size": { + "type": "integer", + "minimum": 1, + "description": "Configured k-threshold; groups with fewer source records are suppressed." + } + }, + "additionalProperties": false +} diff --git a/schemas/decision-ledger-v2.json b/schemas/decision-ledger-v2.json new file mode 100644 index 0000000..7dee902 --- /dev/null +++ b/schemas/decision-ledger-v2.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/decision-ledger-v2.json", + "title": "MARGINAL Decision Ledger Record v2", + "type": "object", + "required": [ + "schema_version", + "event_id", + "sequence", + "timestamp", + "run_id", + "event" + ], + "properties": { + "schema_version": { + "const": "2.0" + }, + "event_id": { + "type": "string", + "minLength": 1 + }, + "sequence": { + "type": "integer", + "minimum": 1 + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "run_id": { + "type": "string", + "minLength": 1 + }, + "task_id": { + "type": "string" + }, + "trajectory_id": { + "type": "string" + }, + "engine": { + "type": "string" + }, + "model": { + "type": "string" + }, + "event": { + "type": "string", + "minLength": 1 + }, + "mode": { + "type": "string" + }, + "policy": { + "type": "object" + }, + "estimator": { + "type": "object" + }, + "action": { + "type": "object" + }, + "decision": { + "type": "object" + }, + "outcome": { + "type": "object" + }, + "privacy_profile": { + "type": "string", + "enum": [ + "local_full", + "safe_telemetry" + ], + "default": "local_full" + } + }, + "additionalProperties": true +} diff --git a/schemas/outcome-v1.json b/schemas/outcome-v1.json new file mode 100644 index 0000000..533e29b --- /dev/null +++ b/schemas/outcome-v1.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/outcome-v1.json", + "title": "MARGINAL Outcome v1", + "type": "object", + "required": ["task_id", "reward"], + "properties": { + "task_id": {"type": "string", "minLength": 1}, + "reward": {"type": "number"}, + "resolved": {"type": ["boolean", "null"]}, + "verifier": {"type": "string"}, + "trajectory_id": {"type": "string"}, + "evidence": {"type": "object"}, + "metrics": {"type": "object", "additionalProperties": {"type": "number"}} + }, + "additionalProperties": false +} diff --git a/schemas/safe-telemetry-v1.json b/schemas/safe-telemetry-v1.json new file mode 100644 index 0000000..a800814 --- /dev/null +++ b/schemas/safe-telemetry-v1.json @@ -0,0 +1,392 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/safe-telemetry-v1.json", + "title": "MARGINAL Safe Telemetry Record v1", + "description": "Strict event-level telemetry with keyed pseudonyms, generalized timestamps, allowlisted labels and numeric fields, and no free-form content.", + "type": "object", + "required": [ + "schema_version", + "privacy_profile", + "event_id", + "sequence", + "timestamp", + "run_id", + "event" + ], + "properties": { + "schema_version": { + "const": "2.0" + }, + "privacy_profile": { + "const": "safe_telemetry" + }, + "event_id": { + "type": "string", + "pattern": "^psn_[0-9a-f]{32}$" + }, + "sequence": { + "type": "integer", + "minimum": 1 + }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T00:00:00\\+00:00$" + }, + "run_id": { + "type": "string", + "pattern": "^psn_[0-9a-f]{32}$" + }, + "task_id": { + "$ref": "#/$defs/pseudonym" + }, + "trajectory_id": { + "$ref": "#/$defs/pseudonym" + }, + "action_id": { + "$ref": "#/$defs/pseudonym" + }, + "engine_instance": { + "$ref": "#/$defs/pseudonym" + }, + "engine": { + "enum": [ + "aider", + "claude-code", + "cline", + "codex", + "continue", + "gemini-cli", + "github-copilot", + "opencode", + "roo-code", + "other" + ] + }, + "event": { + "enum": [ + "abort", + "authorization", + "candidate_ranking", + "commit", + "custom", + "estimator_observation", + "failure_settlement", + "outcome", + "session_end", + "session_start" + ] + }, + "mode": { + "enum": [ + "shadow", + "recommend", + "enforce", + "unknown" + ] + }, + "budget_overrun": { + "type": "boolean" + }, + "realized_gain": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "policy": { + "$ref": "#/$defs/identity" + }, + "estimator": { + "$ref": "#/$defs/identity" + }, + "action": { + "$ref": "#/$defs/action" + }, + "decision": { + "$ref": "#/$defs/decision" + }, + "outcome": { + "$ref": "#/$defs/outcome" + }, + "candidates": { + "type": "array", + "items": { + "$ref": "#/$defs/candidate" + } + }, + "usage": { + "$ref": "#/$defs/numeric_usage" + }, + "reserved": { + "$ref": "#/$defs/numeric_usage" + } + }, + "$defs": { + "pseudonym": { + "type": "string", + "pattern": "^(?:psn_[0-9a-f]{32})?$" + }, + "version": { + "type": "string", + "pattern": "^(?:v?\\d+(?:\\.\\d+){0,3}(?:[-+][0-9A-Za-z.-]{1,24})?|unknown|unversioned)$" + }, + "numeric_usage": { + "type": "object", + "properties": { + "tokens": { + "type": "number", + "minimum": 0 + }, + "usd": { + "type": "number", + "minimum": 0 + }, + "latency_ms": { + "type": "number", + "minimum": 0 + }, + "risk": { + "type": "number", + "minimum": 0 + }, + "input_tokens": { + "type": "number", + "minimum": 0 + }, + "cached_input_tokens": { + "type": "number", + "minimum": 0 + }, + "output_tokens": { + "type": "number", + "minimum": 0 + }, + "reasoning_tokens": { + "type": "number", + "minimum": 0 + }, + "total_tokens": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "version": { + "$ref": "#/$defs/version" + } + }, + "additionalProperties": false + }, + "action": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "command", + "file_read", + "file_write", + "generation", + "llm", + "model_call", + "reasoning", + "research", + "review", + "search", + "subagent", + "test", + "tool", + "verification", + "other" + ] + }, + "expected_gain": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "current_success_probability": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "is_verification": { + "type": "boolean" + }, + "retry_number": { + "type": "integer", + "minimum": 0 + }, + "deduplication_scope": { + "enum": [ + "exact", + "once_per_state", + "once_per_phase", + "allow_retry", + "unknown" + ] + }, + "cost": { + "$ref": "#/$defs/numeric_usage" + }, + "estimated_cost": { + "$ref": "#/$defs/numeric_usage" + }, + "token_usage": { + "$ref": "#/$defs/numeric_usage" + }, + "fingerprint": { + "$ref": "#/$defs/pseudonym" + }, + "action_id": { + "$ref": "#/$defs/pseudonym" + }, + "state_hash": { + "$ref": "#/$defs/pseudonym" + } + }, + "additionalProperties": false + }, + "decision": { + "type": "object", + "properties": { + "allowed": { + "type": "boolean" + }, + "recommended": { + "type": "boolean" + }, + "score": { + "type": "number" + }, + "expected_gain": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "estimated_cost_value": { + "type": "number", + "minimum": 0 + }, + "uncertainty": { + "type": "number", + "minimum": 0 + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "reason_code": { + "enum": [ + "APPROVED", + "BUDGET_REJECTED", + "DENY", + "DUPLICATE_ACTION", + "DUPLICATE_PENDING", + "EXPECTED_GAIN_REJECTED", + "FUNDED", + "MARGINAL_ROI_REJECTED", + "OTHER", + "PARENT_BUDGET_REJECTED", + "RECOMMEND_OVERRIDE", + "SHADOW_OVERRIDE", + "TARGET_REACHED", + "UNSPECIFIED" + ] + }, + "recommendation_reason_code": { + "enum": [ + "APPROVED", + "BUDGET_REJECTED", + "DENY", + "DUPLICATE_ACTION", + "DUPLICATE_PENDING", + "EXPECTED_GAIN_REJECTED", + "FUNDED", + "MARGINAL_ROI_REJECTED", + "OTHER", + "PARENT_BUDGET_REJECTED", + "RECOMMEND_OVERRIDE", + "SHADOW_OVERRIDE", + "TARGET_REACHED", + "UNSPECIFIED" + ] + }, + "mode": { + "enum": [ + "shadow", + "recommend", + "enforce", + "unknown" + ] + }, + "directive": { + "enum": [ + "allow", + "deny", + "modify", + "defer", + "reuse", + "stop", + "force_verify", + "unknown" + ] + }, + "recommended_directive": { + "enum": [ + "allow", + "deny", + "modify", + "defer", + "reuse", + "stop", + "force_verify", + "unknown" + ] + }, + "estimator_version": { + "$ref": "#/$defs/version" + } + }, + "additionalProperties": false + }, + "outcome": { + "type": "object", + "properties": { + "task_id": { + "$ref": "#/$defs/pseudonym" + }, + "trajectory_id": { + "$ref": "#/$defs/pseudonym" + }, + "reward": { + "type": "number" + }, + "resolved": { + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + }, + "candidate": { + "type": "object", + "properties": { + "action": { + "$ref": "#/$defs/action" + }, + "decision": { + "$ref": "#/$defs/decision" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schemas/token-usage-v2.json b/schemas/token-usage-v2.json new file mode 100644 index 0000000..29cd304 --- /dev/null +++ b/schemas/token-usage-v2.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/token-usage-v2.json", + "title": "MARGINAL Token Usage v2", + "type": "object", + "required": [ + "input_tokens", + "cached_input_tokens", + "output_tokens", + "reasoning_tokens", + "total_tokens" + ], + "properties": { + "input_tokens": {"type": "integer", "minimum": 0}, + "cached_input_tokens": {"type": "integer", "minimum": 0}, + "output_tokens": {"type": "integer", "minimum": 0}, + "reasoning_tokens": {"type": "integer", "minimum": 0}, + "total_tokens": {"type": "integer", "minimum": 0} + }, + "additionalProperties": false +} diff --git a/src/marginal/__init__.py b/src/marginal/__init__.py index 990f63e..e393076 100644 --- a/src/marginal/__init__.py +++ b/src/marginal/__init__.py @@ -1,25 +1,74 @@ -"""MARGINAL: fund only the next agent action worth taking.""" +"""MARGINAL: economically disciplined compute allocation for AI agents.""" from .adapters import ( ActionDenied, BudgetedCallable, + FailureUsageExtractor, async_budgeted_call, async_funded_call, budgeted_call, extract_common_llm_usage, + extract_common_token_usage, funded_call, ) from .budget import BudgetExceeded, BudgetLedger, BudgetLimits, BudgetOverrun, BudgetUsage -from .estimator import ValueEstimator +from .estimator import EstimatorIdentity, ValueEstimate, ValueEstimator from .killer_demo import run_killer_demo -from .models import Action, Allocation, Cost, Decision -from .policy import MarginalPolicy, PolicyConfig -from .trace import JsonlTraceSink +from .ledger import ( + LEDGER_SCHEMA_VERSION, + DecisionLedgerContext, + JsonlDecisionLedger, + export_decision_ledger, + read_decision_ledger, + summarize_decision_ledger, +) +from .models import Action, Allocation, Cost, Decision, TokenUsage +from .modes import ExecutionMode +from .outcomes import Outcome +from .policy import MarginalPolicy, PolicyConfig, PolicyIdentity +from .privacy import ( + FIELD_CLASSIFICATION, + LocalPseudonymizer, + PrivacyClass, + PrivacyConfig, + PrivacyProfile, + aggregate_ledger_records, + classify_field, + generate_local_identifier, + load_or_create_privacy_key, + sanitize_ledger_record, + validate_safe_telemetry_record, +) +from .profiles import PolicyProfile, build_policy, policy_config_for_profile +from .protocol import ( + PROTOCOL_VERSION, + AgentAction, + AgentCapabilities, + AgentDecision, + AgentDirective, + AgentEvent, + AgentEventType, + DeduplicationScope, +) +from .registry import EstimatorRegistry +from .replay import ReplayResult, render_replay_report, replay_ledger +from .runtime import UniversalRuntime +from .schema import available_schemas, load_schema +from .trace import CompositeTraceSink, JsonlTraceSink from .treasury import AuthorizationRequired, Treasury __all__ = [ + "FIELD_CLASSIFICATION", + "LEDGER_SCHEMA_VERSION", + "PROTOCOL_VERSION", "Action", "ActionDenied", + "AgentAction", + "AgentCapabilities", + "AgentDecision", + "AgentDirective", + "AgentEvent", + "AgentEventType", "Allocation", "AuthorizationRequired", "BudgetExceeded", @@ -28,19 +77,54 @@ "BudgetOverrun", "BudgetUsage", "BudgetedCallable", + "CompositeTraceSink", "Cost", "Decision", + "DecisionLedgerContext", + "DeduplicationScope", + "EstimatorIdentity", + "EstimatorRegistry", + "ExecutionMode", + "FailureUsageExtractor", + "JsonlDecisionLedger", "JsonlTraceSink", + "LocalPseudonymizer", "MarginalPolicy", + "Outcome", "PolicyConfig", + "PolicyIdentity", + "PolicyProfile", + "PrivacyClass", + "PrivacyConfig", + "PrivacyProfile", + "ReplayResult", + "TokenUsage", "Treasury", + "UniversalRuntime", + "ValueEstimate", "ValueEstimator", + "aggregate_ledger_records", "async_budgeted_call", "async_funded_call", + "available_schemas", "budgeted_call", + "build_policy", + "classify_field", + "export_decision_ledger", "extract_common_llm_usage", + "extract_common_token_usage", "funded_call", + "generate_local_identifier", + "load_or_create_privacy_key", + "load_schema", + "policy_config_for_profile", + "read_decision_ledger", + "render_replay_report", + "replay_ledger", "run_killer_demo", + "sanitize_ledger_record", + "summarize_decision_ledger", + "validate_safe_telemetry_record", ] -__version__ = "0.1.0" +__version__ = "0.2.0" diff --git a/src/marginal/adapters.py b/src/marginal/adapters.py index 70bc562..ec6a58a 100644 --- a/src/marginal/adapters.py +++ b/src/marginal/adapters.py @@ -7,7 +7,7 @@ from typing import Any, Generic, NoReturn, ParamSpec, TypeVar from .fingerprint import fingerprint_call -from .models import Action, Allocation, Cost, Decision +from .models import Action, Allocation, Cost, Decision, TokenUsage from .treasury import AuthorizationRequired, Treasury P = ParamSpec("P") @@ -23,6 +23,7 @@ def __init__(self, decision: Decision) -> None: UsageExtractor = Callable[[Any, Cost], Cost] +FailureUsageExtractor = Callable[[Exception, Cost], Cost | None] ActionFactory = Callable[[tuple[Any, ...], dict[str, Any]], Action] @@ -34,10 +35,7 @@ def _prepare_call( ) -> Action: if action.fingerprint: return action - return replace( - action, - fingerprint=fingerprint_call(action, function, args, kwargs), - ) + return replace(action, fingerprint=fingerprint_call(action, function, args, kwargs)) def _settle_result( @@ -67,11 +65,42 @@ def _require_funded(treasury: Treasury, allocation: Allocation) -> Action: return action -def _abort_after_error(treasury: Treasury, action: Action, error: Exception) -> NoReturn: +def _handle_execution_error( + treasury: Treasury, + action: Action, + error: Exception, + failure_usage_extractor: FailureUsageExtractor | None, +) -> NoReturn: + reason = f"{type(error).__name__}: {error}" + if failure_usage_extractor is None: + try: + treasury.abort(action, reason=reason) + except Exception as abort_error: + raise error from abort_error + raise error + + try: + actual_cost = failure_usage_extractor(error, action.cost) + if actual_cost is not None and not isinstance(actual_cost, Cost): + raise TypeError("failure_usage_extractor must return Cost or None") + except Exception as extraction_error: + try: + treasury.settle_failure( + action, + action.cost, + reason=f"{reason}; usage extraction failed conservatively", + ) + except Exception as settlement_error: + raise error from settlement_error + raise error from extraction_error + try: - treasury.abort(action, reason=f"{type(error).__name__}: {error}") - except Exception as abort_error: - raise error from abort_error + if actual_cost is None: + treasury.abort(action, reason=reason) + else: + treasury.settle_failure(action, actual_cost, reason=reason) + except Exception as settlement_error: + raise error from settlement_error raise error @@ -81,24 +110,19 @@ def budgeted_call( *args: Any, action: Action, usage_extractor: UsageExtractor | None = None, + failure_usage_extractor: FailureUsageExtractor | None = None, **kwargs: Any, ) -> R: - """Authorize, execute, and settle a callable. - - The callable is never invoked when authorization fails. Callable failures release the - reservation. Usage extraction receives both the result and estimated cost so fields - not observable from a provider response can be preserved explicitly. - """ + """Authorize, execute, and settle a synchronous callable.""" prepared = _prepare_call(action, function, tuple(args), kwargs) decision = treasury.authorize(prepared) if not decision.allowed: raise ActionDenied(decision) - try: result = function(*args, **kwargs) except Exception as exc: - _abort_after_error(treasury, prepared, exc) + _handle_execution_error(treasury, prepared, exc, failure_usage_extractor) return _settle_result(treasury, prepared, result, usage_extractor) @@ -108,6 +132,7 @@ async def async_budgeted_call( *args: Any, action: Action, usage_extractor: UsageExtractor | None = None, + failure_usage_extractor: FailureUsageExtractor | None = None, **kwargs: Any, ) -> R: """Async equivalent of :func:`budgeted_call`.""" @@ -116,11 +141,10 @@ async def async_budgeted_call( decision = treasury.authorize(prepared) if not decision.allowed: raise ActionDenied(decision) - try: result = await function(*args, **kwargs) except Exception as exc: - _abort_after_error(treasury, prepared, exc) + _handle_execution_error(treasury, prepared, exc, failure_usage_extractor) return _settle_result(treasury, prepared, result, usage_extractor) @@ -130,15 +154,16 @@ def funded_call( function: Callable[..., R], *args: Any, usage_extractor: UsageExtractor | None = None, + failure_usage_extractor: FailureUsageExtractor | None = None, **kwargs: Any, ) -> R: - """Execute and settle an action already reserved by :meth:`Treasury.fund_best`.""" + """Execute and settle an action reserved by :meth:`Treasury.fund_best`.""" action = _require_funded(treasury, allocation) try: result = function(*args, **kwargs) except Exception as exc: - _abort_after_error(treasury, action, exc) + _handle_execution_error(treasury, action, exc, failure_usage_extractor) return _settle_result(treasury, action, result, usage_extractor) @@ -148,6 +173,7 @@ async def async_funded_call( function: Callable[..., Awaitable[R]], *args: Any, usage_extractor: UsageExtractor | None = None, + failure_usage_extractor: FailureUsageExtractor | None = None, **kwargs: Any, ) -> R: """Async equivalent of :func:`funded_call`.""" @@ -156,7 +182,7 @@ async def async_funded_call( try: result = await function(*args, **kwargs) except Exception as exc: - _abort_after_error(treasury, action, exc) + _handle_execution_error(treasury, action, exc, failure_usage_extractor) return _settle_result(treasury, action, result, usage_extractor) @@ -170,11 +196,13 @@ def __init__( *, action_factory: ActionFactory, usage_extractor: UsageExtractor | None = None, + failure_usage_extractor: FailureUsageExtractor | None = None, ) -> None: self.treasury = treasury self.function = function self.action_factory = action_factory self.usage_extractor = usage_extractor + self.failure_usage_extractor = failure_usage_extractor def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: action = self.action_factory(tuple(args), dict(kwargs)) @@ -184,12 +212,124 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: *args, action=action, usage_extractor=self.usage_extractor, + failure_usage_extractor=self.failure_usage_extractor, **kwargs, ) +def extract_common_token_usage(result: Any) -> TokenUsage: + """Read a normalized token breakdown from common provider response shapes. + + Common provider ``input_tokens`` counters usually include cached input. When a cached + counter is present, this function reports uncached input in ``input_tokens`` and cached + input separately so the four components remain additive. + """ + + usage = getattr(result, "usage", None) + if usage is None and isinstance(result, dict): + usage = result.get("usage") + if usage is None: + raise ValueError("response does not expose usage information") + + def read(*names: str) -> int | None: + for name in names: + if isinstance(usage, dict) and name in usage: + value = usage[name] + else: + value = getattr(usage, name, None) + if value is None: + continue + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError("usage token fields must be integers") + if value < 0: + raise ValueError("usage token fields must be non-negative") + return value + return None + + raw_input = read("input_tokens", "prompt_tokens") or 0 + cached = read("cached_input_tokens", "cached_tokens") or 0 + raw_output = read("output_tokens", "completion_tokens") or 0 + reasoning = read("reasoning_tokens") or 0 + reasoning_is_output_subset = False + + details = None + if isinstance(usage, dict): + details = usage.get("input_tokens_details") or usage.get("prompt_tokens_details") + else: + details = getattr(usage, "input_tokens_details", None) or getattr( + usage, "prompt_tokens_details", None + ) + if details is not None and cached == 0: + if isinstance(details, dict): + detail_cached = details.get("cached_tokens") + else: + detail_cached = getattr(details, "cached_tokens", None) + if detail_cached is not None: + if isinstance(detail_cached, bool) or not isinstance(detail_cached, int): + raise TypeError("usage token fields must be integers") + if detail_cached < 0: + raise ValueError("usage token fields must be non-negative") + cached = detail_cached + + output_details = None + if isinstance(usage, dict): + output_details = usage.get("output_tokens_details") or usage.get( + "completion_tokens_details" + ) + else: + output_details = getattr(usage, "output_tokens_details", None) or getattr( + usage, "completion_tokens_details", None + ) + if output_details is not None and reasoning == 0: + if isinstance(output_details, dict): + detail_reasoning = output_details.get("reasoning_tokens") + else: + detail_reasoning = getattr(output_details, "reasoning_tokens", None) + if detail_reasoning is not None: + if isinstance(detail_reasoning, bool) or not isinstance(detail_reasoning, int): + raise TypeError("usage token fields must be integers") + if detail_reasoning < 0: + raise ValueError("usage token fields must be non-negative") + reasoning = detail_reasoning + reasoning_is_output_subset = True + + if cached > raw_input: + raise ValueError("cached input tokens cannot exceed total input tokens") + uncached_input = raw_input - cached + declared_total = read("total_tokens") + + if declared_total is None: + if reasoning_is_output_subset: + if reasoning > raw_output: + raise ValueError("reasoning tokens cannot exceed total output tokens") + output = raw_output - reasoning + else: + output = raw_output + calculated = uncached_input + cached + output + reasoning + elif declared_total == raw_input + raw_output: + if reasoning > raw_output: + raise ValueError("reasoning tokens cannot exceed total output tokens") + output = raw_output - reasoning + calculated = declared_total + elif declared_total == raw_input + raw_output + reasoning: + output = raw_output + calculated = declared_total + else: + raise ValueError("total_tokens is inconsistent with the normalized token breakdown") + + if calculated == 0 and declared_total is None: + raise ValueError("usage does not expose recognized token fields") + return TokenUsage( + input_tokens=uncached_input, + cached_input_tokens=cached, + output_tokens=output, + reasoning_tokens=reasoning, + total_tokens=calculated, + ) + + def extract_common_llm_usage(result: Any, estimated_cost: Cost) -> Cost: - """Read common token fields while preserving cost dimensions not in the response.""" + """Read common total token fields while preserving unobserved dimensions.""" usage = getattr(result, "usage", None) if usage is None and isinstance(result, dict): diff --git a/src/marginal/budget.py b/src/marginal/budget.py index afdf606..a923648 100644 --- a/src/marginal/budget.py +++ b/src/marginal/budget.py @@ -18,8 +18,6 @@ class BudgetOverrun(BudgetExceeded): @dataclass(frozen=True, slots=True) class BudgetLimits: - """Hard limits and reserves for one treasury.""" - max_tokens: int | None = None max_usd: float | None = None max_latency_ms: int | None = None @@ -38,7 +36,6 @@ def __post_init__(self) -> None: isinstance(integer_value, bool) or not isinstance(integer_value, int) ): raise TypeError(f"{name} must be an integer") - numeric_fields = ( ("max_usd", self.max_usd), ("max_risk", self.max_risk), @@ -51,7 +48,6 @@ def __post_init__(self) -> None: raise TypeError(f"{name} must be a number") if numeric_value is not None and not math.isfinite(float(numeric_value)): raise ValueError("budget values must be finite") - values = ( self.max_tokens, self.max_usd, @@ -89,8 +85,6 @@ def plus(self, cost: Cost) -> BudgetUsage: class BudgetLedger: - """Track committed usage and pending reservations for one budget.""" - def __init__(self, limits: BudgetLimits) -> None: self.limits = limits self._usage = BudgetUsage() @@ -99,31 +93,18 @@ def __init__(self, limits: BudgetLimits) -> None: @property def usage(self) -> BudgetUsage: - """Return committed usage only.""" - return self._usage @property def reserved_usage(self) -> BudgetUsage: - """Return resources reserved by approved but unsettled actions.""" - return self._sum_reservations(regular_only=False) - def can_afford( - self, - action: Action, - *, - replacing_fingerprint: str | None = None, - ) -> Decision: - """Check an action against committed usage plus pending reservations.""" - + def can_afford(self, action: Action, *, replacing_fingerprint: str | None = None) -> Decision: reserved = self._sum_reservations( - regular_only=False, - excluding_fingerprint=replacing_fingerprint, + regular_only=False, excluding_fingerprint=replacing_fingerprint ) regular_reserved = self._sum_reservations( - regular_only=True, - excluding_fingerprint=replacing_fingerprint, + regular_only=True, excluding_fingerprint=replacing_fingerprint ) projected = self._usage.plus( Cost( @@ -134,7 +115,6 @@ def can_afford( ) ) limits = self.limits - if limits.max_tokens is not None: if projected.tokens > limits.max_tokens: return Decision(False, "token budget exceeded") @@ -142,30 +122,22 @@ def can_afford( regular_tokens = ( self._regular_usage.tokens + regular_reserved.tokens + action.cost.tokens ) - regular_limit = limits.max_tokens - limits.verification_reserve_tokens - if regular_tokens > regular_limit: + if regular_tokens > limits.max_tokens - limits.verification_reserve_tokens: return Decision(False, "verification reserve would be breached") - if limits.max_usd is not None: if projected.usd > float(limits.max_usd) + 1e-12: return Decision(False, "USD budget exceeded") if not action.is_verification: regular_usd = self._regular_usage.usd + regular_reserved.usd + action.cost.usd - regular_usd_limit = float(limits.max_usd) - float(limits.verification_reserve_usd) - if regular_usd > regular_usd_limit + 1e-12: + if regular_usd > float(limits.max_usd) - limits.verification_reserve_usd + 1e-12: return Decision(False, "verification reserve would be breached") - if limits.max_latency_ms is not None and projected.latency_ms > limits.max_latency_ms: return Decision(False, "latency budget exceeded") - if limits.max_risk is not None and projected.risk > limits.max_risk + 1e-12: return Decision(False, "risk budget exceeded") - return Decision(True, "within budget") def reserve(self, action: Action) -> None: - """Reserve an approved estimate without increasing committed usage.""" - if not action.fingerprint: raise ValueError("reserved actions require a fingerprint") if action.fingerprint in self._reservations: @@ -175,14 +147,19 @@ def reserve(self, action: Action) -> None: raise BudgetExceeded(decision.reason) self._reservations[action.fingerprint] = action - def release(self, fingerprint: str) -> None: - """Release a pending reservation if it exists.""" + def reserve_unchecked(self, action: Action) -> None: + """Reserve an action for non-blocking observation even when it exceeds limits.""" + + if not action.fingerprint: + raise ValueError("reserved actions require a fingerprint") + if action.fingerprint in self._reservations: + raise BudgetExceeded("duplicate budget reservation") + self._reservations[action.fingerprint] = action + def release(self, fingerprint: str) -> None: self._reservations.pop(fingerprint, None) def commit(self, action: Action) -> BudgetUsage: - """Commit an action without an existing reservation.""" - decision = self.can_afford(action) if not decision.allowed: raise BudgetExceeded(decision.reason) @@ -190,12 +167,6 @@ def commit(self, action: Action) -> BudgetUsage: return self._usage def settle(self, action: Action, *, reservation_fingerprint: str) -> Decision: - """Replace a reservation with actual usage and always account for the spend. - - The returned decision reports whether actual usage remained within the budget. An - overrun is still recorded because the external action has already executed. - """ - decision = self.can_afford(action, replacing_fingerprint=reservation_fingerprint) self.release(reservation_fingerprint) self._record(action) @@ -207,10 +178,7 @@ def _record(self, action: Action) -> None: self._regular_usage = self._regular_usage.plus(action.cost) def _sum_reservations( - self, - *, - regular_only: bool, - excluding_fingerprint: str | None = None, + self, *, regular_only: bool, excluding_fingerprint: str | None = None ) -> BudgetUsage: total = BudgetUsage() for fingerprint, action in self._reservations.items(): diff --git a/src/marginal/cli.py b/src/marginal/cli.py index e3fe17e..97ad644 100644 --- a/src/marginal/cli.py +++ b/src/marginal/cli.py @@ -1,4 +1,4 @@ -"""Command-line interface for MARGINAL traces and demos.""" +"""Command-line interface for MARGINAL traces, ledgers, replay, and demos.""" from __future__ import annotations @@ -38,7 +38,7 @@ def summarize_trace(events: list[dict[str, Any]]) -> dict[str, Any]: approved += 1 else: denied += 1 - elif event.get("event") == "commit": + elif event.get("event") in {"commit", "failure_settlement"}: committed += 1 usage.update(event.get("usage", {})) return { @@ -57,34 +57,78 @@ def _build_parser() -> argparse.ArgumentParser: ) subparsers = parser.add_subparsers(dest="command", required=True) - report = subparsers.add_parser("report", help="summarize a MARGINAL JSONL trace") + report = subparsers.add_parser("report", help="summarize a v0.1 MARGINAL JSONL trace") report.add_argument("trace", type=Path) report.add_argument("--json", action="store_true", dest="as_json") - validate = subparsers.add_parser("validate", help="validate a MARGINAL JSONL trace") + validate = subparsers.add_parser("validate", help="validate a v0.1 MARGINAL JSONL trace") validate.add_argument("trace", type=Path) - subparsers.add_parser("demo", help="run the deterministic bundled benchmark") + ledger_report = subparsers.add_parser( + "ledger-report", help="summarize a MARGINAL decision ledger v2" + ) + ledger_report.add_argument("ledger", type=Path) + ledger_report.add_argument("--json", action="store_true", dest="as_json") - killer = subparsers.add_parser( - "killer-demo", - help="run the end-to-end compute allocation demonstration", + ledger_validate = subparsers.add_parser( + "ledger-validate", help="validate a MARGINAL decision ledger v2" ) - public_eval = subparsers.add_parser( - "public-eval", - help="compare matched baseline and MARGINAL public-benchmark runs", + ledger_validate.add_argument("ledger", type=Path) + + ledger_export = subparsers.add_parser( + "ledger-export", + help="export a decision ledger with a privacy-preserving profile", ) - public_eval.add_argument("baseline", type=Path) - public_eval.add_argument("marginal", type=Path) - public_eval.add_argument("--json", action="store_true", dest="as_json") - public_eval.add_argument("--bootstrap-samples", type=int, default=2_000) + ledger_export.add_argument("source", type=Path) + ledger_export.add_argument("destination", type=Path) + ledger_export.add_argument( + "--privacy-profile", + required=True, + choices=["safe_telemetry", "aggregate_export"], + ) + ledger_export.add_argument("--privacy-key-file", type=Path) + ledger_export.add_argument( + "--minimum-group-size", + type=int, + default=5, + help=( + "suppress aggregate groups smaller than this count (default: 5; aggregate_export only)" + ), + ) + + replay = subparsers.add_parser( + "replay", help="re-evaluate ledger decisions with a reference policy profile" + ) + replay.add_argument("ledger", type=Path) + replay.add_argument( + "--profile", + choices=["quality-first", "balanced", "token-saver", "strict-budget"], + default="balanced", + ) + replay.add_argument("--json", action="store_true", dest="as_json") + subparsers.add_parser("demo", help="run the deterministic bundled benchmark") + + killer = subparsers.add_parser( + "killer-demo", help="run the end-to-end compute allocation demonstration" + ) killer.add_argument( "--output", type=Path, default=Path("killer-demo-output"), help="directory for HTML, Markdown, JSON, SVG, and trace artifacts", ) + + public_eval = subparsers.add_parser( + "public-eval", help="compare matched baseline and MARGINAL public-benchmark runs" + ) + public_eval.add_argument("baseline", type=Path) + public_eval.add_argument("marginal", type=Path) + public_eval.add_argument("--json", action="store_true", dest="as_json") + public_eval.add_argument("--bootstrap-samples", type=int, default=2_000) + public_eval.add_argument("--confidence-level", type=float, default=0.95) + public_eval.add_argument("--quality-margin-pp", type=float, default=1.0) + public_eval.add_argument("--seed", type=int, default=42) return parser @@ -92,6 +136,63 @@ def main(argv: Sequence[str] | None = None) -> int: parser = _build_parser() args = parser.parse_args(argv) + if args.command == "ledger-export": + from .ledger import export_decision_ledger + + try: + exported = export_decision_ledger( + args.source, + args.destination, + privacy_profile=args.privacy_profile, + privacy_key_path=args.privacy_key_file, + minimum_group_size=args.minimum_group_size, + ) + except (OSError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + print(f"exported {exported} records to {args.destination} with {args.privacy_profile}") + return 0 + + if args.command in {"ledger-report", "ledger-validate"}: + from .ledger import read_decision_ledger, summarize_decision_ledger + + try: + records = read_decision_ledger(args.ledger) + except (OSError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.command == "ledger-validate": + print(f"valid decision ledger: {len(records)} events") + return 0 + summary = summarize_decision_ledger(records) + if args.as_json: + print(json.dumps(summary, sort_keys=True)) + else: + print("MARGINAL decision ledger report") + print(f"Events: {summary['events']}") + print(f"Authorizations: {summary['authorizations']}") + print(f"Recommended allowed: {summary['recommended_allowed']}") + print(f"Applied allowed: {summary['applied_allowed']}") + print(f"Non-blocking overrides: {summary['nonblocking_overrides']}") + print(f"Outcomes: {summary['outcomes']}") + print(f"Privacy profiles: {', '.join(summary['privacy_profiles'])}") + return 0 + + if args.command == "replay": + from .profiles import build_policy + from .replay import render_replay_report, replay_ledger + + try: + replay_result = replay_ledger(args.ledger, build_policy(args.profile)) + except (OSError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + if args.as_json: + print(json.dumps(replay_result.to_dict(), sort_keys=True)) + else: + print(render_replay_report(replay_result), end="") + return 0 + if args.command == "demo": from .benchmark import render_markdown, run_benchmark @@ -102,34 +203,39 @@ def main(argv: Sequence[str] | None = None) -> int: from .public_eval import compare_runs, load_runs, render_public_report try: - result = compare_runs( + report = compare_runs( load_runs(args.baseline), load_runs(args.marginal), bootstrap_samples=args.bootstrap_samples, + confidence_level=args.confidence_level, + quality_margin_pp=args.quality_margin_pp, + seed=args.seed, ) except (OSError, ValueError) as exc: print(str(exc), file=sys.stderr) return 1 if args.as_json: - print(json.dumps(result, sort_keys=True)) + print(json.dumps(report, sort_keys=True)) else: - print(render_public_report(result), end="") + print(render_public_report(report), end="") return 0 if args.command == "killer-demo": from .killer_demo import run_killer_demo - result = run_killer_demo(args.output) - savings = result["savings"] - print("MARGINAL Killer Demo") - token_summary = ( - f"Declared tokens: {result['baseline']['tokens']:,} → " - f"{result['marginal']['tokens']:,} " - f"({savings['tokens_percent']:.2f}% fewer)" - ) - print(token_summary) - print("Verified outcome: preserved") - print(f"Artifacts: {args.output.resolve()}") + demo_result = run_killer_demo(args.output) + if "savings" in demo_result: + savings = demo_result["savings"] + print("MARGINAL Killer Demo") + print( + f"Declared tokens: {demo_result['baseline']['tokens']:,} -> " + f"{demo_result['marginal']['tokens']:,} " + f"({savings['tokens_percent']:.2f}% fewer)" + ) + print("Verified outcome: preserved") + print(f"Artifacts: {args.output.resolve()}") + else: + print("MARGINAL Killer Demo completed") return 0 try: diff --git a/src/marginal/estimator.py b/src/marginal/estimator.py index 082597a..fb5cbca 100644 --- a/src/marginal/estimator.py +++ b/src/marginal/estimator.py @@ -1,37 +1,247 @@ -"""Simple calibrated estimates for an action's expected success gain.""" +"""Transparent, versioned estimates for an action's expected success gain.""" from __future__ import annotations +import hashlib +import json import math +import statistics from collections import defaultdict +from dataclasses import dataclass +from typing import Any from .models import Action +def _stable_hash(payload: Any) -> str: + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + return hashlib.sha256(encoded.encode("utf-8")).hexdigest() + + +@dataclass(frozen=True, slots=True) +class EstimatorIdentity: + """Stable identity for an estimator implementation and configuration.""" + + name: str + version: str + config_hash: str + training_data_fingerprint: str | None = None + + def __post_init__(self) -> None: + for field_name in ("name", "version", "config_hash"): + value = getattr(self, field_name) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field_name} must not be empty") + if self.training_data_fingerprint is not None: + if not isinstance(self.training_data_fingerprint, str): + raise TypeError("training_data_fingerprint must be a string or None") + if not self.training_data_fingerprint.strip(): + raise ValueError("training_data_fingerprint must not be empty") + + @property + def key(self) -> tuple[str, str]: + return self.name, self.version + + def to_dict(self) -> dict[str, str | None]: + return { + "name": self.name, + "version": self.version, + "config_hash": self.config_hash, + "training_data_fingerprint": self.training_data_fingerprint, + } + + +@dataclass(frozen=True, slots=True) +class ValueEstimate: + """Expected gain with uncertainty and provenance metadata.""" + + expected_gain: float + uncertainty: float + confidence: float + sample_size: int + provenance: str + estimator: EstimatorIdentity + + def __post_init__(self) -> None: + for name, value in ( + ("expected_gain", self.expected_gain), + ("uncertainty", self.uncertainty), + ("confidence", self.confidence), + ): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise TypeError(f"{name} must be a number") + if not math.isfinite(float(value)): + raise ValueError(f"{name} must be finite") + object.__setattr__(self, name, float(value)) + if not 0.0 <= self.expected_gain <= 1.0: + raise ValueError("expected_gain must be between 0 and 1") + if self.uncertainty < 0: + raise ValueError("uncertainty must be non-negative") + if not 0.0 <= self.confidence <= 1.0: + raise ValueError("confidence must be between 0 and 1") + if isinstance(self.sample_size, bool) or not isinstance(self.sample_size, int): + raise TypeError("sample_size must be an integer") + if self.sample_size < 0: + raise ValueError("sample_size must be non-negative") + if not isinstance(self.provenance, str): + raise TypeError("provenance must be a string") + if not self.provenance.strip(): + raise ValueError("provenance must not be empty") + if not isinstance(self.estimator, EstimatorIdentity): + raise TypeError("estimator must be EstimatorIdentity") + + class ValueEstimator: - """Estimate expected gain from explicit values or observed action history. + """Estimate expected gain from explicit values or contextual observations. - The estimator deliberately stays small and transparent. Applications can replace it - with any object exposing ``estimate(Action) -> float``. + The implementation is deliberately transparent. It does not claim causal attribution: + callers must explicitly provide action-level realized gain through ``observe_action``. """ - def __init__(self, default_gain: float = 0.05) -> None: + def __init__( + self, + default_gain: float = 0.05, + *, + name: str = "historical-mean", + version: str = "2.0.0", + context_fields: tuple[str, ...] = ("engine", "phase", "task_type", "language", "model"), + training_data_fingerprint: str | None = None, + ) -> None: self.default_gain = self._validated_gain(default_gain, name="default_gain") - self._observations: dict[str, list[float]] = defaultdict(list) + if not isinstance(name, str): + raise TypeError("name must be a string") + if not name.strip(): + raise ValueError("name must not be empty") + if not isinstance(version, str): + raise TypeError("version must be a string") + if not version.strip(): + raise ValueError("version must not be empty") + if not isinstance(context_fields, tuple): + raise TypeError("context_fields must be a tuple of strings") + if any(not isinstance(field, str) or not field.strip() for field in context_fields): + raise ValueError("context_fields must contain non-empty strings") + if len(set(context_fields)) != len(context_fields): + raise ValueError("context_fields must be unique") + self.context_fields = context_fields + self._kind_observations: dict[str, list[float]] = defaultdict(list) + self._context_observations: dict[tuple[str, tuple[str, ...]], list[float]] = defaultdict( + list + ) + self._base_training_data_fingerprint = training_data_fingerprint + config_hash = _stable_hash( + {"default_gain": self.default_gain, "context_fields": self.context_fields} + ) + self.identity = EstimatorIdentity( + name=name, + version=version, + config_hash=config_hash, + training_data_fingerprint=training_data_fingerprint, + ) def observe(self, action_kind: str, realized_gain: float) -> None: - if not action_kind.strip(): + if not isinstance(action_kind, str) or not action_kind.strip(): raise ValueError("action_kind must not be empty") + self._kind_observations[action_kind].append( + self._validated_gain(realized_gain, name="realized_gain") + ) + self._refresh_identity() + + def observe_action(self, action: Action, realized_gain: float) -> None: gain = self._validated_gain(realized_gain, name="realized_gain") - self._observations[action_kind].append(gain) + self._kind_observations[action.kind].append(gain) + context = self._context_key(action) + if context is not None: + self._context_observations[(action.kind, context)].append(gain) + self._refresh_identity() def estimate(self, action: Action) -> float: + return self.estimate_detail(action).expected_gain + + def estimate_detail(self, action: Action) -> ValueEstimate: if action.expected_gain is not None: - return action.expected_gain - observations = self._observations.get(action.kind) - if not observations: - return self.default_gain - return sum(observations) / len(observations) + return ValueEstimate( + expected_gain=action.expected_gain, + uncertainty=0.0, + confidence=1.0, + sample_size=0, + provenance="action.expected_gain", + estimator=self.identity, + ) + + context = self._context_key(action) + if context is not None: + observations = self._context_observations.get((action.kind, context)) + if observations: + return self._historical_estimate( + observations, provenance=f"historical:context:{action.kind}" + ) + + observations = self._kind_observations.get(action.kind) + if observations: + return self._historical_estimate( + observations, provenance=f"historical:kind:{action.kind}" + ) + + return ValueEstimate( + expected_gain=self.default_gain, + uncertainty=0.5, + confidence=0.0, + sample_size=0, + provenance="default_gain", + estimator=self.identity, + ) + + def _historical_estimate(self, observations: list[float], *, provenance: str) -> ValueEstimate: + sample_size = len(observations) + mean = statistics.fmean(observations) + uncertainty = ( + statistics.stdev(observations) / math.sqrt(sample_size) if sample_size > 1 else 0.5 + ) + confidence = sample_size / (sample_size + 5.0) + return ValueEstimate( + expected_gain=mean, + uncertainty=uncertainty, + confidence=confidence, + sample_size=sample_size, + provenance=provenance, + estimator=self.identity, + ) + + def _context_key(self, action: Action) -> tuple[str, ...] | None: + values: list[str] = [] + any_value = False + for field in self.context_fields: + value = action.metadata.get(field) + normalized = "" if value is None else str(value) + values.append(normalized) + any_value = any_value or bool(normalized) + return tuple(values) if any_value else None + + def _refresh_identity(self) -> None: + kind_observations = { + kind: sorted(values) for kind, values in sorted(self._kind_observations.items()) + } + context_observations = [ + { + "kind": kind, + "context": list(context), + "values": sorted(values), + } + for (kind, context), values in sorted(self._context_observations.items()) + ] + training_data_fingerprint = _stable_hash( + { + "base": self._base_training_data_fingerprint, + "kind_observations": kind_observations, + "context_observations": context_observations, + } + ) + self.identity = EstimatorIdentity( + name=self.identity.name, + version=self.identity.version, + config_hash=self.identity.config_hash, + training_data_fingerprint=training_data_fingerprint, + ) @staticmethod def _validated_gain(value: float, *, name: str) -> float: diff --git a/src/marginal/killer_demo.py b/src/marginal/killer_demo.py index d688f74..2eab027 100644 --- a/src/marginal/killer_demo.py +++ b/src/marginal/killer_demo.py @@ -570,7 +570,7 @@ def _render_flow_steps( else: state_class = "excess" state_label = "Executed" - state_icon = "×" + state_icon = "x" rows.append( "".join( [ @@ -2224,7 +2224,7 @@ def render_killer_demo_html(result: dict[str, Any]) -> str:

The deterministic defect

{{SCENARIO}}

- − {{DEFECT_BEFORE}} + - {{DEFECT_BEFORE}} + {{DEFECT_AFTER}}
diff --git a/src/marginal/ledger.py b/src/marginal/ledger.py new file mode 100644 index 0000000..002b903 --- /dev/null +++ b/src/marginal/ledger.py @@ -0,0 +1,377 @@ +"""Schema-versioned decision evidence for MARGINAL learning loops.""" + +from __future__ import annotations + +import json +import os +import stat +import threading +import uuid +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, TextIO, cast + +from .outcomes import Outcome +from .privacy import ( + LocalPseudonymizer, + PrivacyProfile, + aggregate_ledger_records, + load_or_create_privacy_key, + sanitize_ledger_record, + validate_safe_telemetry_record, +) + +LEDGER_SCHEMA_VERSION = "2.0" + + +@dataclass(frozen=True, slots=True) +class DecisionLedgerContext: + """Stable correlation fields applied to every ledger event.""" + + run_id: str + task_id: str = "" + trajectory_id: str = "" + engine: str = "" + model: str = "" + + def __post_init__(self) -> None: + if not isinstance(self.run_id, str) or not self.run_id.strip(): + raise ValueError("run_id must not be empty") + for name in ("task_id", "trajectory_id", "engine", "model"): + if not isinstance(getattr(self, name), str): + raise TypeError(f"{name} must be a string") + + def to_dict(self) -> dict[str, str]: + return { + "run_id": self.run_id, + "task_id": self.task_id, + "trajectory_id": self.trajectory_id, + "engine": self.engine, + "model": self.model, + } + + +class JsonlDecisionLedger: + """Append-only JSONL ledger with explicit schema and correlation identity.""" + + _RESERVED_FIELDS = frozenset( + { + "schema_version", + "event_id", + "sequence", + "timestamp", + "run_id", + "task_id", + "trajectory_id", + "engine", + "model", + "privacy_profile", + } + ) + + def __init__( + self, + path: str | Path, + *, + context: DecisionLedgerContext, + privacy_profile: PrivacyProfile | str = PrivacyProfile.LOCAL_FULL, + privacy_key: bytes | None = None, + privacy_key_path: str | Path | None = None, + ) -> None: + self.path = Path(path) + self.path.parent.mkdir(parents=True, exist_ok=True) + _validate_append_target(self.path) + self.context = context + self.privacy_profile = PrivacyProfile.parse(privacy_profile) + if self.privacy_profile is PrivacyProfile.AGGREGATE_EXPORT: + raise ValueError( + "aggregate_export is not an operational ledger profile; use export_decision_ledger" + ) + if privacy_key is not None and privacy_key_path is not None: + raise ValueError("provide privacy_key or privacy_key_path, not both") + self.privacy_key_path: Path | None = None + self._pseudonymizer: LocalPseudonymizer | None = None + if self.privacy_profile is PrivacyProfile.SAFE_TELEMETRY: + if privacy_key is None: + selected_path = ( + Path(privacy_key_path) + if privacy_key_path is not None + else self.path.with_name(f".{self.path.name}.privacy.key") + ) + privacy_key = load_or_create_privacy_key(selected_path) + self.privacy_key_path = selected_path + self._pseudonymizer = LocalPseudonymizer(privacy_key) + elif privacy_key is not None or privacy_key_path is not None: + raise ValueError("privacy keys are only valid with safe_telemetry") + self._lock = threading.Lock() + self._sequence = self._existing_sequence() + + def emit(self, event: Mapping[str, Any]) -> None: + if not isinstance(event, Mapping): + raise TypeError("ledger events must be mappings") + event_name = event.get("event") + if not isinstance(event_name, str) or not event_name.strip(): + raise ValueError("ledger events require a non-empty event name") + reserved = self._RESERVED_FIELDS.intersection(event) + if reserved: + names = ", ".join(sorted(reserved)) + raise ValueError(f"event cannot override reserved ledger fields: {names}") + if event_name == "outcome" and self.context.task_id: + outcome = event.get("outcome") + if not isinstance(outcome, Mapping): + raise ValueError("outcome events require an outcome object") + if outcome.get("task_id") != self.context.task_id: + raise ValueError("outcome task_id does not match the decision ledger context") + with self._lock: + next_sequence = self._sequence + 1 + full_record = { + "schema_version": LEDGER_SCHEMA_VERSION, + "privacy_profile": self.privacy_profile.value, + "event_id": str(uuid.uuid4()), + "sequence": next_sequence, + "timestamp": datetime.now(timezone.utc).isoformat(), + **self.context.to_dict(), + **dict(event), + } + record = sanitize_ledger_record( + full_record, + profile=self.privacy_profile, + pseudonymizer=self._pseudonymizer, + ) + encoded = json.dumps( + record, + sort_keys=True, + ensure_ascii=False, + allow_nan=False, + ) + with _open_owner_only_text(self.path, append=True) as stream: + stream.write(encoded + "\n") + stream.flush() + self._sequence = next_sequence + + def record_outcome(self, outcome: Outcome) -> None: + if not isinstance(outcome, Outcome): + raise TypeError("outcome must be Outcome") + self.emit({"event": "outcome", "outcome": outcome.to_dict()}) + + def _existing_sequence(self) -> int: + if not self.path.exists() or self.path.stat().st_size == 0: + return 0 + records = read_decision_ledger(self.path) + return int(records[-1]["sequence"]) + + +def read_decision_ledger(path: str | Path) -> list[dict[str, Any]]: + """Load and structurally validate a MARGINAL v2 decision ledger.""" + + ledger_path = Path(path) + records: list[dict[str, Any]] = [] + previous_sequence = 0 + with _open_readonly_text(ledger_path) as stream: + for line_number, raw in enumerate(stream, start=1): + if not raw.strip(): + continue + try: + record = json.loads(raw) + except json.JSONDecodeError as exc: + raise ValueError(f"invalid ledger JSON on line {line_number}") from exc + if not isinstance(record, dict): + raise ValueError(f"ledger record on line {line_number} must be an object") + if record.get("schema_version") != LEDGER_SCHEMA_VERSION: + raise ValueError(f"unsupported ledger schema on line {line_number}") + try: + privacy_profile = PrivacyProfile.parse(record.get("privacy_profile", "local_full")) + except (TypeError, ValueError) as exc: + raise ValueError(f"unsupported privacy profile on line {line_number}") from exc + if privacy_profile is PrivacyProfile.SAFE_TELEMETRY: + try: + validate_safe_telemetry_record(record) + except ValueError as exc: + raise ValueError( + f"invalid safe telemetry on line {line_number}: {exc}" + ) from exc + for field_name in ("event_id", "timestamp", "run_id", "event"): + value = record.get(field_name) + if not isinstance(value, str) or not value.strip(): + raise ValueError( + f"ledger {field_name} missing or invalid on line {line_number}" + ) + for field_name in ("task_id", "trajectory_id", "engine", "model"): + value = record.get(field_name, "") + if not isinstance(value, str): + raise ValueError(f"ledger {field_name} must be a string on line {line_number}") + try: + datetime.fromisoformat(record["timestamp"]) + except ValueError as exc: + raise ValueError(f"ledger timestamp invalid on line {line_number}") from exc + sequence = record.get("sequence") + if isinstance(sequence, bool) or not isinstance(sequence, int): + raise ValueError(f"invalid ledger sequence on line {line_number}") + if sequence <= previous_sequence: + raise ValueError( + f"ledger sequence is not strictly increasing on line {line_number}" + ) + previous_sequence = sequence + records.append(record) + if not records: + raise ValueError("decision ledger is empty") + return records + + +def export_decision_ledger( + source: str | Path, + destination: str | Path, + *, + privacy_profile: PrivacyProfile | str, + privacy_key: bytes | None = None, + privacy_key_path: str | Path | None = None, + minimum_group_size: int = 5, +) -> int: + """Export a ledger with strict safe telemetry or grouped aggregate privacy.""" + + selected = PrivacyProfile.parse(privacy_profile) + if selected is PrivacyProfile.LOCAL_FULL: + raise ValueError("ledger export requires safe_telemetry or aggregate_export") + if privacy_key is not None and privacy_key_path is not None: + raise ValueError("provide privacy_key or privacy_key_path, not both") + + source_path = Path(source) + destination_path = Path(destination) + if destination_path.exists(): + raise FileExistsError(f"destination already exists: {destination_path}") + records = read_decision_ledger(source_path) + + if selected is PrivacyProfile.AGGREGATE_EXPORT: + if privacy_key is not None or privacy_key_path is not None: + raise ValueError("aggregate_export does not use a pseudonymization key") + exported = aggregate_ledger_records(records, minimum_group_size=minimum_group_size) + else: + if privacy_key is None: + selected_key_path = ( + Path(privacy_key_path) + if privacy_key_path is not None + else destination_path.with_name(f".{destination_path.name}.privacy.key") + ) + privacy_key = load_or_create_privacy_key(selected_key_path) + pseudonymizer = LocalPseudonymizer(privacy_key) + exported = [ + sanitize_ledger_record( + record, + profile=PrivacyProfile.SAFE_TELEMETRY, + pseudonymizer=pseudonymizer, + ) + for record in records + ] + + destination_path.parent.mkdir(parents=True, exist_ok=True) + created = False + try: + stream = _open_owner_only_text(destination_path, exclusive=True) + created = True + with stream: + for record in exported: + stream.write( + json.dumps( + record, + sort_keys=True, + ensure_ascii=False, + allow_nan=False, + ) + + "\n" + ) + stream.flush() + os.fsync(stream.fileno()) + except Exception: + if created: + destination_path.unlink(missing_ok=True) + raise + return len(exported) + + +def _validate_append_target(path: Path) -> None: + if path.is_symlink(): + raise ValueError("decision ledger path must not be a symbolic link") + if not path.exists(): + return + details = path.stat() + if not stat.S_ISREG(details.st_mode): + raise ValueError("decision ledger path must be a regular file") + if os.name != "nt" and details.st_mode & 0o077: + raise PermissionError("decision ledger file must not be accessible by group or others") + + +def _open_readonly_text(path: Path) -> TextIO: + flags = os.O_RDONLY + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(path, flags) + try: + details = os.fstat(descriptor) + if not stat.S_ISREG(details.st_mode): + raise ValueError("decision ledger path must be a regular file") + return os.fdopen(descriptor, "r", encoding="utf-8") + except Exception: + os.close(descriptor) + raise + + +def _open_owner_only_text(path: Path, *, append: bool = False, exclusive: bool = False) -> TextIO: + flags = os.O_WRONLY | os.O_CREAT + mode = "w" + if append: + flags |= os.O_APPEND + mode = "a" + if exclusive: + flags |= os.O_EXCL + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(path, flags, 0o600) + try: + details = os.fstat(descriptor) + if not stat.S_ISREG(details.st_mode): + raise ValueError("decision ledger path must be a regular file") + if os.name != "nt" and details.st_mode & 0o077: + raise PermissionError("decision ledger file must not be accessible by group or others") + return cast(TextIO, os.fdopen(descriptor, mode, encoding="utf-8")) + except Exception: + os.close(descriptor) + raise + + +def summarize_decision_ledger(records: list[dict[str, Any]]) -> dict[str, Any]: + """Summarize applied and recommended behavior without inferring causality.""" + + authorizations = [record for record in records if record.get("event") == "authorization"] + outcomes = [record for record in records if record.get("event") == "outcome"] + commits = [ + record for record in records if record.get("event") in {"commit", "failure_settlement"} + ] + recommended_allowed = 0 + applied_allowed = 0 + overrides = 0 + for record in authorizations: + decision = record.get("decision", {}) + if decision.get("recommended") is True: + recommended_allowed += 1 + if decision.get("allowed") is True: + applied_allowed += 1 + if decision.get("allowed") is True and decision.get("recommended") is False: + overrides += 1 + last_usage = commits[-1].get("usage", {}) if commits else {} + return { + "schema_version": LEDGER_SCHEMA_VERSION, + "events": len(records), + "authorizations": len(authorizations), + "recommended_allowed": recommended_allowed, + "applied_allowed": applied_allowed, + "nonblocking_overrides": overrides, + "commits": len(commits), + "outcomes": len(outcomes), + "usage": last_usage, + "run_ids": sorted({str(record.get("run_id", "")) for record in records}), + "privacy_profiles": sorted( + {str(record.get("privacy_profile", "local_full")) for record in records} + ), + } diff --git a/src/marginal/models.py b/src/marginal/models.py index d0808e8..cadfd66 100644 --- a/src/marginal/models.py +++ b/src/marginal/models.py @@ -9,6 +9,47 @@ from typing import Any +def _validate_non_negative_int(name: str, value: int) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{name} must be an integer") + if value < 0: + raise ValueError(f"{name} must be non-negative") + return value + + +@dataclass(frozen=True, slots=True) +class TokenUsage: + """Measured token breakdown for one model or agent action. + + ``total_tokens`` is calculated from the component fields when omitted. Cached input is + intentionally represented separately because providers may price it differently. + """ + + input_tokens: int = 0 + cached_input_tokens: int = 0 + output_tokens: int = 0 + reasoning_tokens: int = 0 + total_tokens: int | None = None + + def __post_init__(self) -> None: + components = ( + _validate_non_negative_int("input_tokens", self.input_tokens), + _validate_non_negative_int("cached_input_tokens", self.cached_input_tokens), + _validate_non_negative_int("output_tokens", self.output_tokens), + _validate_non_negative_int("reasoning_tokens", self.reasoning_tokens), + ) + calculated = sum(components) + if self.total_tokens is None: + object.__setattr__(self, "total_tokens", calculated) + return + total = _validate_non_negative_int("total_tokens", self.total_tokens) + if total != calculated: + raise ValueError( + "total_tokens must equal input_tokens + cached_input_tokens + " + "output_tokens + reasoning_tokens" + ) + + @dataclass(frozen=True, slots=True) class Cost: """Estimated or actual resource cost for one agent action.""" @@ -19,16 +60,14 @@ class Cost: risk: float = 0.0 def __post_init__(self) -> None: - if isinstance(self.tokens, bool) or not isinstance(self.tokens, int): - raise TypeError("tokens must be an integer") - if isinstance(self.latency_ms, bool) or not isinstance(self.latency_ms, int): - raise TypeError("latency_ms must be an integer") + _validate_non_negative_int("tokens", self.tokens) + _validate_non_negative_int("latency_ms", self.latency_ms) for name, value in (("usd", self.usd), ("risk", self.risk)): if isinstance(value, bool) or not isinstance(value, (int, float)): raise TypeError(f"{name} must be a number") if not math.isfinite(float(value)): raise ValueError("cost values must be finite") - if self.tokens < 0 or self.latency_ms < 0 or self.usd < 0 or self.risk < 0: + if self.usd < 0 or self.risk < 0: raise ValueError("cost values must be non-negative") object.__setattr__(self, "usd", float(self.usd)) object.__setattr__(self, "risk", float(self.risk)) @@ -103,13 +142,57 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True) class Decision: - """An explainable authorization decision.""" + """An explainable applied decision and its underlying recommendation. + + ``allowed`` describes the behavior applied by the current execution mode. + ``recommended`` describes the policy recommendation before shadow/recommend overrides. + Existing v0.1 callers can continue to inspect only ``allowed`` and ``reason``. + """ allowed: bool reason: str score: float = 0.0 expected_gain: float = 0.0 estimated_cost_value: float = 0.0 + recommended: bool | None = None + recommendation_reason: str | None = None + reason_code: str = "UNSPECIFIED" + recommendation_reason_code: str | None = None + mode: str = "enforce" + uncertainty: float = 0.0 + confidence: float = 0.0 + estimator_name: str = "" + estimator_version: str = "" + + def __post_init__(self) -> None: + if not isinstance(self.allowed, bool): + raise TypeError("allowed must be a boolean") + if not isinstance(self.reason, str) or not self.reason.strip(): + raise ValueError("reason must not be empty") + if self.recommended is None: + object.__setattr__(self, "recommended", self.allowed) + elif not isinstance(self.recommended, bool): + raise TypeError("recommended must be a boolean or None") + if not isinstance(self.reason_code, str) or not self.reason_code.strip(): + raise ValueError("reason_code must not be empty") + for name, value in ( + ("score", self.score), + ("expected_gain", self.expected_gain), + ("estimated_cost_value", self.estimated_cost_value), + ("uncertainty", self.uncertainty), + ("confidence", self.confidence), + ): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise TypeError(f"{name} must be a number") + if not math.isfinite(float(value)): + raise ValueError(f"{name} must be finite") + object.__setattr__(self, name, float(value)) + if not 0.0 <= self.expected_gain <= 1.0: + raise ValueError("expected_gain must be between 0 and 1") + if self.uncertainty < 0: + raise ValueError("uncertainty must be non-negative") + if not 0.0 <= self.confidence <= 1.0: + raise ValueError("confidence must be between 0 and 1") @dataclass(frozen=True, slots=True) diff --git a/src/marginal/modes.py b/src/marginal/modes.py new file mode 100644 index 0000000..ea642b7 --- /dev/null +++ b/src/marginal/modes.py @@ -0,0 +1,30 @@ +"""Execution modes for MARGINAL authorization decisions.""" + +from __future__ import annotations + +from enum import Enum + + +class ExecutionMode(str, Enum): + """Control whether MARGINAL enforces or only records recommendations.""" + + SHADOW = "shadow" + RECOMMEND = "recommend" + ENFORCE = "enforce" + + @classmethod + def parse(cls, value: ExecutionMode | str) -> ExecutionMode: + if isinstance(value, cls): + return value + if not isinstance(value, str): + raise TypeError("execution mode must be a string or ExecutionMode") + normalized = value.strip().lower() + try: + return cls(normalized) + except ValueError as exc: + choices = ", ".join(mode.value for mode in cls) + raise ValueError(f"unknown execution mode {value!r}; choose one of: {choices}") from exc + + @property + def is_blocking(self) -> bool: + return self is self.ENFORCE diff --git a/src/marginal/outcomes.py b/src/marginal/outcomes.py new file mode 100644 index 0000000..1a3adea --- /dev/null +++ b/src/marginal/outcomes.py @@ -0,0 +1,63 @@ +"""Provider-neutral contracts for verified task outcomes.""" + +from __future__ import annotations + +import math +from collections.abc import Mapping +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import Any + + +@dataclass(frozen=True, slots=True) +class Outcome: + """A measured task outcome, kept separate from action-level realized gain.""" + + task_id: str + reward: float + resolved: bool | None = None + verifier: str = "" + trajectory_id: str = "" + evidence: Mapping[str, Any] = field(default_factory=dict) + metrics: Mapping[str, float | int] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not isinstance(self.task_id, str) or not self.task_id.strip(): + raise ValueError("task_id must not be empty") + if isinstance(self.reward, bool) or not isinstance(self.reward, (int, float)): + raise TypeError("reward must be a number") + reward = float(self.reward) + if not math.isfinite(reward): + raise ValueError("reward must be finite") + if self.resolved is not None and not isinstance(self.resolved, bool): + raise TypeError("resolved must be a boolean or None") + for name in ("verifier", "trajectory_id"): + if not isinstance(getattr(self, name), str): + raise TypeError(f"{name} must be a string") + if not isinstance(self.evidence, Mapping): + raise TypeError("evidence must be a mapping") + if not isinstance(self.metrics, Mapping): + raise TypeError("metrics must be a mapping") + normalized_metrics: dict[str, float | int] = {} + for name, value in self.metrics.items(): + if not isinstance(name, str) or not name: + raise ValueError("metric names must be non-empty strings") + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise TypeError("metric values must be numbers") + if not math.isfinite(float(value)): + raise ValueError("metric values must be finite") + normalized_metrics[name] = value + object.__setattr__(self, "reward", reward) + object.__setattr__(self, "evidence", MappingProxyType(dict(self.evidence))) + object.__setattr__(self, "metrics", MappingProxyType(normalized_metrics)) + + def to_dict(self) -> dict[str, Any]: + return { + "task_id": self.task_id, + "reward": self.reward, + "resolved": self.resolved, + "verifier": self.verifier, + "trajectory_id": self.trajectory_id, + "evidence": dict(self.evidence), + "metrics": dict(self.metrics), + } diff --git a/src/marginal/policy.py b/src/marginal/policy.py index 3614f35..009dbf3 100644 --- a/src/marginal/policy.py +++ b/src/marginal/policy.py @@ -2,14 +2,32 @@ from __future__ import annotations +import hashlib +import json import math -from dataclasses import dataclass +from dataclasses import asdict, dataclass from .budget import BudgetLedger -from .estimator import ValueEstimator +from .estimator import EstimatorIdentity, ValueEstimate, ValueEstimator from .models import Action, Decision +@dataclass(frozen=True, slots=True) +class PolicyIdentity: + name: str + version: str + config_hash: str + + def __post_init__(self) -> None: + for field_name in ("name", "version", "config_hash"): + value = getattr(self, field_name) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field_name} must not be empty") + + def to_dict(self) -> dict[str, str]: + return asdict(self) + + @dataclass(frozen=True, slots=True) class PolicyConfig: """Economic assumptions used to score proposed actions.""" @@ -25,14 +43,8 @@ class PolicyConfig: def __post_init__(self) -> None: values = ( ("outcome_value_usd", self.outcome_value_usd), - ( - "token_shadow_price_per_million_usd", - self.token_shadow_price_per_million_usd, - ), - ( - "latency_shadow_price_per_second_usd", - self.latency_shadow_price_per_second_usd, - ), + ("token_shadow_price_per_million_usd", self.token_shadow_price_per_million_usd), + ("latency_shadow_price_per_second_usd", self.latency_shadow_price_per_second_usd), ("risk_shadow_price_usd", self.risk_shadow_price_usd), ("minimum_roi", self.minimum_roi), ("minimum_expected_gain", self.minimum_expected_gain), @@ -44,9 +56,7 @@ def __post_init__(self) -> None: if not math.isfinite(float(value)): raise ValueError("policy values must be finite") object.__setattr__(self, name, float(value)) - - non_negative = values[:-1] - if any(float(value) < 0 for _, value in non_negative): + if any(float(value) < 0 for _, value in values[:-1]): raise ValueError("policy values must be non-negative") if not 0.0 <= self.minimum_expected_gain <= 1.0: raise ValueError("minimum_expected_gain must be between 0 and 1") @@ -61,9 +71,28 @@ def __init__( self, config: PolicyConfig | None = None, estimator: ValueEstimator | None = None, + *, + name: str = "marginal-reference", + version: str = "2.0.0", ) -> None: self.config = config or PolicyConfig() self.estimator = estimator or ValueEstimator() + if not isinstance(name, str): + raise TypeError("name must be a string") + if not name.strip(): + raise ValueError("name must not be empty") + if not isinstance(version, str): + raise TypeError("version must be a string") + if not version.strip(): + raise ValueError("version must not be empty") + config_payload = json.dumps( + asdict(self.config), sort_keys=True, separators=(",", ":") + ).encode("utf-8") + self.identity = PolicyIdentity( + name=name, + version=version, + config_hash=hashlib.sha256(config_payload).hexdigest(), + ) self._executed_fingerprints: set[str] = set() def mark_executed(self, fingerprint: str) -> None: @@ -72,48 +101,117 @@ def mark_executed(self, fingerprint: str) -> None: def evaluate(self, action: Action, ledger: BudgetLedger) -> Decision: if action.current_success_probability >= self.config.target_success_probability: - return Decision(False, "rejected: target success probability already reached") - + return self._decision( + False, + "rejected: target success probability already reached", + "TARGET_REACHED", + ) if action.fingerprint and action.fingerprint in self._executed_fingerprints: - return Decision(False, "rejected: duplicate action") + return self._decision(False, "rejected: duplicate action", "DUPLICATE_ACTION") affordability = ledger.can_afford(action) if not affordability.allowed: - return Decision(False, f"rejected: {affordability.reason}") + return self._decision( + False, + f"rejected: {affordability.reason}", + "BUDGET_REJECTED", + ) - estimated_gain = self.estimator.estimate(action) + estimate = self._estimate(action) remaining_probability = max( 0.0, self.config.target_success_probability - action.current_success_probability, ) - expected_gain = min(estimated_gain, remaining_probability) + expected_gain = min(estimate.expected_gain, remaining_probability) if expected_gain < self.config.minimum_expected_gain: - return Decision( + return self._decision( False, "rejected: expected gain below minimum", + "EXPECTED_GAIN_REJECTED", expected_gain=expected_gain, + estimate=estimate, ) cost_value = self._cost_value(action) expected_value = expected_gain * self.config.outcome_value_usd score = expected_value - cost_value roi = float("inf") if cost_value == 0 else expected_value / cost_value - if score < 0 or roi < self.config.minimum_roi: - return Decision( + return self._decision( False, f"rejected: marginal ROI {roi:.3f} below {self.config.minimum_roi:.3f}", + "MARGINAL_ROI_REJECTED", score=score, expected_gain=expected_gain, estimated_cost_value=cost_value, + estimate=estimate, ) - - return Decision( + return self._decision( True, f"approved: marginal ROI {roi:.3f}", + "APPROVED", score=score, expected_gain=expected_gain, estimated_cost_value=cost_value, + estimate=estimate, + ) + + @property + def estimator_identity(self) -> EstimatorIdentity: + identity = getattr(self.estimator, "identity", None) + if isinstance(identity, EstimatorIdentity): + return identity + estimator_type = type(self.estimator) + return EstimatorIdentity( + name=f"{estimator_type.__module__}.{estimator_type.__qualname__}", + version="unversioned", + config_hash="unversioned", + ) + + def _estimate(self, action: Action) -> ValueEstimate: + detailed = getattr(self.estimator, "estimate_detail", None) + if callable(detailed): + result = detailed(action) + if not isinstance(result, ValueEstimate): + raise TypeError("estimate_detail must return ValueEstimate") + return result + value = self.estimator.estimate(action) + return ValueEstimate( + expected_gain=value, + uncertainty=0.0, + confidence=0.0, + sample_size=0, + provenance="legacy-estimator", + estimator=self.estimator_identity, + ) + + def _decision( + self, + allowed: bool, + reason: str, + reason_code: str, + *, + score: float = 0.0, + expected_gain: float = 0.0, + estimated_cost_value: float = 0.0, + estimate: ValueEstimate | None = None, + ) -> Decision: + return Decision( + allowed=allowed, + reason=reason, + score=score, + expected_gain=expected_gain, + estimated_cost_value=estimated_cost_value, + recommended=allowed, + recommendation_reason=reason, + reason_code=reason_code, + recommendation_reason_code=reason_code, + uncertainty=estimate.uncertainty if estimate else 0.0, + confidence=estimate.confidence if estimate else 0.0, + estimator_name=estimate.estimator.name if estimate else self.estimator_identity.name, + estimator_version=( + estimate.estimator.version if estimate else self.estimator_identity.version + ), ) def _cost_value(self, action: Action) -> float: diff --git a/src/marginal/privacy.py b/src/marginal/privacy.py new file mode 100644 index 0000000..963382f --- /dev/null +++ b/src/marginal/privacy.py @@ -0,0 +1,804 @@ +"""Privacy controls for MARGINAL decision evidence and telemetry exports.""" + +from __future__ import annotations + +import copy +import errno +import hashlib +import hmac +import math +import os +import re +import secrets +import stat +from collections import Counter +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import Enum +from pathlib import Path +from types import MappingProxyType +from typing import Any, Protocol + + +class PrivacyProfile(str, Enum): + """Privacy posture applied to operational ledgers or exported evidence.""" + + LOCAL_FULL = "local_full" + SAFE_TELEMETRY = "safe_telemetry" + AGGREGATE_EXPORT = "aggregate_export" + + @classmethod + def parse(cls, value: PrivacyProfile | str) -> PrivacyProfile: + if isinstance(value, cls): + return value + if not isinstance(value, str): + raise TypeError("privacy profile must be a string or PrivacyProfile") + normalized = value.strip().lower().replace("-", "_") + try: + return cls(normalized) + except ValueError as exc: + raise ValueError(f"unknown privacy profile: {value}") from exc + + +class PrivacyClass(str, Enum): + """Classification assigned to fields before they are persisted or exported.""" + + SAFE_BY_DEFAULT = "safe_by_default" + PSEUDONYMOUS = "pseudonymous" + POTENTIALLY_SENSITIVE = "potentially_sensitive" + + +FIELD_CLASSIFICATION: Mapping[str, PrivacyClass] = MappingProxyType( + { + "schema_version": PrivacyClass.SAFE_BY_DEFAULT, + "privacy_profile": PrivacyClass.SAFE_BY_DEFAULT, + "event": PrivacyClass.SAFE_BY_DEFAULT, + "sequence": PrivacyClass.SAFE_BY_DEFAULT, + "engine": PrivacyClass.SAFE_BY_DEFAULT, + "mode": PrivacyClass.SAFE_BY_DEFAULT, + "action.kind": PrivacyClass.SAFE_BY_DEFAULT, + "action.cost": PrivacyClass.SAFE_BY_DEFAULT, + "action.estimated_cost": PrivacyClass.SAFE_BY_DEFAULT, + "action.token_usage": PrivacyClass.SAFE_BY_DEFAULT, + "action.expected_gain": PrivacyClass.SAFE_BY_DEFAULT, + "action.current_success_probability": PrivacyClass.SAFE_BY_DEFAULT, + "action.is_verification": PrivacyClass.SAFE_BY_DEFAULT, + "action.retry_number": PrivacyClass.SAFE_BY_DEFAULT, + "action.deduplication_scope": PrivacyClass.SAFE_BY_DEFAULT, + "decision.allowed": PrivacyClass.SAFE_BY_DEFAULT, + "decision.recommended": PrivacyClass.SAFE_BY_DEFAULT, + "decision.reason_code": PrivacyClass.SAFE_BY_DEFAULT, + "decision.recommendation_reason_code": PrivacyClass.SAFE_BY_DEFAULT, + "decision.confidence": PrivacyClass.SAFE_BY_DEFAULT, + "decision.uncertainty": PrivacyClass.SAFE_BY_DEFAULT, + "decision.score": PrivacyClass.SAFE_BY_DEFAULT, + "decision.expected_gain": PrivacyClass.SAFE_BY_DEFAULT, + "decision.estimated_cost_value": PrivacyClass.SAFE_BY_DEFAULT, + "decision.mode": PrivacyClass.SAFE_BY_DEFAULT, + "decision.directive": PrivacyClass.SAFE_BY_DEFAULT, + "decision.recommended_directive": PrivacyClass.SAFE_BY_DEFAULT, + "decision.estimator_version": PrivacyClass.SAFE_BY_DEFAULT, + "usage": PrivacyClass.SAFE_BY_DEFAULT, + "reserved": PrivacyClass.SAFE_BY_DEFAULT, + "budget_overrun": PrivacyClass.SAFE_BY_DEFAULT, + "realized_gain": PrivacyClass.SAFE_BY_DEFAULT, + "outcome.reward": PrivacyClass.SAFE_BY_DEFAULT, + "outcome.resolved": PrivacyClass.SAFE_BY_DEFAULT, + "policy.version": PrivacyClass.SAFE_BY_DEFAULT, + "estimator.version": PrivacyClass.SAFE_BY_DEFAULT, + "event_id": PrivacyClass.PSEUDONYMOUS, + "run_id": PrivacyClass.PSEUDONYMOUS, + "task_id": PrivacyClass.PSEUDONYMOUS, + "trajectory_id": PrivacyClass.PSEUDONYMOUS, + "action_id": PrivacyClass.PSEUDONYMOUS, + "action.fingerprint": PrivacyClass.PSEUDONYMOUS, + "action.action_id": PrivacyClass.PSEUDONYMOUS, + "action.state_hash": PrivacyClass.PSEUDONYMOUS, + "outcome.task_id": PrivacyClass.PSEUDONYMOUS, + "outcome.trajectory_id": PrivacyClass.PSEUDONYMOUS, + "engine_instance": PrivacyClass.PSEUDONYMOUS, + "timestamp": PrivacyClass.PSEUDONYMOUS, + "action.name": PrivacyClass.POTENTIALLY_SENSITIVE, + "action.metadata": PrivacyClass.POTENTIALLY_SENSITIVE, + "action.tags": PrivacyClass.POTENTIALLY_SENSITIVE, + "action.tool_arguments": PrivacyClass.POTENTIALLY_SENSITIVE, + "model": PrivacyClass.POTENTIALLY_SENSITIVE, + "metadata": PrivacyClass.POTENTIALLY_SENSITIVE, + "tags": PrivacyClass.POTENTIALLY_SENSITIVE, + "reason": PrivacyClass.POTENTIALLY_SENSITIVE, + "error": PrivacyClass.POTENTIALLY_SENSITIVE, + "exception": PrivacyClass.POTENTIALLY_SENSITIVE, + "outcome.verifier": PrivacyClass.POTENTIALLY_SENSITIVE, + "outcome.evidence": PrivacyClass.POTENTIALLY_SENSITIVE, + "outcome.metrics": PrivacyClass.POTENTIALLY_SENSITIVE, + "policy.name": PrivacyClass.POTENTIALLY_SENSITIVE, + "policy.config_hash": PrivacyClass.POTENTIALLY_SENSITIVE, + "estimator.name": PrivacyClass.POTENTIALLY_SENSITIVE, + "estimator.config_hash": PrivacyClass.POTENTIALLY_SENSITIVE, + "estimator.training_data_fingerprint": PrivacyClass.POTENTIALLY_SENSITIVE, + "treasury": PrivacyClass.POTENTIALLY_SENSITIVE, + "tool_arguments": PrivacyClass.POTENTIALLY_SENSITIVE, + } +) + + +_KNOWN_ACTION_KINDS = frozenset( + { + "command", + "file_read", + "file_write", + "generation", + "llm", + "model_call", + "reasoning", + "research", + "review", + "search", + "subagent", + "test", + "tool", + "verification", + } +) + +_KNOWN_REASON_CODES = frozenset( + { + "APPROVED", + "BUDGET_REJECTED", + "DENY", + "DUPLICATE_ACTION", + "DUPLICATE_PENDING", + "EXPECTED_GAIN_REJECTED", + "FUNDED", + "MARGINAL_ROI_REJECTED", + "PARENT_BUDGET_REJECTED", + "RECOMMEND_OVERRIDE", + "SHADOW_OVERRIDE", + "TARGET_REACHED", + "UNSPECIFIED", + } +) + + +def classify_field(field_path: str) -> PrivacyClass: + """Classify a dotted field path, defaulting unknown fields to sensitive.""" + + if not isinstance(field_path, str): + raise TypeError("field_path must be a string") + normalized = field_path.strip() + if not normalized: + raise ValueError("field_path must not be empty") + candidates = [normalized] + candidate_prefix = "candidates[]." + if normalized.startswith(candidate_prefix): + candidates.append(normalized[len(candidate_prefix) :]) + for candidate in candidates: + current = candidate + while current: + classification = FIELD_CLASSIFICATION.get(current) + if classification is not None: + return classification + if "." not in current: + break + current = current.rsplit(".", 1)[0] + return PrivacyClass.POTENTIALLY_SENSITIVE + + +@dataclass(frozen=True, slots=True) +class PrivacyConfig: + """Validated configuration for privacy-aware ledger persistence or export.""" + + profile: PrivacyProfile | str = PrivacyProfile.LOCAL_FULL + key: bytes | None = field(default=None, repr=False) + key_path: str | Path | None = None + + def __post_init__(self) -> None: + profile = PrivacyProfile.parse(self.profile) + object.__setattr__(self, "profile", profile) + if self.key is not None: + _validate_key(self.key) + if self.key_path is not None: + object.__setattr__(self, "key_path", Path(self.key_path)) + if self.key is not None and self.key_path is not None: + raise ValueError("provide privacy key or privacy key path, not both") + if profile is PrivacyProfile.AGGREGATE_EXPORT and ( + self.key is not None or self.key_path is not None + ): + raise ValueError("aggregate_export does not use a pseudonymization key") + + +class _Pseudonymizer(Protocol): + def pseudonymize(self, field_name: str, value: str) -> str: ... + + +class LocalPseudonymizer: + """Create stable, field-separated pseudonyms with a caller-controlled local key.""" + + _DOMAIN = b"marginal-privacy-v1\x00" + + def __init__(self, key: bytes) -> None: + self._key = _validate_key(key) + + def pseudonymize(self, field_name: str, value: str) -> str: + if not isinstance(field_name, str) or not field_name.strip(): + raise ValueError("field_name must not be empty") + if not isinstance(value, str): + raise TypeError("pseudonymized values must be strings") + if not value: + return "" + payload = self._DOMAIN + field_name.encode("utf-8") + b"\x00" + value.encode("utf-8") + digest = hmac.new(self._key, payload, hashlib.sha256).hexdigest() + return f"psn_{digest[:32]}" + + +def generate_local_identifier(namespace: str = "id") -> str: + """Return a random local identifier that carries no caller-provided meaning.""" + + if not isinstance(namespace, str): + raise TypeError("namespace must be a string") + normalized = namespace.strip().lower() + if not re.fullmatch(r"[a-z][a-z0-9_-]{0,31}", normalized): + raise ValueError("namespace must be a simple lowercase identifier") + return f"{normalized}_{secrets.token_urlsafe(18)}" + + +def load_or_create_privacy_key(path: str | Path) -> bytes: + """Load a local 256-bit key or atomically create one with owner-only permissions.""" + + key_path = Path(path) + key_path.parent.mkdir(parents=True, exist_ok=True) + if key_path.is_symlink(): + raise ValueError("privacy key path must not be a symbolic link") + if key_path.exists(): + return _read_existing_key(key_path) + + key = secrets.token_bytes(32) + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + try: + descriptor = os.open(key_path, flags, 0o600) + except FileExistsError: + return _read_existing_key(key_path) + try: + with os.fdopen(descriptor, "wb") as stream: + stream.write(key) + stream.flush() + os.fsync(stream.fileno()) + except Exception: + key_path.unlink(missing_ok=True) + raise + return key + + +def sanitize_ledger_record( + record: Mapping[str, Any], + *, + profile: PrivacyProfile | str, + pseudonymizer: _Pseudonymizer | None = None, +) -> dict[str, Any]: + """Apply a privacy profile to one JSON-compatible Decision Ledger record.""" + + if not isinstance(record, Mapping): + raise TypeError("ledger record must be a mapping") + selected = PrivacyProfile.parse(profile) + if selected is PrivacyProfile.LOCAL_FULL: + return copy.deepcopy(dict(record)) + if selected is PrivacyProfile.AGGREGATE_EXPORT: + raise ValueError("aggregate_export requires aggregate_ledger_records") + if pseudonymizer is None: + raise ValueError("safe_telemetry requires a pseudonymizer") + + sanitized: dict[str, Any] = { + "schema_version": "2.0", + "privacy_profile": PrivacyProfile.SAFE_TELEMETRY.value, + } + sequence = record.get("sequence") + if not isinstance(sequence, bool) and isinstance(sequence, int): + sanitized["sequence"] = sequence + event = record.get("event") + if isinstance(event, str): + sanitized["event"] = _generalize_event(event) + mode = record.get("mode") + if isinstance(mode, str): + sanitized["mode"] = _generalize_mode(mode) + budget_overrun = record.get("budget_overrun") + if isinstance(budget_overrun, bool): + sanitized["budget_overrun"] = budget_overrun + realized_gain = record.get("realized_gain") + realized_gain_value = _finite_float(realized_gain) + if realized_gain_value is not None and 0.0 <= realized_gain_value <= 1.0: + sanitized["realized_gain"] = realized_gain_value + + for name in ( + "event_id", + "run_id", + "task_id", + "trajectory_id", + "action_id", + "engine_instance", + ): + value = record.get(name) + if isinstance(value, str): + sanitized[name] = pseudonymizer.pseudonymize(name, value) + + timestamp = record.get("timestamp") + if isinstance(timestamp, str) and timestamp: + sanitized["timestamp"] = _generalize_timestamp(timestamp) + + engine = record.get("engine") + if isinstance(engine, str): + sanitized["engine"] = _generalize_engine(engine) + + policy = record.get("policy") + if isinstance(policy, Mapping): + sanitized["policy"] = _sanitize_identity(policy) + estimator = record.get("estimator") + if isinstance(estimator, Mapping): + sanitized["estimator"] = _sanitize_identity(estimator) + + action = record.get("action") + if isinstance(action, Mapping): + sanitized["action"] = _sanitize_action(action, pseudonymizer) + decision = record.get("decision") + if isinstance(decision, Mapping): + sanitized["decision"] = _sanitize_decision(decision) + outcome = record.get("outcome") + if isinstance(outcome, Mapping): + sanitized["outcome"] = _sanitize_outcome(outcome, pseudonymizer) + + candidates = record.get("candidates") + if isinstance(candidates, list): + sanitized["candidates"] = _sanitize_candidates(candidates, pseudonymizer) + + for name in ("usage", "reserved"): + value = record.get(name) + if isinstance(value, Mapping): + sanitized[name] = _sanitize_numeric_mapping(value) + return sanitized + + +class _ExistingPseudonymValidator: + """Validate already-pseudonymized identifiers without changing correlation.""" + + _PATTERN = re.compile(r"^psn_[0-9a-f]{32}$") + + def pseudonymize(self, field_name: str, value: str) -> str: + del field_name + if not isinstance(value, str): + raise TypeError("safe telemetry pseudonyms must be strings") + if value and self._PATTERN.fullmatch(value) is None: + raise ValueError("safe telemetry contains an invalid pseudonym") + return value + + +def validate_safe_telemetry_record(record: Mapping[str, Any]) -> None: + """Validate that a record is the canonical output of the strict privacy profile.""" + + if not isinstance(record, Mapping): + raise TypeError("safe telemetry record must be a mapping") + try: + canonical = sanitize_ledger_record( + record, + profile=PrivacyProfile.SAFE_TELEMETRY, + pseudonymizer=_ExistingPseudonymValidator(), + ) + except (TypeError, ValueError) as exc: + raise ValueError(f"invalid safe telemetry record: {exc}") from exc + if canonical != dict(record): + raise ValueError( + "invalid safe telemetry record: unreviewed, noncanonical, or sensitive fields" + ) + + +def aggregate_ledger_records( + records: Iterable[Mapping[str, Any]], + *, + minimum_group_size: int = 5, +) -> list[dict[str, Any]]: + """Group generalized rows and suppress groups smaller than the privacy threshold.""" + + if isinstance(minimum_group_size, bool) or not isinstance(minimum_group_size, int): + raise TypeError("minimum_group_size must be an integer") + if minimum_group_size < 1: + raise ValueError("minimum_group_size must be at least 1") + + counter: Counter[tuple[str, str, str, str, str, str, str, str]] = Counter() + for record in records: + if not isinstance(record, Mapping): + raise TypeError("aggregate source records must be mappings") + event = record.get("event") + if event == "authorization": + action = record.get("action", {}) + decision = record.get("decision", {}) + if not isinstance(action, Mapping) or not isinstance(decision, Mapping): + continue + key = ( + "decision", + _generalize_action_kind(action.get("kind")), + _cost_bucket(action.get("cost")), + _gain_bucket(decision.get("expected_gain")), + _boolean_decision(decision.get("recommended")), + _boolean_decision(decision.get("allowed")), + _safe_reason_code(decision.get("reason_code")), + "not_applicable", + ) + counter[key] += 1 + elif event == "outcome": + outcome = record.get("outcome", {}) + if not isinstance(outcome, Mapping): + continue + key = ( + "outcome", + "unknown", + "unknown", + "unknown", + "not_applicable", + "not_applicable", + "not_applicable", + _outcome_class(outcome), + ) + counter[key] += 1 + + order = {"decision": 0, "outcome": 1} + rows: list[dict[str, Any]] = [] + for key, count in sorted(counter.items(), key=lambda item: (order[item[0][0]], item[0])): + if count < minimum_group_size: + continue + ( + record_type, + action_kind, + cost_bucket, + gain_bucket, + recommendation, + applied_decision, + reason_code, + outcome_class, + ) = key + rows.append( + { + "schema_version": "1.0", + "privacy_profile": PrivacyProfile.AGGREGATE_EXPORT.value, + "record_type": record_type, + "action_kind": action_kind, + "cost_bucket": cost_bucket, + "gain_bucket": gain_bucket, + "recommendation": recommendation, + "applied_decision": applied_decision, + "reason_code": reason_code, + "outcome_class": outcome_class, + "count": count, + "minimum_group_size": minimum_group_size, + } + ) + return rows + + +def _validate_key(key: bytes) -> bytes: + if not isinstance(key, bytes): + raise TypeError("privacy key must be bytes") + if len(key) < 32: + raise ValueError("privacy key must contain at least 32 bytes") + return key + + +def _read_existing_key(path: Path) -> bytes: + flags = os.O_RDONLY + if hasattr(os, "O_BINARY"): + flags |= os.O_BINARY + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + try: + descriptor = os.open(path, flags) + except OSError as exc: + if exc.errno == errno.ELOOP: + raise ValueError("privacy key path must not be a symbolic link") from exc + raise + try: + details = os.fstat(descriptor) + if not stat.S_ISREG(details.st_mode): + raise ValueError("privacy key path must be a regular file") + if os.name != "nt" and details.st_mode & 0o077: + raise PermissionError("privacy key file must not be accessible by group or others") + with os.fdopen(descriptor, "rb", closefd=False) as stream: + material = stream.read() + finally: + os.close(descriptor) + return _validate_key(material) + + +def _generalize_timestamp(value: str) -> str: + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise ValueError("ledger timestamp must be ISO 8601") from exc + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + day = parsed.astimezone(timezone.utc).date() + return f"{day.isoformat()}T00:00:00+00:00" + + +def _generalize_engine(value: str) -> str: + normalized = value.strip().lower().replace("_", "-") + known = { + "aider", + "claude-code", + "cline", + "codex", + "continue", + "gemini-cli", + "github-copilot", + "opencode", + "roo-code", + } + return normalized if normalized in known else "other" + + +def _sanitize_identity(payload: Mapping[str, Any]) -> dict[str, str]: + result: dict[str, str] = {} + version = payload.get("version") + if isinstance(version, str): + result["version"] = _safe_version(version) + return result + + +def _sanitize_action(payload: Mapping[str, Any], pseudonymizer: _Pseudonymizer) -> dict[str, Any]: + result: dict[str, Any] = {} + if "kind" in payload: + result["kind"] = _generalize_action_kind(payload.get("kind")) + for name in ("expected_gain", "current_success_probability"): + value = payload.get(name) + numeric_value = _finite_float(value) + if numeric_value is not None and 0.0 <= numeric_value <= 1.0: + result[name] = numeric_value + is_verification = payload.get("is_verification") + if isinstance(is_verification, bool): + result["is_verification"] = is_verification + retry_number = payload.get("retry_number") + if not isinstance(retry_number, bool) and isinstance(retry_number, int) and retry_number >= 0: + result["retry_number"] = retry_number + deduplication_scope = payload.get("deduplication_scope") + if isinstance(deduplication_scope, str): + result["deduplication_scope"] = _generalize_deduplication_scope(deduplication_scope) + for name in ("cost", "estimated_cost", "token_usage"): + value = payload.get(name) + if isinstance(value, Mapping): + result[name] = _sanitize_numeric_mapping(value) + for name in ("fingerprint", "action_id", "state_hash"): + value = payload.get(name) + if isinstance(value, str): + result[name] = pseudonymizer.pseudonymize(f"action.{name}", value) + return result + + +def _sanitize_decision(payload: Mapping[str, Any]) -> dict[str, Any]: + result: dict[str, Any] = {} + for name in ("allowed", "recommended"): + value = payload.get(name) + if isinstance(value, bool): + result[name] = value + score = payload.get("score") + score_value = _finite_float(score) + if score_value is not None: + result["score"] = score_value + expected_gain = payload.get("expected_gain") + expected_gain_value = _finite_float(expected_gain) + if expected_gain_value is not None and 0.0 <= expected_gain_value <= 1.0: + result["expected_gain"] = expected_gain_value + estimated_cost_value = payload.get("estimated_cost_value") + estimated_cost_value_float = _finite_float(estimated_cost_value) + if estimated_cost_value_float is not None and estimated_cost_value_float >= 0.0: + result["estimated_cost_value"] = estimated_cost_value_float + uncertainty = payload.get("uncertainty") + uncertainty_value = _finite_float(uncertainty) + if uncertainty_value is not None and uncertainty_value >= 0.0: + result["uncertainty"] = uncertainty_value + confidence = payload.get("confidence") + confidence_value = _finite_float(confidence) + if confidence_value is not None and 0.0 <= confidence_value <= 1.0: + result["confidence"] = confidence_value + for name in ("reason_code", "recommendation_reason_code"): + if name in payload: + result[name] = _safe_reason_code(payload.get(name)) + if "mode" in payload: + result["mode"] = _generalize_mode(payload.get("mode")) + for name in ("directive", "recommended_directive"): + if name in payload: + result[name] = _generalize_directive(payload.get(name)) + if "estimator_version" in payload: + result["estimator_version"] = _safe_version(payload.get("estimator_version")) + return result + + +def _sanitize_outcome(payload: Mapping[str, Any], pseudonymizer: _Pseudonymizer) -> dict[str, Any]: + result: dict[str, Any] = {} + reward = payload.get("reward") + if ( + not isinstance(reward, bool) + and isinstance(reward, (int, float)) + and math.isfinite(float(reward)) + ): + result["reward"] = reward + resolved = payload.get("resolved") + if isinstance(resolved, bool) or resolved is None: + result["resolved"] = resolved + for name in ("task_id", "trajectory_id"): + value = payload.get(name) + if isinstance(value, str): + result[name] = pseudonymizer.pseudonymize(name, value) + return result + + +def _sanitize_candidates(payload: list[Any], pseudonymizer: _Pseudonymizer) -> list[dict[str, Any]]: + result: list[dict[str, Any]] = [] + for candidate in payload: + if not isinstance(candidate, Mapping): + continue + item: dict[str, Any] = {} + action = candidate.get("action") + if isinstance(action, Mapping): + item["action"] = _sanitize_action(action, pseudonymizer) + decision = candidate.get("decision") + if isinstance(decision, Mapping): + item["decision"] = _sanitize_decision(decision) + result.append(item) + return result + + +def _sanitize_numeric_mapping(payload: Mapping[str, Any]) -> dict[str, int | float]: + allowed = { + "tokens", + "usd", + "latency_ms", + "risk", + "input_tokens", + "cached_input_tokens", + "output_tokens", + "reasoning_tokens", + "total_tokens", + } + result: dict[str, int | float] = {} + for name, value in payload.items(): + if ( + name in allowed + and not isinstance(value, bool) + and isinstance(value, (int, float)) + and math.isfinite(float(value)) + and float(value) >= 0.0 + ): + result[name] = value + return result + + +def _generalize_action_kind(value: Any) -> str: + if not isinstance(value, str): + return "other" + normalized = value.strip().lower().replace("-", "_").replace(" ", "_") + return normalized if normalized in _KNOWN_ACTION_KINDS else "other" + + +def _generalize_event(value: str) -> str: + normalized = value.strip().lower().replace("-", "_").replace(" ", "_") + known = { + "abort", + "authorization", + "candidate_ranking", + "commit", + "estimator_observation", + "failure_settlement", + "outcome", + "session_end", + "session_start", + } + return normalized if normalized in known else "custom" + + +def _generalize_mode(value: Any) -> str: + if not isinstance(value, str): + return "unknown" + normalized = value.strip().lower().replace("-", "_") + return normalized if normalized in {"shadow", "recommend", "enforce"} else "unknown" + + +def _generalize_directive(value: Any) -> str: + if not isinstance(value, str): + return "unknown" + normalized = value.strip().lower().replace("-", "_") + known = {"allow", "deny", "modify", "defer", "reuse", "stop", "force_verify"} + return normalized if normalized in known else "unknown" + + +def _generalize_deduplication_scope(value: str) -> str: + normalized = value.strip().lower().replace("-", "_") + known = {"exact", "once_per_state", "once_per_phase", "allow_retry"} + return normalized if normalized in known else "unknown" + + +def _safe_version(value: Any) -> str: + if not isinstance(value, str): + return "unknown" + normalized = value.strip() + if normalized in {"unknown", "unversioned"}: + return normalized + if re.fullmatch( + r"v?\d+(?:\.\d+){0,3}(?:[-+][0-9A-Za-z.-]{1,24})?", + normalized, + ): + return normalized + return "unknown" + + +def _safe_reason_code(value: Any) -> str: + if not isinstance(value, str): + return "UNSPECIFIED" + normalized = value.strip().upper().replace("-", "_").replace(" ", "_") + return normalized if normalized in _KNOWN_REASON_CODES else "OTHER" + + +def _cost_bucket(value: Any) -> str: + if not isinstance(value, Mapping): + return "unknown" + tokens = _number(value.get("tokens"), default=0.0) + usd = _number(value.get("usd"), default=0.0) + latency = _number(value.get("latency_ms"), default=0.0) + if tokens <= 2_000 and usd <= 0.02 and latency <= 1_000: + return "low" + if tokens <= 10_000 and usd <= 0.20 and latency <= 10_000: + return "medium" + return "high" + + +def _gain_bucket(value: Any) -> str: + if isinstance(value, bool) or not isinstance(value, (int, float)): + return "unknown" + gain = float(value) + if not math.isfinite(gain): + return "unknown" + if gain < 0.1: + return "low" + if gain < 0.3: + return "medium" + return "high" + + +def _boolean_decision(value: Any) -> str: + if value is True: + return "allow" + if value is False: + return "deny" + return "unknown" + + +def _outcome_class(payload: Mapping[str, Any]) -> str: + resolved = payload.get("resolved") + if resolved is True: + return "verified_success" + if resolved is False: + return "verified_failure" + reward = payload.get("reward") + if not isinstance(reward, bool) and isinstance(reward, (int, float)): + return "positive_reward" if float(reward) > 0 else "non_positive_reward" + return "unknown" + + +def _is_finite_number(value: Any) -> bool: + return ( + not isinstance(value, bool) + and isinstance(value, (int, float)) + and math.isfinite(float(value)) + ) + + +def _finite_float(value: Any) -> float | None: + if isinstance(value, bool) or not isinstance(value, (int, float)): + return None + if not math.isfinite(float(value)): + return None + return float(value) + + +def _number(value: Any, *, default: float) -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)): + return default + return float(value) diff --git a/src/marginal/profiles.py b/src/marginal/profiles.py new file mode 100644 index 0000000..4a652ca --- /dev/null +++ b/src/marginal/profiles.py @@ -0,0 +1,76 @@ +"""Conservative reference policy profiles for common user intents.""" + +from __future__ import annotations + +from enum import Enum + +from .estimator import ValueEstimator +from .policy import MarginalPolicy, PolicyConfig + + +class PolicyProfile(str, Enum): + QUALITY_FIRST = "quality-first" + BALANCED = "balanced" + TOKEN_SAVER = "token-saver" + STRICT_BUDGET = "strict-budget" + + @classmethod + def parse(cls, value: PolicyProfile | str) -> PolicyProfile: + if isinstance(value, cls): + return value + normalized = str(value).strip().lower().replace("_", "-") + try: + return cls(normalized) + except ValueError as exc: + raise ValueError(f"unknown policy profile: {value}") from exc + + +def policy_config_for_profile(profile: PolicyProfile | str) -> PolicyConfig: + """Return transparent reference defaults, not empirically calibrated guarantees.""" + + selected = PolicyProfile.parse(profile) + configs = { + PolicyProfile.QUALITY_FIRST: PolicyConfig( + outcome_value_usd=10.0, + token_shadow_price_per_million_usd=2.0, + minimum_roi=0.5, + minimum_expected_gain=0.005, + target_success_probability=0.99, + ), + PolicyProfile.BALANCED: PolicyConfig( + outcome_value_usd=5.0, + token_shadow_price_per_million_usd=10.0, + minimum_roi=1.0, + minimum_expected_gain=0.01, + target_success_probability=0.95, + ), + PolicyProfile.TOKEN_SAVER: PolicyConfig( + outcome_value_usd=3.0, + token_shadow_price_per_million_usd=25.0, + minimum_roi=1.2, + minimum_expected_gain=0.02, + target_success_probability=0.92, + ), + PolicyProfile.STRICT_BUDGET: PolicyConfig( + outcome_value_usd=2.0, + token_shadow_price_per_million_usd=60.0, + minimum_roi=1.5, + minimum_expected_gain=0.03, + target_success_probability=0.90, + ), + } + return configs[selected] + + +def build_policy( + profile: PolicyProfile | str, + *, + estimator: ValueEstimator | None = None, +) -> MarginalPolicy: + selected = PolicyProfile.parse(profile) + return MarginalPolicy( + policy_config_for_profile(selected), + estimator=estimator, + name=f"profile:{selected.value}", + version="2.0.0", + ) diff --git a/src/marginal/protocol.py b/src/marginal/protocol.py new file mode 100644 index 0000000..4c2ffaf --- /dev/null +++ b/src/marginal/protocol.py @@ -0,0 +1,499 @@ +"""Universal, engine-neutral protocol values for AI development agents.""" + +from __future__ import annotations + +import hashlib +import json +import math +from collections.abc import Mapping +from dataclasses import asdict, dataclass, field +from enum import Enum +from types import MappingProxyType +from typing import Any + +from .models import Action, Cost, Decision, TokenUsage +from .modes import ExecutionMode + +PROTOCOL_VERSION = "1.0" + + +class AgentEventType(str, Enum): + SESSION_START = "session.start" + SESSION_END = "session.end" + ACTION_BEFORE = "action.before" + ACTION_AFTER = "action.after" + ACTION_FAILED = "action.failed" + OUTCOME = "outcome" + + @classmethod + def parse(cls, value: AgentEventType | str) -> AgentEventType: + if isinstance(value, cls): + return value + try: + return cls(str(value)) + except ValueError as exc: + raise ValueError(f"unknown agent event type: {value}") from exc + + +class AgentDirective(str, Enum): + """Action requested from an engine adapter by the universal protocol.""" + + ALLOW = "allow" + DENY = "deny" + MODIFY = "modify" + DEFER = "defer" + REUSE = "reuse" + STOP = "stop" + FORCE_VERIFY = "force_verify" + + @classmethod + def parse(cls, value: AgentDirective | str) -> AgentDirective: + if isinstance(value, cls): + return value + if not isinstance(value, str): + raise TypeError("agent directive must be a string or AgentDirective") + normalized = value.strip().lower().replace("-", "_") + try: + return cls(normalized) + except ValueError as exc: + raise ValueError(f"unknown agent directive: {value}") from exc + + +class DeduplicationScope(str, Enum): + EXACT = "exact" + ONCE_PER_STATE = "once_per_state" + ONCE_PER_PHASE = "once_per_phase" + ALLOW_RETRY = "allow_retry" + + @classmethod + def parse(cls, value: DeduplicationScope | str) -> DeduplicationScope: + if isinstance(value, cls): + return value + normalized = str(value).strip().lower().replace("-", "_") + try: + return cls(normalized) + except ValueError as exc: + raise ValueError(f"unknown deduplication scope: {value}") from exc + + +@dataclass(frozen=True, slots=True) +class AgentCapabilities: + """Capabilities negotiated by an engine adapter.""" + + observe_model_usage: bool = False + block_actions: bool = False + modify_actions: bool = False + stop_agent: bool = False + control_model_turns: bool = False + record_outcomes: bool = False + + def __post_init__(self) -> None: + for name, value in asdict(self).items(): + if not isinstance(value, bool): + raise TypeError(f"{name} must be a boolean") + + @property + def level(self) -> str: + if all(asdict(self).values()): + return "full" + control_capabilities = ( + self.block_actions, + self.modify_actions, + self.stop_agent, + self.control_model_turns, + ) + if any(control_capabilities): + return "control" + return "observe" + + def supports(self, directive: AgentDirective | str) -> bool: + selected = AgentDirective.parse(directive) + if selected is AgentDirective.ALLOW: + return True + if selected is AgentDirective.DENY: + return self.block_actions + if selected is AgentDirective.MODIFY: + return self.modify_actions + if selected is AgentDirective.STOP: + return self.stop_agent + if selected is AgentDirective.FORCE_VERIFY: + return self.block_actions + return self.block_actions + + def to_dict(self) -> dict[str, bool | str]: + return {**asdict(self), "level": self.level} + + @classmethod + def from_dict(cls, payload: Mapping[str, Any]) -> AgentCapabilities: + if not isinstance(payload, Mapping): + raise TypeError("capability payload must be a mapping") + fields = ( + "observe_model_usage", + "block_actions", + "modify_actions", + "stop_agent", + "control_model_turns", + "record_outcomes", + ) + values: dict[str, bool] = {} + for name in fields: + value = payload.get(name, False) + if not isinstance(value, bool): + raise TypeError(f"{name} must be a boolean") + values[name] = value + capabilities = cls( + observe_model_usage=values["observe_model_usage"], + block_actions=values["block_actions"], + modify_actions=values["modify_actions"], + stop_agent=values["stop_agent"], + control_model_turns=values["control_model_turns"], + record_outcomes=values["record_outcomes"], + ) + declared_level = payload.get("level") + if declared_level is not None and declared_level != capabilities.level: + raise ValueError( + f"declared capability level {declared_level!r} does not match " + f"derived level {capabilities.level!r}" + ) + return capabilities + + +@dataclass(frozen=True, slots=True) +class AgentAction: + """Normalized action proposed by an external agent runtime.""" + + action_id: str + name: str + kind: str + estimated_cost: Cost = field(default_factory=Cost) + token_usage: TokenUsage | None = None + expected_gain: float | None = None + current_success_probability: float = 0.0 + is_verification: bool = False + state_hash: str = "" + phase: str = "" + retry_number: int = 0 + deduplication_scope: DeduplicationScope | str = DeduplicationScope.EXACT + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + for name in ("action_id", "name", "kind"): + value = getattr(self, name) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must not be empty") + if not isinstance(self.estimated_cost, Cost): + raise TypeError("estimated_cost must be Cost") + if not isinstance(self.is_verification, bool): + raise TypeError("is_verification must be a boolean") + for field_name in ("state_hash", "phase"): + if not isinstance(getattr(self, field_name), str): + raise TypeError(f"{field_name} must be a string") + if self.token_usage is not None and not isinstance(self.token_usage, TokenUsage): + raise TypeError("token_usage must be TokenUsage or None") + if self.expected_gain is not None: + if isinstance(self.expected_gain, bool) or not isinstance( + self.expected_gain, (int, float) + ): + raise TypeError("expected_gain must be a number") + gain = float(self.expected_gain) + if not math.isfinite(gain) or not 0.0 <= gain <= 1.0: + raise ValueError("expected_gain must be finite and between 0 and 1") + object.__setattr__(self, "expected_gain", gain) + probability = self.current_success_probability + if isinstance(probability, bool) or not isinstance(probability, (int, float)): + raise TypeError("current_success_probability must be a number") + normalized_probability = float(probability) + if not math.isfinite(normalized_probability) or not 0.0 <= normalized_probability <= 1.0: + raise ValueError("current_success_probability must be finite and between 0 and 1") + object.__setattr__(self, "current_success_probability", normalized_probability) + if isinstance(self.retry_number, bool) or not isinstance(self.retry_number, int): + raise TypeError("retry_number must be an integer") + if self.retry_number < 0: + raise ValueError("retry_number must be non-negative") + if not isinstance(self.metadata, Mapping): + raise TypeError("metadata must be a mapping") + object.__setattr__( + self, "deduplication_scope", DeduplicationScope.parse(self.deduplication_scope) + ) + object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata))) + + def core_fingerprint(self) -> str: + payload: dict[str, Any] = { + "name": self.name, + "kind": self.kind, + "is_verification": self.is_verification, + "metadata": dict(self.metadata), + } + scope = DeduplicationScope.parse(self.deduplication_scope) + if scope is DeduplicationScope.ONCE_PER_STATE: + payload["state_hash"] = self.state_hash + elif scope is DeduplicationScope.ONCE_PER_PHASE: + payload["phase"] = self.phase + elif scope is DeduplicationScope.ALLOW_RETRY: + payload["retry_number"] = self.retry_number + try: + canonical = json.dumps( + payload, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + allow_nan=False, + ) + except (TypeError, ValueError) as exc: + raise TypeError("agent action fingerprint metadata must be JSON serializable") from exc + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + def to_core_action(self, *, engine: str) -> Action: + metadata = { + **dict(self.metadata), + "engine": engine, + "phase": self.phase, + "state_hash": self.state_hash, + "retry_number": self.retry_number, + "deduplication_scope": DeduplicationScope.parse(self.deduplication_scope).value, + "agent_action_id": self.action_id, + } + if self.token_usage is not None: + metadata["token_usage"] = asdict(self.token_usage) + return Action( + name=self.name, + kind=self.kind, + cost=self.estimated_cost, + expected_gain=self.expected_gain, + current_success_probability=self.current_success_probability, + is_verification=self.is_verification, + fingerprint=self.core_fingerprint(), + metadata=metadata, + ) + + def to_dict(self) -> dict[str, Any]: + return { + "action_id": self.action_id, + "name": self.name, + "kind": self.kind, + "estimated_cost": asdict(self.estimated_cost), + "token_usage": asdict(self.token_usage) if self.token_usage else None, + "expected_gain": self.expected_gain, + "current_success_probability": self.current_success_probability, + "is_verification": self.is_verification, + "state_hash": self.state_hash, + "phase": self.phase, + "retry_number": self.retry_number, + "deduplication_scope": DeduplicationScope.parse(self.deduplication_scope).value, + "metadata": dict(self.metadata), + } + + @classmethod + def from_dict(cls, payload: Mapping[str, Any]) -> AgentAction: + cost_payload = payload.get("estimated_cost", {}) + token_payload = payload.get("token_usage") + return cls( + action_id=payload["action_id"], + name=payload["name"], + kind=payload["kind"], + estimated_cost=Cost(**dict(cost_payload)), + token_usage=TokenUsage(**dict(token_payload)) if token_payload else None, + expected_gain=payload.get("expected_gain"), + current_success_probability=payload.get("current_success_probability", 0.0), + is_verification=payload.get("is_verification", False), + state_hash=payload.get("state_hash", ""), + phase=payload.get("phase", ""), + retry_number=payload.get("retry_number", 0), + deduplication_scope=payload.get("deduplication_scope", "exact"), + metadata=dict(payload.get("metadata", {})), + ) + + +@dataclass(frozen=True, slots=True) +class AgentDecision: + """Normalized decision returned to an engine adapter.""" + + action_id: str + allowed: bool + recommended: bool + reason: str + reason_code: str + recommendation_reason: str + recommendation_reason_code: str + mode: str + directive: AgentDirective | str = AgentDirective.ALLOW + recommended_directive: AgentDirective | str = AgentDirective.ALLOW + replacement: Mapping[str, Any] = field(default_factory=dict) + score: float = 0.0 + expected_gain: float = 0.0 + estimated_cost_value: float = 0.0 + uncertainty: float = 0.0 + confidence: float = 0.0 + + def __post_init__(self) -> None: + for name in ( + "action_id", + "reason", + "reason_code", + "recommendation_reason", + "recommendation_reason_code", + "mode", + ): + value = getattr(self, name) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must not be empty") + if not isinstance(self.allowed, bool) or not isinstance(self.recommended, bool): + raise TypeError("allowed and recommended must be booleans") + object.__setattr__(self, "mode", ExecutionMode.parse(self.mode).value) + for name, value in ( + ("score", self.score), + ("expected_gain", self.expected_gain), + ("estimated_cost_value", self.estimated_cost_value), + ("uncertainty", self.uncertainty), + ("confidence", self.confidence), + ): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise TypeError(f"{name} must be a number") + if not math.isfinite(float(value)): + raise ValueError(f"{name} must be finite") + object.__setattr__(self, name, float(value)) + if not 0.0 <= self.expected_gain <= 1.0: + raise ValueError("expected_gain must be between 0 and 1") + if self.uncertainty < 0.0: + raise ValueError("uncertainty must be non-negative") + if not 0.0 <= self.confidence <= 1.0: + raise ValueError("confidence must be between 0 and 1") + object.__setattr__(self, "directive", AgentDirective.parse(self.directive)) + object.__setattr__( + self, + "recommended_directive", + AgentDirective.parse(self.recommended_directive), + ) + if not isinstance(self.replacement, Mapping): + raise TypeError("replacement must be a mapping") + object.__setattr__(self, "replacement", MappingProxyType(dict(self.replacement))) + + @classmethod + def from_core(cls, action_id: str, decision: Decision) -> AgentDecision: + return cls( + action_id=action_id, + allowed=decision.allowed, + recommended=bool(decision.recommended), + reason=decision.reason, + reason_code=decision.reason_code, + recommendation_reason=decision.recommendation_reason or decision.reason, + recommendation_reason_code=( + decision.recommendation_reason_code or decision.reason_code + ), + mode=decision.mode, + directive=(AgentDirective.ALLOW if decision.allowed else AgentDirective.DENY), + recommended_directive=( + AgentDirective.ALLOW if decision.recommended else AgentDirective.DENY + ), + score=decision.score, + expected_gain=decision.expected_gain, + estimated_cost_value=decision.estimated_cost_value, + uncertainty=decision.uncertainty, + confidence=decision.confidence, + ) + + def to_dict(self) -> dict[str, Any]: + return { + "action_id": self.action_id, + "allowed": self.allowed, + "recommended": self.recommended, + "reason": self.reason, + "reason_code": self.reason_code, + "recommendation_reason": self.recommendation_reason, + "recommendation_reason_code": self.recommendation_reason_code, + "mode": self.mode, + "directive": AgentDirective.parse(self.directive).value, + "recommended_directive": AgentDirective.parse(self.recommended_directive).value, + "replacement": dict(self.replacement), + "score": self.score, + "expected_gain": self.expected_gain, + "estimated_cost_value": self.estimated_cost_value, + "uncertainty": self.uncertainty, + "confidence": self.confidence, + } + + @classmethod + def from_dict(cls, payload: Mapping[str, Any]) -> AgentDecision: + if not isinstance(payload, Mapping): + raise TypeError("decision payload must be a mapping") + replacement = payload.get("replacement", {}) + if not isinstance(replacement, Mapping): + raise TypeError("replacement must be a mapping") + return cls( + action_id=payload["action_id"], + allowed=payload["allowed"], + recommended=payload["recommended"], + reason=payload["reason"], + reason_code=payload["reason_code"], + recommendation_reason=payload["recommendation_reason"], + recommendation_reason_code=payload["recommendation_reason_code"], + mode=payload["mode"], + directive=payload.get("directive", "allow"), + recommended_directive=payload.get("recommended_directive", "allow"), + replacement=dict(replacement), + score=payload.get("score", 0.0), + expected_gain=payload.get("expected_gain", 0.0), + estimated_cost_value=payload.get("estimated_cost_value", 0.0), + uncertainty=payload.get("uncertainty", 0.0), + confidence=payload.get("confidence", 0.0), + ) + + +@dataclass(frozen=True, slots=True) +class AgentEvent: + """One normalized lifecycle event emitted by an engine adapter.""" + + engine: str + session_id: str + task_id: str + event_type: AgentEventType | str + action: AgentAction | None = None + protocol_version: str = PROTOCOL_VERSION + state: Mapping[str, Any] = field(default_factory=dict) + metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + for name in ("engine", "session_id", "task_id", "protocol_version"): + value = getattr(self, name) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must not be empty") + if self.protocol_version != PROTOCOL_VERSION: + raise ValueError( + f"unsupported protocol_version {self.protocol_version!r}; " + f"expected {PROTOCOL_VERSION!r}" + ) + object.__setattr__(self, "event_type", AgentEventType.parse(self.event_type)) + if self.action is not None and not isinstance(self.action, AgentAction): + raise TypeError("action must be AgentAction or None") + if not isinstance(self.state, Mapping) or not isinstance(self.metadata, Mapping): + raise TypeError("state and metadata must be mappings") + object.__setattr__(self, "state", MappingProxyType(dict(self.state))) + object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata))) + + def to_dict(self) -> dict[str, Any]: + return { + "protocol_version": self.protocol_version, + "engine": self.engine, + "session_id": self.session_id, + "task_id": self.task_id, + "event_type": AgentEventType.parse(self.event_type).value, + "action": self.action.to_dict() if self.action else None, + "state": dict(self.state), + "metadata": dict(self.metadata), + } + + @classmethod + def from_dict(cls, payload: Mapping[str, Any]) -> AgentEvent: + action_payload = payload.get("action") + if action_payload is not None and not isinstance(action_payload, Mapping): + raise TypeError("action payload must be a mapping or None") + return cls( + protocol_version=payload.get("protocol_version", PROTOCOL_VERSION), + engine=payload["engine"], + session_id=payload["session_id"], + task_id=payload["task_id"], + event_type=payload["event_type"], + action=(AgentAction.from_dict(action_payload) if action_payload is not None else None), + state=dict(payload.get("state", {})), + metadata=dict(payload.get("metadata", {})), + ) diff --git a/src/marginal/public_eval.py b/src/marginal/public_eval.py index f6dafb9..db75eb0 100644 --- a/src/marginal/public_eval.py +++ b/src/marginal/public_eval.py @@ -12,6 +12,8 @@ @dataclass(frozen=True, slots=True) class RunRecord: + """Measured result for one benchmark instance.""" + instance_id: str resolved: bool tokens: int @@ -20,16 +22,28 @@ class RunRecord: tool_calls: int = 0 def __post_init__(self) -> None: - if not self.instance_id: + if not isinstance(self.instance_id, str) or not self.instance_id: raise ValueError("instance_id must not be empty") - if self.tokens < 0 or self.usd < 0 or self.latency_ms < 0 or self.tool_calls < 0: - raise ValueError("metrics must be non-negative") - if not math.isfinite(self.usd): + if not isinstance(self.resolved, bool): + raise TypeError("resolved must be a boolean") + for name in ("tokens", "latency_ms", "tool_calls"): + value = getattr(self, name) + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{name} must be an integer") + if value < 0: + raise ValueError("metrics must be non-negative") + if isinstance(self.usd, bool) or not isinstance(self.usd, (int, float)): + raise TypeError("usd must be a number") + if not math.isfinite(float(self.usd)): raise ValueError("usd must be finite") + if self.usd < 0: + raise ValueError("metrics must be non-negative") + object.__setattr__(self, "usd", float(self.usd)) def load_runs(path: Path) -> dict[str, RunRecord]: - """Load one JSON object per benchmark instance.""" + """Load one strict JSON object per benchmark instance.""" + records: dict[str, RunRecord] = {} with path.open(encoding="utf-8") as stream: for line_number, raw in enumerate(stream, start=1): @@ -37,13 +51,15 @@ def load_runs(path: Path) -> dict[str, RunRecord]: continue try: item = json.loads(raw) + if not isinstance(item, dict): + raise TypeError("benchmark row must be an object") record = RunRecord( - instance_id=str(item["instance_id"]), - resolved=bool(item["resolved"]), - tokens=int(item.get("tokens", 0)), - usd=float(item.get("usd", 0.0)), - latency_ms=int(item.get("latency_ms", 0)), - tool_calls=int(item.get("tool_calls", 0)), + instance_id=item["instance_id"], + resolved=item["resolved"], + tokens=item.get("tokens", 0), + usd=item.get("usd", 0.0), + latency_ms=item.get("latency_ms", 0), + tool_calls=item.get("tool_calls", 0), ) except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc: raise ValueError(f"invalid benchmark row on line {line_number}") from exc @@ -74,10 +90,15 @@ def _saving(original: float, optimized: float) -> float: def _bootstrap_token_savings( - pairs: list[tuple[RunRecord, RunRecord]], samples: int, seed: int + pairs: list[tuple[RunRecord, RunRecord]], + samples: int, + seed: int, + confidence_level: float, ) -> tuple[float, float]: if samples <= 0: raise ValueError("bootstrap_samples must be positive") + if not 0.0 < confidence_level < 1.0: + raise ValueError("confidence_level must be between 0 and 1") rng = random.Random(seed) estimates: list[float] = [] for _ in range(samples): @@ -86,8 +107,9 @@ def _bootstrap_token_savings( marginal = sum(right.tokens for _, right in draw) estimates.append(_saving(float(baseline), float(marginal))) estimates.sort() - lower = estimates[int(0.025 * (samples - 1))] - upper = estimates[int(0.975 * (samples - 1))] + tail = (1.0 - confidence_level) / 2.0 + lower = estimates[int(tail * (samples - 1))] + upper = estimates[int((1.0 - tail) * (samples - 1))] return round(lower, 2), round(upper, 2) @@ -97,8 +119,19 @@ def compare_runs( *, bootstrap_samples: int = 2_000, seed: int = 42, + confidence_level: float = 0.95, + quality_margin_pp: float = 1.0, ) -> dict[str, Any]: - """Compare matched public-benchmark executions without imputing missing tasks.""" + """Compare matched executions without imputing missing tasks.""" + + if isinstance(quality_margin_pp, bool) or not isinstance(quality_margin_pp, (int, float)): + raise TypeError("quality_margin_pp must be a number") + if not math.isfinite(float(quality_margin_pp)) or quality_margin_pp < 0: + raise ValueError("quality_margin_pp must be finite and non-negative") + if not 0.0 < confidence_level < 1.0: + raise ValueError("confidence_level must be between 0 and 1") + quality_margin_pp = float(quality_margin_pp) + if set(baseline) != set(marginal): missing = sorted(set(baseline) ^ set(marginal)) raise ValueError(f"baseline and MARGINAL instance IDs differ: {missing[:5]}") @@ -112,13 +145,30 @@ def compare_runs( 2, ) token_ci = _bootstrap_token_savings( - list(zip(baseline_rows, marginal_rows, strict=True)), bootstrap_samples, seed + list(zip(baseline_rows, marginal_rows, strict=True)), + bootstrap_samples, + seed, + confidence_level, ) + + def efficiency(total: dict[str, Any]) -> dict[str, float | None]: + resolved = int(total["resolved"]) + if resolved == 0: + return {"tokens_per_resolved": None, "usd_per_resolved": None} + return { + "tokens_per_resolved": round(float(total["tokens"]) / resolved, 6), + "usd_per_resolved": round(float(total["usd"]) / resolved, 6), + } + return { - "benchmark": "public-agent-benchmark-comparison-v1", + "benchmark": "public-agent-benchmark-comparison-v2", "tasks": len(ids), "baseline": baseline_total, "marginal": marginal_total, + "efficiency": { + "baseline": efficiency(baseline_total), + "marginal": efficiency(marginal_total), + }, "savings": { "tokens_percent": _saving(baseline_total["tokens"], marginal_total["tokens"]), "usd_percent": _saving(baseline_total["usd"], marginal_total["usd"]), @@ -126,10 +176,14 @@ def compare_runs( "tool_calls_percent": _saving( baseline_total["tool_calls"], marginal_total["tool_calls"] ), + "confidence_level": confidence_level, + "tokens_confidence_interval": list(token_ci), "tokens_95pct_ci": list(token_ci), }, "quality": { "resolved_delta_pp": delta_pp, + "non_inferiority_margin_pp": quality_margin_pp, + "preserved_within_margin": delta_pp >= -quality_margin_pp, "preserved_within_one_pp": delta_pp >= -1.0, "regressions": sum( baseline[item].resolved and not marginal[item].resolved for item in ids @@ -141,12 +195,25 @@ def compare_runs( } +def _format_optional_number(value: float | None) -> str: + return "n/a" if value is None else f"{value:,.2f}" + + +def _format_optional_usd(value: float | None) -> str: + return "n/a" if value is None else f"${value:.6f}" + + def render_public_report(result: dict[str, Any]) -> str: baseline = result["baseline"] marginal = result["marginal"] savings = result["savings"] quality = result["quality"] - ci = savings["tokens_95pct_ci"] + efficiency = result["efficiency"] + baseline_efficiency = efficiency["baseline"] + marginal_efficiency = efficiency["marginal"] + ci = savings["tokens_confidence_interval"] + confidence_percent = float(savings["confidence_level"]) * 100.0 + margin = float(quality["non_inferiority_margin_pp"]) return "\n".join( [ "# Measured public benchmark comparison", @@ -170,15 +237,33 @@ def render_public_report(result: dict[str, Any]) -> str: ), ( f"| Latency | {baseline['latency_ms']:,} ms | " - f"{marginal['latency_ms']:,} ms | {savings['latency_percent']:.2f}% lower |" + f"{marginal['latency_ms']:,} ms | " + f"{savings['latency_percent']:.2f}% lower |" ), ( f"| Tool calls | {baseline['tool_calls']} | " - f"{marginal['tool_calls']} | {savings['tool_calls_percent']:.2f}% fewer |" + f"{marginal['tool_calls']} | " + f"{savings['tool_calls_percent']:.2f}% fewer |" + ), + ( + "| Tokens per resolved task | " + f"{_format_optional_number(baseline_efficiency['tokens_per_resolved'])} | " + f"{_format_optional_number(marginal_efficiency['tokens_per_resolved'])} | — |" + ), + ( + "| USD per resolved task | " + f"{_format_optional_usd(baseline_efficiency['usd_per_resolved'])} | " + f"{_format_optional_usd(marginal_efficiency['usd_per_resolved'])} | — |" ), "", - f"Token savings 95% bootstrap interval: **{ci[0]:.2f}% to {ci[1]:.2f}%**.", - f"Quality preserved within 1 pp: **{quality['preserved_within_one_pp']}**.", + ( + f"Token savings {confidence_percent:.1f}% bootstrap interval: " + f"**{ci[0]:.2f}% to {ci[1]:.2f}%**." + ), + ( + f"Quality preserved within the {margin:.2f} pp non-inferiority margin: " + f"**{quality['preserved_within_margin']}**." + ), f"Regressions: **{quality['regressions']}**. Recoveries: **{quality['recoveries']}**.", "", ] diff --git a/src/marginal/registry.py b/src/marginal/registry.py new file mode 100644 index 0000000..b44f771 --- /dev/null +++ b/src/marginal/registry.py @@ -0,0 +1,42 @@ +"""Registry for explicitly versioned value estimators.""" + +from __future__ import annotations + +from typing import Protocol + +from .estimator import EstimatorIdentity +from .models import Action + + +class RegisteredEstimator(Protocol): + identity: EstimatorIdentity + + def estimate(self, action: Action) -> float: ... + + +class EstimatorRegistry: + """Resolve estimators by stable ``(name, version)`` identity.""" + + def __init__(self) -> None: + self._estimators: dict[tuple[str, str], RegisteredEstimator] = {} + + def register(self, estimator: RegisteredEstimator) -> None: + key = estimator.identity.key + if key in self._estimators: + raise ValueError( + f"estimator {estimator.identity.name}@{estimator.identity.version} " + "is already registered" + ) + self._estimators[key] = estimator + + def resolve(self, name: str, version: str) -> RegisteredEstimator: + try: + return self._estimators[(name, version)] + except KeyError as exc: + raise KeyError(f"unknown estimator {name}@{version}") from exc + + def identities(self) -> tuple[EstimatorIdentity, ...]: + return tuple( + estimator.identity + for _, estimator in sorted(self._estimators.items(), key=lambda item: item[0]) + ) diff --git a/src/marginal/replay.py b/src/marginal/replay.py new file mode 100644 index 0000000..7a40476 --- /dev/null +++ b/src/marginal/replay.py @@ -0,0 +1,141 @@ +"""Off-policy replay of versioned MARGINAL decision evidence.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from .budget import BudgetLedger, BudgetLimits +from .ledger import read_decision_ledger +from .models import Action, Cost +from .policy import MarginalPolicy + + +@dataclass(frozen=True, slots=True) +class ReplayResult: + policy_name: str + policy_version: str + actions: int + recorded_allowed: int + replayed_allowed: int + agreements: int + disagreements: int + estimated_considered_tokens: int + estimated_selected_tokens: int + estimated_avoided_tokens: int + + def to_dict(self) -> dict[str, Any]: + return { + "policy": {"name": self.policy_name, "version": self.policy_version}, + "actions": self.actions, + "recorded_allowed": self.recorded_allowed, + "replayed_allowed": self.replayed_allowed, + "agreements": self.agreements, + "disagreements": self.disagreements, + "estimated_considered_tokens": self.estimated_considered_tokens, + "estimated_selected_tokens": self.estimated_selected_tokens, + "estimated_avoided_tokens": self.estimated_avoided_tokens, + "causal_interpretation": False, + } + + +def replay_ledger( + path: str | Path, + policy: MarginalPolicy, + limits: BudgetLimits | None = None, +) -> ReplayResult: + """Re-evaluate authorization events using estimated costs. + + Replay describes what a policy would have recommended over recorded actions. It does not + simulate missing task trajectories, infer outcome quality, or prove causal savings. + """ + + records = read_decision_ledger(path) + ledger = BudgetLedger(limits or BudgetLimits()) + actions = 0 + recorded_allowed = 0 + replayed_allowed = 0 + agreements = 0 + considered_tokens = 0 + selected_tokens = 0 + + for record in records: + if record.get("event") != "authorization": + continue + action_payload = record.get("action") + decision_payload = record.get("decision") + if not isinstance(action_payload, dict) or not isinstance(decision_payload, dict): + raise ValueError("authorization records require action and decision objects") + try: + cost_payload = action_payload.get("cost", {}) + action = Action( + name=action_payload["name"], + kind=action_payload["kind"], + cost=Cost(**dict(cost_payload)), + expected_gain=action_payload.get("expected_gain"), + current_success_probability=action_payload.get("current_success_probability", 0.0), + is_verification=action_payload.get("is_verification", False), + fingerprint=action_payload.get("fingerprint"), + metadata=dict(action_payload.get("metadata", {})), + ) + except (KeyError, TypeError, ValueError) as exc: + sequence = record.get("sequence", "unknown") + raise ValueError(f"malformed authorization record at sequence {sequence}") from exc + actions += 1 + considered_tokens += action.cost.tokens + recorded_value = decision_payload.get("recommended", decision_payload.get("allowed")) + if not isinstance(recorded_value, bool): + raise ValueError("recorded recommended decision must be a boolean") + recorded = recorded_value + recorded_allowed += int(recorded) + replayed = policy.evaluate(action, ledger) + replayed_allowed += int(replayed.allowed) + agreements += int(recorded == replayed.allowed) + if replayed.allowed: + ledger.commit(action) + selected_tokens += action.cost.tokens + if action.fingerprint: + policy.mark_executed(action.fingerprint) + + if actions == 0: + raise ValueError("decision ledger contains no authorization events") + return ReplayResult( + policy_name=policy.identity.name, + policy_version=policy.identity.version, + actions=actions, + recorded_allowed=recorded_allowed, + replayed_allowed=replayed_allowed, + agreements=agreements, + disagreements=actions - agreements, + estimated_considered_tokens=considered_tokens, + estimated_selected_tokens=selected_tokens, + estimated_avoided_tokens=considered_tokens - selected_tokens, + ) + + +def render_replay_report(result: ReplayResult) -> str: + return "\n".join( + [ + "# MARGINAL policy replay", + "", + ( + "This is an off-policy diagnostic based on recorded proposed actions and " + "estimated costs. It is **not causal proof** of token savings or preserved quality." + ), + "", + f"Policy: **{result.policy_name}@{result.policy_version}**", + "", + "| Metric | Value |", + "|---|---:|", + f"| Actions replayed | {result.actions} |", + f"| Recorded recommendations allowed | {result.recorded_allowed} |", + f"| Replayed recommendations allowed | {result.replayed_allowed} |", + f"| Agreements | {result.agreements} |", + f"| Disagreements | {result.disagreements} |", + f"| Estimated considered tokens | {result.estimated_considered_tokens:,} |", + f"| Estimated selected tokens | {result.estimated_selected_tokens:,} |", + f"| Estimated avoided tokens | {result.estimated_avoided_tokens:,} |", + "", + ] + ) diff --git a/src/marginal/runtime.py b/src/marginal/runtime.py new file mode 100644 index 0000000..7584106 --- /dev/null +++ b/src/marginal/runtime.py @@ -0,0 +1,102 @@ +"""Local engine-neutral runtime for thin MARGINAL adapters.""" + +from __future__ import annotations + +from dataclasses import replace + +from .models import Action, Cost +from .outcomes import Outcome +from .protocol import AgentAction, AgentCapabilities, AgentDecision +from .treasury import Treasury + + +class UniversalRuntime: + """Translate normalized agent actions into transactional MARGINAL operations.""" + + def __init__( + self, + treasury: Treasury, + *, + engine: str, + session_id: str, + task_id: str, + capabilities: AgentCapabilities | None = None, + ) -> None: + for name, value in ( + ("engine", engine), + ("session_id", session_id), + ("task_id", task_id), + ): + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must not be empty") + if capabilities is not None and not isinstance(capabilities, AgentCapabilities): + raise TypeError("capabilities must be AgentCapabilities or None") + negotiated = capabilities or AgentCapabilities() + if treasury.mode.is_blocking and not negotiated.block_actions: + raise ValueError("enforce mode requires an adapter with block_actions capability") + self.treasury = treasury + self.engine = engine + self.session_id = session_id + self.task_id = task_id + self.capabilities = negotiated + self._pending: dict[str, Action] = {} + + def before_action(self, action: AgentAction) -> AgentDecision: + if not isinstance(action, AgentAction): + raise TypeError("action must be AgentAction") + if action.action_id in self._pending: + raise ValueError(f"action_id is already pending: {action.action_id}") + core_action = action.to_core_action(engine=self.engine) + core_action = replace( + core_action, + metadata={ + **dict(core_action.metadata), + "session_id": self.session_id, + "task_id": self.task_id, + }, + ) + decision = self.treasury.authorize(core_action) + if decision.allowed: + self._pending[action.action_id] = core_action + return AgentDecision.from_core(action.action_id, decision) + + def after_action(self, action_id: str, *, actual_cost: Cost | None = None) -> None: + if actual_cost is not None and not isinstance(actual_cost, Cost): + raise TypeError("actual_cost must be Cost or None") + action = self._pop_pending(action_id) + committed = action if actual_cost is None else replace(action, cost=actual_cost) + self.treasury.commit(committed) + + def fail_action( + self, + action_id: str, + *, + reason: str, + actual_cost: Cost | None = None, + ) -> None: + if not isinstance(reason, str) or not reason.strip(): + raise ValueError("reason must not be empty") + if actual_cost is not None and not isinstance(actual_cost, Cost): + raise TypeError("actual_cost must be Cost or None") + action = self._pop_pending(action_id) + if actual_cost is None: + self.treasury.abort(action, reason=reason) + else: + self.treasury.settle_failure(action, actual_cost, reason=reason) + + def record_outcome(self, outcome: Outcome) -> None: + if outcome.task_id != self.task_id: + raise ValueError( + f"outcome task_id {outcome.task_id!r} does not match runtime " + f"task_id {self.task_id!r}" + ) + self.treasury.record_outcome(outcome) + + def pending_action_ids(self) -> tuple[str, ...]: + return tuple(sorted(self._pending)) + + def _pop_pending(self, action_id: str) -> Action: + try: + return self._pending.pop(action_id) + except KeyError as exc: + raise KeyError(f"unknown or settled action_id: {action_id}") from exc diff --git a/src/marginal/schema.py b/src/marginal/schema.py new file mode 100644 index 0000000..29ef579 --- /dev/null +++ b/src/marginal/schema.py @@ -0,0 +1,41 @@ +"""Access the versioned JSON Schemas shipped with MARGINAL.""" + +from __future__ import annotations + +import json +from importlib.resources import files +from pathlib import PurePath +from typing import Any + +_SCHEMA_PACKAGE = "marginal.schemas" + + +def available_schemas() -> tuple[str, ...]: + """Return the packaged public schema names in deterministic order.""" + + root = files(_SCHEMA_PACKAGE) + return tuple( + sorted( + item.name for item in root.iterdir() if item.is_file() and item.name.endswith(".json") + ) + ) + + +def load_schema(name: str) -> dict[str, Any]: + """Load one packaged schema by file name. + + Paths, traversal components, and unknown names are rejected so callers cannot use this + helper as a generic package-resource reader. + """ + + if not isinstance(name, str): + raise TypeError("schema name must be a string") + if not name or PurePath(name).name != name or not name.endswith(".json"): + raise ValueError("schema name must be a plain JSON file name") + if name not in available_schemas(): + raise KeyError(f"unknown MARGINAL schema: {name}") + text = files(_SCHEMA_PACKAGE).joinpath(name).read_text(encoding="utf-8") + payload = json.loads(text) + if not isinstance(payload, dict): + raise ValueError(f"packaged schema {name!r} is not a JSON object") + return payload diff --git a/src/marginal/schemas/__init__.py b/src/marginal/schemas/__init__.py new file mode 100644 index 0000000..e794b0f --- /dev/null +++ b/src/marginal/schemas/__init__.py @@ -0,0 +1 @@ +"""Packaged JSON Schema resources for the MARGINAL public protocols.""" diff --git a/src/marginal/schemas/agent-capabilities-v1.json b/src/marginal/schemas/agent-capabilities-v1.json new file mode 100644 index 0000000..9a8268c --- /dev/null +++ b/src/marginal/schemas/agent-capabilities-v1.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/agent-capabilities-v1.json", + "title": "MARGINAL Agent Capabilities v1", + "type": "object", + "required": [ + "observe_model_usage", + "block_actions", + "modify_actions", + "stop_agent", + "control_model_turns", + "record_outcomes" + ], + "properties": { + "observe_model_usage": {"type": "boolean"}, + "block_actions": {"type": "boolean"}, + "modify_actions": {"type": "boolean"}, + "stop_agent": {"type": "boolean"}, + "control_model_turns": {"type": "boolean"}, + "record_outcomes": {"type": "boolean"}, + "level": {"enum": ["observe", "control", "full"]} + }, + "additionalProperties": false +} diff --git a/src/marginal/schemas/agent-decision-v1.json b/src/marginal/schemas/agent-decision-v1.json new file mode 100644 index 0000000..67cf9b1 --- /dev/null +++ b/src/marginal/schemas/agent-decision-v1.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/agent-decision-v1.json", + "title": "MARGINAL Agent Decision v1", + "type": "object", + "required": [ + "action_id", + "allowed", + "recommended", + "reason", + "reason_code", + "recommendation_reason", + "recommendation_reason_code", + "mode", + "directive", + "recommended_directive", + "replacement", + "score", + "expected_gain", + "estimated_cost_value", + "uncertainty", + "confidence" + ], + "properties": { + "action_id": {"type": "string", "minLength": 1}, + "allowed": {"type": "boolean"}, + "recommended": {"type": "boolean"}, + "reason": {"type": "string", "minLength": 1}, + "reason_code": {"type": "string", "minLength": 1}, + "recommendation_reason": {"type": "string", "minLength": 1}, + "recommendation_reason_code": {"type": "string", "minLength": 1}, + "mode": {"enum": ["shadow", "recommend", "enforce"]}, + "directive": { + "enum": ["allow", "deny", "modify", "defer", "reuse", "stop", "force_verify"] + }, + "recommended_directive": { + "enum": ["allow", "deny", "modify", "defer", "reuse", "stop", "force_verify"] + }, + "replacement": {"type": "object"}, + "score": {"type": "number"}, + "expected_gain": {"type": "number", "minimum": 0, "maximum": 1}, + "estimated_cost_value": {"type": "number"}, + "uncertainty": {"type": "number", "minimum": 0}, + "confidence": {"type": "number", "minimum": 0, "maximum": 1} + }, + "additionalProperties": false +} diff --git a/src/marginal/schemas/agent-event-v1.json b/src/marginal/schemas/agent-event-v1.json new file mode 100644 index 0000000..a9830d9 --- /dev/null +++ b/src/marginal/schemas/agent-event-v1.json @@ -0,0 +1,111 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/agent-event-v1.json", + "title": "MARGINAL Agent Event v1", + "type": "object", + "required": ["protocol_version", "engine", "session_id", "task_id", "event_type"], + "properties": { + "protocol_version": {"const": "1.0"}, + "engine": {"type": "string", "minLength": 1}, + "session_id": {"type": "string", "minLength": 1}, + "task_id": {"type": "string", "minLength": 1}, + "event_type": { + "enum": [ + "session.start", + "session.end", + "action.before", + "action.after", + "action.failed", + "outcome" + ] + }, + "action": { + "oneOf": [ + {"$ref": "#/$defs/agent_action"}, + {"type": "null"} + ] + }, + "state": {"type": "object"}, + "metadata": {"type": "object"} + }, + "$defs": { + "cost": { + "type": "object", + "required": ["tokens", "usd", "latency_ms", "risk"], + "properties": { + "tokens": {"type": "integer", "minimum": 0}, + "usd": {"type": "number", "minimum": 0}, + "latency_ms": {"type": "integer", "minimum": 0}, + "risk": {"type": "number", "minimum": 0} + }, + "additionalProperties": false + }, + "token_usage": { + "type": "object", + "required": [ + "input_tokens", + "cached_input_tokens", + "output_tokens", + "reasoning_tokens", + "total_tokens" + ], + "properties": { + "input_tokens": {"type": "integer", "minimum": 0}, + "cached_input_tokens": {"type": "integer", "minimum": 0}, + "output_tokens": {"type": "integer", "minimum": 0}, + "reasoning_tokens": {"type": "integer", "minimum": 0}, + "total_tokens": {"type": "integer", "minimum": 0} + }, + "additionalProperties": false + }, + "agent_action": { + "type": "object", + "required": [ + "action_id", + "name", + "kind", + "estimated_cost", + "current_success_probability", + "is_verification", + "state_hash", + "phase", + "retry_number", + "deduplication_scope", + "metadata" + ], + "properties": { + "action_id": {"type": "string", "minLength": 1}, + "name": {"type": "string", "minLength": 1}, + "kind": {"type": "string", "minLength": 1}, + "estimated_cost": {"$ref": "#/$defs/cost"}, + "token_usage": { + "oneOf": [ + {"$ref": "#/$defs/token_usage"}, + {"type": "null"} + ] + }, + "expected_gain": { + "oneOf": [ + {"type": "number", "minimum": 0, "maximum": 1}, + {"type": "null"} + ] + }, + "current_success_probability": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "is_verification": {"type": "boolean"}, + "state_hash": {"type": "string"}, + "phase": {"type": "string"}, + "retry_number": {"type": "integer", "minimum": 0}, + "deduplication_scope": { + "enum": ["exact", "once_per_state", "once_per_phase", "allow_retry"] + }, + "metadata": {"type": "object"} + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/src/marginal/schemas/aggregate-export-v1.json b/src/marginal/schemas/aggregate-export-v1.json new file mode 100644 index 0000000..7c53dfb --- /dev/null +++ b/src/marginal/schemas/aggregate-export-v1.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/aggregate-export-v1.json", + "title": "MARGINAL Aggregate Privacy Export Record v1", + "description": "Grouped, generalized decision or outcome data with no identifiers, timestamps, free text, metadata, model identity, verifier details, or tool arguments.", + "type": "object", + "required": [ + "schema_version", + "privacy_profile", + "record_type", + "action_kind", + "cost_bucket", + "gain_bucket", + "recommendation", + "applied_decision", + "reason_code", + "outcome_class", + "count", + "minimum_group_size" + ], + "properties": { + "schema_version": { + "const": "1.0" + }, + "privacy_profile": { + "const": "aggregate_export" + }, + "record_type": { + "enum": [ + "decision", + "outcome" + ] + }, + "action_kind": { + "type": "string", + "pattern": "^[a-z0-9_.-]{1,64}$" + }, + "cost_bucket": { + "enum": [ + "low", + "medium", + "high", + "unknown" + ] + }, + "gain_bucket": { + "enum": [ + "low", + "medium", + "high", + "unknown" + ] + }, + "recommendation": { + "enum": [ + "allow", + "deny", + "unknown", + "not_applicable" + ] + }, + "applied_decision": { + "enum": [ + "allow", + "deny", + "unknown", + "not_applicable" + ] + }, + "reason_code": { + "type": "string", + "pattern": "^[A-Z0-9_]{1,64}$|^not_applicable$" + }, + "outcome_class": { + "enum": [ + "verified_success", + "verified_failure", + "positive_reward", + "non_positive_reward", + "unknown", + "not_applicable" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "minimum_group_size": { + "type": "integer", + "minimum": 1, + "description": "Configured k-threshold; groups with fewer source records are suppressed." + } + }, + "additionalProperties": false +} diff --git a/src/marginal/schemas/decision-ledger-v2.json b/src/marginal/schemas/decision-ledger-v2.json new file mode 100644 index 0000000..7dee902 --- /dev/null +++ b/src/marginal/schemas/decision-ledger-v2.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/decision-ledger-v2.json", + "title": "MARGINAL Decision Ledger Record v2", + "type": "object", + "required": [ + "schema_version", + "event_id", + "sequence", + "timestamp", + "run_id", + "event" + ], + "properties": { + "schema_version": { + "const": "2.0" + }, + "event_id": { + "type": "string", + "minLength": 1 + }, + "sequence": { + "type": "integer", + "minimum": 1 + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "run_id": { + "type": "string", + "minLength": 1 + }, + "task_id": { + "type": "string" + }, + "trajectory_id": { + "type": "string" + }, + "engine": { + "type": "string" + }, + "model": { + "type": "string" + }, + "event": { + "type": "string", + "minLength": 1 + }, + "mode": { + "type": "string" + }, + "policy": { + "type": "object" + }, + "estimator": { + "type": "object" + }, + "action": { + "type": "object" + }, + "decision": { + "type": "object" + }, + "outcome": { + "type": "object" + }, + "privacy_profile": { + "type": "string", + "enum": [ + "local_full", + "safe_telemetry" + ], + "default": "local_full" + } + }, + "additionalProperties": true +} diff --git a/src/marginal/schemas/outcome-v1.json b/src/marginal/schemas/outcome-v1.json new file mode 100644 index 0000000..533e29b --- /dev/null +++ b/src/marginal/schemas/outcome-v1.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/outcome-v1.json", + "title": "MARGINAL Outcome v1", + "type": "object", + "required": ["task_id", "reward"], + "properties": { + "task_id": {"type": "string", "minLength": 1}, + "reward": {"type": "number"}, + "resolved": {"type": ["boolean", "null"]}, + "verifier": {"type": "string"}, + "trajectory_id": {"type": "string"}, + "evidence": {"type": "object"}, + "metrics": {"type": "object", "additionalProperties": {"type": "number"}} + }, + "additionalProperties": false +} diff --git a/src/marginal/schemas/safe-telemetry-v1.json b/src/marginal/schemas/safe-telemetry-v1.json new file mode 100644 index 0000000..a800814 --- /dev/null +++ b/src/marginal/schemas/safe-telemetry-v1.json @@ -0,0 +1,392 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/safe-telemetry-v1.json", + "title": "MARGINAL Safe Telemetry Record v1", + "description": "Strict event-level telemetry with keyed pseudonyms, generalized timestamps, allowlisted labels and numeric fields, and no free-form content.", + "type": "object", + "required": [ + "schema_version", + "privacy_profile", + "event_id", + "sequence", + "timestamp", + "run_id", + "event" + ], + "properties": { + "schema_version": { + "const": "2.0" + }, + "privacy_profile": { + "const": "safe_telemetry" + }, + "event_id": { + "type": "string", + "pattern": "^psn_[0-9a-f]{32}$" + }, + "sequence": { + "type": "integer", + "minimum": 1 + }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T00:00:00\\+00:00$" + }, + "run_id": { + "type": "string", + "pattern": "^psn_[0-9a-f]{32}$" + }, + "task_id": { + "$ref": "#/$defs/pseudonym" + }, + "trajectory_id": { + "$ref": "#/$defs/pseudonym" + }, + "action_id": { + "$ref": "#/$defs/pseudonym" + }, + "engine_instance": { + "$ref": "#/$defs/pseudonym" + }, + "engine": { + "enum": [ + "aider", + "claude-code", + "cline", + "codex", + "continue", + "gemini-cli", + "github-copilot", + "opencode", + "roo-code", + "other" + ] + }, + "event": { + "enum": [ + "abort", + "authorization", + "candidate_ranking", + "commit", + "custom", + "estimator_observation", + "failure_settlement", + "outcome", + "session_end", + "session_start" + ] + }, + "mode": { + "enum": [ + "shadow", + "recommend", + "enforce", + "unknown" + ] + }, + "budget_overrun": { + "type": "boolean" + }, + "realized_gain": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "policy": { + "$ref": "#/$defs/identity" + }, + "estimator": { + "$ref": "#/$defs/identity" + }, + "action": { + "$ref": "#/$defs/action" + }, + "decision": { + "$ref": "#/$defs/decision" + }, + "outcome": { + "$ref": "#/$defs/outcome" + }, + "candidates": { + "type": "array", + "items": { + "$ref": "#/$defs/candidate" + } + }, + "usage": { + "$ref": "#/$defs/numeric_usage" + }, + "reserved": { + "$ref": "#/$defs/numeric_usage" + } + }, + "$defs": { + "pseudonym": { + "type": "string", + "pattern": "^(?:psn_[0-9a-f]{32})?$" + }, + "version": { + "type": "string", + "pattern": "^(?:v?\\d+(?:\\.\\d+){0,3}(?:[-+][0-9A-Za-z.-]{1,24})?|unknown|unversioned)$" + }, + "numeric_usage": { + "type": "object", + "properties": { + "tokens": { + "type": "number", + "minimum": 0 + }, + "usd": { + "type": "number", + "minimum": 0 + }, + "latency_ms": { + "type": "number", + "minimum": 0 + }, + "risk": { + "type": "number", + "minimum": 0 + }, + "input_tokens": { + "type": "number", + "minimum": 0 + }, + "cached_input_tokens": { + "type": "number", + "minimum": 0 + }, + "output_tokens": { + "type": "number", + "minimum": 0 + }, + "reasoning_tokens": { + "type": "number", + "minimum": 0 + }, + "total_tokens": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "version": { + "$ref": "#/$defs/version" + } + }, + "additionalProperties": false + }, + "action": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "command", + "file_read", + "file_write", + "generation", + "llm", + "model_call", + "reasoning", + "research", + "review", + "search", + "subagent", + "test", + "tool", + "verification", + "other" + ] + }, + "expected_gain": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "current_success_probability": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "is_verification": { + "type": "boolean" + }, + "retry_number": { + "type": "integer", + "minimum": 0 + }, + "deduplication_scope": { + "enum": [ + "exact", + "once_per_state", + "once_per_phase", + "allow_retry", + "unknown" + ] + }, + "cost": { + "$ref": "#/$defs/numeric_usage" + }, + "estimated_cost": { + "$ref": "#/$defs/numeric_usage" + }, + "token_usage": { + "$ref": "#/$defs/numeric_usage" + }, + "fingerprint": { + "$ref": "#/$defs/pseudonym" + }, + "action_id": { + "$ref": "#/$defs/pseudonym" + }, + "state_hash": { + "$ref": "#/$defs/pseudonym" + } + }, + "additionalProperties": false + }, + "decision": { + "type": "object", + "properties": { + "allowed": { + "type": "boolean" + }, + "recommended": { + "type": "boolean" + }, + "score": { + "type": "number" + }, + "expected_gain": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "estimated_cost_value": { + "type": "number", + "minimum": 0 + }, + "uncertainty": { + "type": "number", + "minimum": 0 + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "reason_code": { + "enum": [ + "APPROVED", + "BUDGET_REJECTED", + "DENY", + "DUPLICATE_ACTION", + "DUPLICATE_PENDING", + "EXPECTED_GAIN_REJECTED", + "FUNDED", + "MARGINAL_ROI_REJECTED", + "OTHER", + "PARENT_BUDGET_REJECTED", + "RECOMMEND_OVERRIDE", + "SHADOW_OVERRIDE", + "TARGET_REACHED", + "UNSPECIFIED" + ] + }, + "recommendation_reason_code": { + "enum": [ + "APPROVED", + "BUDGET_REJECTED", + "DENY", + "DUPLICATE_ACTION", + "DUPLICATE_PENDING", + "EXPECTED_GAIN_REJECTED", + "FUNDED", + "MARGINAL_ROI_REJECTED", + "OTHER", + "PARENT_BUDGET_REJECTED", + "RECOMMEND_OVERRIDE", + "SHADOW_OVERRIDE", + "TARGET_REACHED", + "UNSPECIFIED" + ] + }, + "mode": { + "enum": [ + "shadow", + "recommend", + "enforce", + "unknown" + ] + }, + "directive": { + "enum": [ + "allow", + "deny", + "modify", + "defer", + "reuse", + "stop", + "force_verify", + "unknown" + ] + }, + "recommended_directive": { + "enum": [ + "allow", + "deny", + "modify", + "defer", + "reuse", + "stop", + "force_verify", + "unknown" + ] + }, + "estimator_version": { + "$ref": "#/$defs/version" + } + }, + "additionalProperties": false + }, + "outcome": { + "type": "object", + "properties": { + "task_id": { + "$ref": "#/$defs/pseudonym" + }, + "trajectory_id": { + "$ref": "#/$defs/pseudonym" + }, + "reward": { + "type": "number" + }, + "resolved": { + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + }, + "candidate": { + "type": "object", + "properties": { + "action": { + "$ref": "#/$defs/action" + }, + "decision": { + "$ref": "#/$defs/decision" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/src/marginal/schemas/token-usage-v2.json b/src/marginal/schemas/token-usage-v2.json new file mode 100644 index 0000000..29cd304 --- /dev/null +++ b/src/marginal/schemas/token-usage-v2.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SignalLayerLabs/Marginal/schemas/token-usage-v2.json", + "title": "MARGINAL Token Usage v2", + "type": "object", + "required": [ + "input_tokens", + "cached_input_tokens", + "output_tokens", + "reasoning_tokens", + "total_tokens" + ], + "properties": { + "input_tokens": {"type": "integer", "minimum": 0}, + "cached_input_tokens": {"type": "integer", "minimum": 0}, + "output_tokens": {"type": "integer", "minimum": 0}, + "reasoning_tokens": {"type": "integer", "minimum": 0}, + "total_tokens": {"type": "integer", "minimum": 0} + }, + "additionalProperties": false +} diff --git a/src/marginal/trace.py b/src/marginal/trace.py index 177b9bd..3579262 100644 --- a/src/marginal/trace.py +++ b/src/marginal/trace.py @@ -4,7 +4,7 @@ import json import threading -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from dataclasses import asdict from datetime import datetime, timezone from pathlib import Path @@ -23,8 +23,19 @@ def emit(self, event: Mapping[str, Any]) -> None: del event +class CompositeTraceSink: + """Fan one event out to multiple sinks in deterministic order.""" + + def __init__(self, sinks: Sequence[TraceSink]) -> None: + self.sinks = tuple(sinks) + + def emit(self, event: Mapping[str, Any]) -> None: + for sink in self.sinks: + sink.emit(event) + + class JsonlTraceSink: - """Write one self-contained JSON object per line.""" + """Write one v0.1-compatible, self-contained JSON object per line.""" def __init__(self, path: str | Path) -> None: self.path = Path(path) @@ -32,10 +43,7 @@ def __init__(self, path: str | Path) -> None: self._lock = threading.Lock() def emit(self, event: Mapping[str, Any]) -> None: - record = { - "timestamp": datetime.now(timezone.utc).isoformat(), - **dict(event), - } + record = {"timestamp": datetime.now(timezone.utc).isoformat(), **dict(event)} encoded = json.dumps(record, sort_keys=True, ensure_ascii=False, default=repr) with self._lock, self.path.open("a", encoding="utf-8") as stream: stream.write(encoded + "\n") diff --git a/src/marginal/treasury.py b/src/marginal/treasury.py index 2e536b5..f120c30 100644 --- a/src/marginal/treasury.py +++ b/src/marginal/treasury.py @@ -9,7 +9,9 @@ from .budget import BudgetLedger, BudgetLimits, BudgetOverrun, BudgetUsage from .fingerprint import fingerprint_action -from .models import Action, Allocation, Decision +from .models import Action, Allocation, Cost, Decision +from .modes import ExecutionMode +from .outcomes import Outcome from .policy import MarginalPolicy from .trace import NullTraceSink, TraceSink, action_payload, decision_payload, usage_payload @@ -29,18 +31,30 @@ def __init__( trace_sink: TraceSink | None = None, name: str = "root", parent: Treasury | None = None, + mode: ExecutionMode | str = ExecutionMode.ENFORCE, ) -> None: self.name = name self.ledger = BudgetLedger(limits) self.policy = policy or MarginalPolicy() self.trace_sink = trace_sink or NullTraceSink() self.parent = parent + self.mode = ExecutionMode.parse(mode) self._lock: threading.RLock = parent._lock if parent is not None else threading.RLock() + self._root: Treasury = parent._root if parent is not None else self self._pending: dict[str, Treasury] = parent._pending if parent is not None else {} + self._pending_semantics: dict[str, list[str]] = ( + parent._pending_semantics if parent is not None else {} + ) + if parent is None: + self._reservation_counter = 0 self._approved_count = 0 self._denied_count = 0 self._committed_count = 0 self._aborted_count = 0 + self._observed_overruns = 0 + self._failed_settled_count = 0 + self._outcome_count = 0 + self._observation_count = 0 @property def usage(self) -> BudgetUsage: @@ -53,16 +67,27 @@ def limits(self) -> BudgetLimits: def propose(self, action: Action) -> Decision: return self.authorize(action) - def is_authorized(self, action: Action) -> bool: - """Return whether an action currently owns a pending reservation.""" + def evaluate(self, action: Action) -> Decision: + """Evaluate an action without reserving resources or mutating counters.""" + + prepared = self._prepare(action) + assert prepared.fingerprint is not None + with self._lock: + return self._recommended_decision(prepared) + def is_authorized(self, action: Action) -> bool: prepared = self._prepare(action) assert prepared.fingerprint is not None with self._lock: - return self._pending.get(prepared.fingerprint) is self + return self._owned_reservation_fingerprint(prepared.fingerprint) is not None def fund_best(self, actions: Iterable[Action]) -> Allocation | None: - """Evaluate candidates and reserve the one with the highest marginal score.""" + """Evaluate candidates and reserve the recommended highest-value candidate. + + This remains an active allocation API in every mode. Shadow mode applies to a + proposed action supplied by an external agent; it does not invent an uncontrolled + baseline candidate when MARGINAL itself is asked to choose. + """ with self._lock: candidates: list[tuple[Action, Decision]] = [] @@ -70,19 +95,7 @@ def fund_best(self, actions: Iterable[Action]) -> Allocation | None: for action in actions: prepared = self._prepare(action) assert prepared.fingerprint is not None - if prepared.fingerprint in self._pending: - decision = Decision(False, "rejected: duplicate pending action") - else: - decision = self.policy.evaluate(prepared, self.ledger) - if decision.allowed: - for ancestor in self._ancestors(): - affordability = ancestor.ledger.can_afford(prepared) - if not affordability.allowed: - decision = Decision( - False, - f"rejected by parent: {affordability.reason}", - ) - break + decision = self._recommended_decision(prepared) evaluated.append( { "action": action_payload(prepared), @@ -94,6 +107,7 @@ def fund_best(self, actions: Iterable[Action]) -> Allocation | None: self.trace_sink.emit( { + **self._identity_payload(), "event": "candidate_ranking", "treasury": self.name, "candidates": evaluated, @@ -111,34 +125,27 @@ def fund_best(self, actions: Iterable[Action]) -> Allocation | None: item[0].fingerprint or "", ), ) - decision = self.authorize(prepared) + decision = self.authorize(prepared, apply_mode=False) if not decision.allowed: return None return Allocation(action=prepared, decision=decision) - def authorize(self, action: Action) -> Decision: + def authorize(self, action: Action, *, apply_mode: bool = True) -> Decision: prepared = self._prepare(action) assert prepared.fingerprint is not None with self._lock: - if prepared.fingerprint in self._pending: - decision = Decision(False, "rejected: duplicate pending action") - else: - decision = self.policy.evaluate(prepared, self.ledger) - - if decision.allowed: - for ancestor in self._ancestors(): - affordability = ancestor.ledger.can_afford(prepared) - if not affordability.allowed: - decision = Decision( - False, - f"rejected by parent: {affordability.reason}", - ) - break + recommended = self._recommended_decision(prepared) + decision = self._apply_mode(recommended) if apply_mode else recommended + reservation_action: Action | None = None if decision.allowed: + reservation_action = self._reservation_action(prepared) for ledger in self._ledger_chain(): - ledger.reserve(prepared) - self._pending[prepared.fingerprint] = self + if self.mode.is_blocking or not apply_mode: + ledger.reserve(reservation_action) + else: + ledger.reserve_unchecked(reservation_action) + self._register_pending(prepared.fingerprint, reservation_action.fingerprint) self._approved_count += 1 else: self._denied_count += 1 @@ -146,6 +153,7 @@ def authorize(self, action: Action) -> Decision: try: self.trace_sink.emit( { + **self._identity_payload(), "event": "authorization", "treasury": self.name, "action": action_payload(prepared), @@ -156,9 +164,14 @@ def authorize(self, action: Action) -> Decision: ) except Exception: if decision.allowed: + assert reservation_action is not None + assert reservation_action.fingerprint is not None for ledger in self._ledger_chain(): - ledger.release(prepared.fingerprint) - del self._pending[prepared.fingerprint] + ledger.release(reservation_action.fingerprint) + self._unregister_pending( + prepared.fingerprint, + reservation_action.fingerprint, + ) self._approved_count -= 1 else: self._denied_count -= 1 @@ -166,10 +179,35 @@ def authorize(self, action: Action) -> Decision: return decision def commit(self, action: Action) -> BudgetUsage: + return self._settle(action, failed=False, failure_reason="") + + def settle_failure( + self, + action: Action, + actual_cost: Cost, + *, + reason: str, + ) -> BudgetUsage: + """Account measured external spend from a failed action without hiding its error.""" + + if not isinstance(actual_cost, Cost): + raise TypeError("actual_cost must be Cost") + prepared = self._prepare(action) + committed = replace(prepared, cost=actual_cost) + return self._settle(committed, failed=True, failure_reason=reason) + + def _settle( + self, + action: Action, + *, + failed: bool, + failure_reason: str, + ) -> BudgetUsage: prepared = self._prepare(action) assert prepared.fingerprint is not None with self._lock: - if self._pending.get(prepared.fingerprint) is not self: + reservation_fingerprint = self._owned_reservation_fingerprint(prepared.fingerprint) + if reservation_fingerprint is None: raise AuthorizationRequired( "action must be authorized by this treasury before commit" ) @@ -178,44 +216,53 @@ def commit(self, action: Action) -> BudgetUsage: for ledger in self._ledger_chain(): decision = ledger.settle( prepared, - reservation_fingerprint=prepared.fingerprint, + reservation_fingerprint=reservation_fingerprint, ) if not decision.allowed: violations.append(decision.reason) - self.policy.mark_executed(prepared.fingerprint) - del self._pending[prepared.fingerprint] + if not failed: + self.policy.mark_executed(prepared.fingerprint) + self._unregister_pending(prepared.fingerprint, reservation_fingerprint) self._committed_count += 1 - self.trace_sink.emit( - { - "event": "commit", - "treasury": self.name, - "action": action_payload(prepared), - "usage": usage_payload(self.usage), - "budget_overrun": bool(violations), - "violations": sorted(set(violations)), - } - ) - if violations: + if failed: + self._failed_settled_count += 1 + if violations and not self.mode.is_blocking: + self._observed_overruns += 1 + + event = { + **self._identity_payload(), + "event": "failure_settlement" if failed else "commit", + "treasury": self.name, + "action": action_payload(prepared), + "usage": usage_payload(self.usage), + "budget_overrun": bool(violations), + "violations": sorted(set(violations)), + } + if failed: + event["reason"] = failure_reason + self.trace_sink.emit(event) + + if violations and self.mode.is_blocking and not failed: raise BudgetOverrun("; ".join(sorted(set(violations)))) return self.usage def abort(self, action: Action, *, reason: str = "execution aborted") -> None: - """Release all reservations for an authorized action without recording spend.""" - prepared = self._prepare(action) assert prepared.fingerprint is not None with self._lock: - if self._pending.get(prepared.fingerprint) is not self: + reservation_fingerprint = self._owned_reservation_fingerprint(prepared.fingerprint) + if reservation_fingerprint is None: raise AuthorizationRequired( "action must be authorized by this treasury before abort" ) for ledger in self._ledger_chain(): - ledger.release(prepared.fingerprint) - del self._pending[prepared.fingerprint] + ledger.release(reservation_fingerprint) + self._unregister_pending(prepared.fingerprint, reservation_fingerprint) self._aborted_count += 1 self.trace_sink.emit( { + **self._identity_payload(), "event": "abort", "treasury": self.name, "action": action_payload(prepared), @@ -224,6 +271,43 @@ def abort(self, action: Action, *, reason: str = "execution aborted") -> None: } ) + def observe_value(self, action: Action, realized_gain: float) -> None: + """Record explicit action-level realized gain for the configured estimator.""" + + observe = getattr(self.policy.estimator, "observe_action", None) + if not callable(observe): + raise TypeError("configured estimator does not support action observations") + observe(action, realized_gain) + self._observation_count += 1 + self.trace_sink.emit( + { + **self._identity_payload(), + "event": "estimator_observation", + "treasury": self.name, + "action": action_payload(self._prepare(action)), + "realized_gain": float(realized_gain), + } + ) + + def record_outcome(self, outcome: Outcome) -> None: + """Record a verified task outcome without inferring individual action causality.""" + + if not isinstance(outcome, Outcome): + raise TypeError("outcome must be Outcome") + self._outcome_count += 1 + try: + self.trace_sink.emit( + { + **self._identity_payload(), + "event": "outcome", + "treasury": self.name, + "outcome": outcome.to_dict(), + } + ) + except Exception: + self._outcome_count -= 1 + raise + def child(self, name: str, limits: BudgetLimits) -> Treasury: if not name.strip(): raise ValueError("child treasury name must not be empty") @@ -233,18 +317,134 @@ def child(self, name: str, limits: BudgetLimits) -> Treasury: trace_sink=self.trace_sink, name=f"{self.name}/{name}", parent=self, + mode=self.mode, ) def summary(self) -> dict[str, Any]: return { "name": self.name, + "mode": self.mode.value, "approved": self._approved_count, "denied": self._denied_count, "committed": self._committed_count, "aborted": self._aborted_count, + "observed_overruns": self._observed_overruns, + "failed_settled": self._failed_settled_count, + "outcomes": self._outcome_count, + "estimator_observations": self._observation_count, "usage": asdict(self.usage), "reserved": asdict(self.ledger.reserved_usage), "limits": asdict(self.limits), + "policy": self.policy.identity.to_dict(), + "estimator": self.policy.estimator_identity.to_dict(), + } + + def _recommended_decision(self, prepared: Action) -> Decision: + assert prepared.fingerprint is not None + if self._pending_semantics.get(prepared.fingerprint): + return Decision( + False, + "rejected: duplicate pending action", + recommended=False, + recommendation_reason="rejected: duplicate pending action", + reason_code="DUPLICATE_PENDING", + recommendation_reason_code="DUPLICATE_PENDING", + estimator_name=self.policy.estimator_identity.name, + estimator_version=self.policy.estimator_identity.version, + ) + + decision = self.policy.evaluate(prepared, self.ledger) + if decision.allowed: + for ancestor in self._ancestors(): + affordability = ancestor.ledger.can_afford(prepared) + if not affordability.allowed: + return replace( + decision, + allowed=False, + recommended=False, + reason=f"rejected by parent: {affordability.reason}", + recommendation_reason=f"rejected by parent: {affordability.reason}", + reason_code="PARENT_BUDGET_REJECTED", + recommendation_reason_code="PARENT_BUDGET_REJECTED", + ) + return decision + + def _apply_mode(self, recommended: Decision) -> Decision: + if self.mode.is_blocking: + return replace( + recommended, + mode=self.mode.value, + recommended=recommended.allowed, + recommendation_reason=recommended.reason, + recommendation_reason_code=recommended.reason_code, + ) + if recommended.allowed: + return replace( + recommended, + mode=self.mode.value, + recommended=True, + recommendation_reason=recommended.reason, + recommendation_reason_code=recommended.reason_code, + ) + return replace( + recommended, + allowed=True, + recommended=False, + reason=( + f"{self.mode.value}: action executed; recommendation was deny " + f"({recommended.reason})" + ), + reason_code=f"{self.mode.value.upper()}_OVERRIDE", + recommendation_reason=recommended.reason, + recommendation_reason_code=recommended.reason_code, + mode=self.mode.value, + ) + + def _reservation_action(self, prepared: Action) -> Action: + assert prepared.fingerprint is not None + if not self._pending_semantics.get(prepared.fingerprint): + return prepared + self._root._reservation_counter += 1 + reservation_fingerprint = ( + f"{prepared.fingerprint}:reservation:{self._root._reservation_counter}" + ) + return replace(prepared, fingerprint=reservation_fingerprint) + + def _register_pending( + self, + semantic_fingerprint: str, + reservation_fingerprint: str | None, + ) -> None: + if reservation_fingerprint is None: + raise ValueError("reservation fingerprint must not be empty") + self._pending[reservation_fingerprint] = self + self._pending_semantics.setdefault(semantic_fingerprint, []).append(reservation_fingerprint) + + def _unregister_pending( + self, + semantic_fingerprint: str, + reservation_fingerprint: str, + ) -> None: + del self._pending[reservation_fingerprint] + reservations = self._pending_semantics[semantic_fingerprint] + reservations.remove(reservation_fingerprint) + if not reservations: + del self._pending_semantics[semantic_fingerprint] + + def _owned_reservation_fingerprint( + self, + semantic_fingerprint: str, + ) -> str | None: + for reservation_fingerprint in self._pending_semantics.get(semantic_fingerprint, ()): + if self._pending.get(reservation_fingerprint) is self: + return reservation_fingerprint + return None + + def _identity_payload(self) -> dict[str, Any]: + return { + "mode": self.mode.value, + "policy": self.policy.identity.to_dict(), + "estimator": self.policy.estimator_identity.to_dict(), } def _prepare(self, action: Action) -> Action: diff --git a/tests/test_adapters_regression.py b/tests/test_adapters_regression.py new file mode 100644 index 0000000..cf6789c --- /dev/null +++ b/tests/test_adapters_regression.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import pytest + +from marginal import Action, BudgetLimits, Cost, MarginalPolicy, PolicyConfig, Treasury +from marginal.adapters import ActionDenied, budgeted_call, extract_common_llm_usage + + +def treasury() -> Treasury: + return Treasury( + BudgetLimits(max_tokens=1_000, max_usd=1.0), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + ) + + +def test_budgeted_call_does_not_execute_when_denied() -> None: + called = False + + def operation() -> None: + nonlocal called + called = True + + with pytest.raises(ActionDenied): + budgeted_call( + treasury(), + operation, + action=Action(name="too large", kind="llm", cost=Cost(tokens=1_001), expected_gain=0.5), + ) + assert not called + + +def test_budgeted_call_commits_actual_usage_from_extractor() -> None: + account = treasury() + budgeted_call( + account, + lambda: {"usage": {"total_tokens": 40}}, + action=Action(name="model", kind="llm", cost=Cost(tokens=100), expected_gain=0.2), + usage_extractor=lambda result, _estimate: Cost(tokens=result["usage"]["total_tokens"]), + ) + assert account.usage.tokens == 40 + + +def test_common_usage_extractor_preserves_unobserved_dimensions() -> None: + actual = extract_common_llm_usage( + {"usage": {"input_tokens": 30, "output_tokens": 10}}, + Cost(tokens=100, usd=0.08, latency_ms=500, risk=0.02), + ) + assert actual == Cost(tokens=40, usd=0.08, latency_ms=500, risk=0.02) + + +def test_exact_repeated_call_is_rejected() -> None: + account = treasury() + action = Action(name="transform", kind="tool", cost=Cost(tokens=10), expected_gain=0.2) + budgeted_call(account, str.upper, "same", action=action) + with pytest.raises(ActionDenied, match="duplicate action"): + budgeted_call(account, str.upper, "same", action=action) diff --git a/tests/test_budget_regression.py b/tests/test_budget_regression.py new file mode 100644 index 0000000..cfcf624 --- /dev/null +++ b/tests/test_budget_regression.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import pytest + +from marginal.budget import BudgetLedger, BudgetLimits +from marginal.models import Action, Cost + + +def test_cost_rejects_negative_values() -> None: + with pytest.raises(ValueError, match="non-negative"): + Cost(tokens=-1) + + +def test_regular_action_cannot_spend_verification_reserve() -> None: + ledger = BudgetLedger( + BudgetLimits(max_tokens=1_000, max_usd=1.0, verification_reserve_tokens=200) + ) + action = Action(name="research", kind="research", cost=Cost(tokens=850, usd=0.10)) + affordability = ledger.can_afford(action) + assert not affordability.allowed + assert affordability.reason == "verification reserve would be breached" + + +def test_reservations_reduce_available_budget_before_commit() -> None: + ledger = BudgetLedger(BudgetLimits(max_tokens=100)) + first = Action(name="first", kind="tool", cost=Cost(tokens=70), fingerprint="first") + second = Action(name="second", kind="tool", cost=Cost(tokens=40), fingerprint="second") + ledger.reserve(first) + assert ledger.usage.tokens == 0 + assert ledger.reserved_usage.tokens == 70 + assert not ledger.can_afford(second).allowed + + +def test_verification_spend_does_not_reduce_regular_limit_unnecessarily() -> None: + ledger = BudgetLedger(BudgetLimits(max_tokens=1_000, verification_reserve_tokens=200)) + ledger.commit( + Action( + name="verify", + kind="verification", + cost=Cost(tokens=200), + is_verification=True, + ) + ) + assert ledger.can_afford( + Action(name="finish", kind="generation", cost=Cost(tokens=800)) + ).allowed diff --git a/tests/test_cli.py b/tests/test_cli.py index b8f95de..3b30894 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -64,24 +64,3 @@ def test_demo_matches_committed_benchmark(capsys) -> None: assert exit_code == 0 assert capsys.readouterr().out == render_markdown(run_benchmark()) - - -def test_killer_demo_command_writes_artifacts(tmp_path, capsys) -> None: - exit_code = main(["killer-demo", "--output", str(tmp_path)]) - - output = capsys.readouterr().out - assert exit_code == 0 - assert "MARGINAL Killer Demo" in output - assert "Verified outcome: preserved" in output - assert (tmp_path / "RESULTS.md").exists() - assert (tmp_path / "index.html").exists() - assert (tmp_path / "comparison.svg").exists() - - -def test_public_eval_cli(tmp_path, capsys): - baseline = tmp_path / "baseline.jsonl" - marginal = tmp_path / "marginal.jsonl" - baseline.write_text('{"instance_id":"task","resolved":true,"tokens":100}\n') - marginal.write_text('{"instance_id":"task","resolved":true,"tokens":50}\n') - assert main(["public-eval", str(baseline), str(marginal), "--bootstrap-samples", "20"]) == 0 - assert "50.00% fewer" in capsys.readouterr().out diff --git a/tests/test_cli_v2.py b/tests/test_cli_v2.py new file mode 100644 index 0000000..01224ca --- /dev/null +++ b/tests/test_cli_v2.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from marginal.cli import main +from marginal.ledger import DecisionLedgerContext, JsonlDecisionLedger + + +def test_cli_validates_and_reports_decision_ledger(tmp_path: Path, capsys) -> None: + path = tmp_path / "ledger.jsonl" + JsonlDecisionLedger(path, context=DecisionLedgerContext(run_id="run")).emit({"event": "custom"}) + assert main(["ledger-validate", str(path)]) == 0 + assert "valid decision ledger" in capsys.readouterr().out + assert main(["ledger-report", str(path), "--json"]) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["events"] == 1 + + +def test_public_eval_cli_accepts_statistical_configuration(tmp_path: Path, capsys) -> None: + baseline = tmp_path / "baseline.jsonl" + marginal = tmp_path / "marginal.jsonl" + baseline.write_text( + '{"instance_id":"task","resolved":true,"tokens":100}\n', + encoding="utf-8", + ) + marginal.write_text( + '{"instance_id":"task","resolved":true,"tokens":50}\n', + encoding="utf-8", + ) + + assert ( + main( + [ + "public-eval", + str(baseline), + str(marginal), + "--json", + "--confidence-level", + "0.9", + "--quality-margin-pp", + "0.5", + "--seed", + "7", + ] + ) + == 0 + ) + payload = json.loads(capsys.readouterr().out) + assert payload["savings"]["confidence_level"] == 0.9 + assert payload["quality"]["non_inferiority_margin_pp"] == 0.5 + + +def test_cli_exports_safe_and_aggregate_privacy_profiles(tmp_path: Path, capsys) -> None: + source = tmp_path / "source.jsonl" + JsonlDecisionLedger( + source, + context=DecisionLedgerContext( + run_id="customer-acme", + task_id="customer-acme", + model="internal-model", + ), + ).emit( + { + "event": "authorization", + "action": { + "name": "review termination clause", + "kind": "verification", + "cost": {"tokens": 100, "usd": 0.0, "latency_ms": 0, "risk": 0.0}, + "fingerprint": "guessable", + "metadata": {"repository": "secret-merger-project"}, + }, + "decision": { + "allowed": True, + "recommended": True, + "reason": "approved confidential review", + "reason_code": "APPROVED", + "recommendation_reason": "approved confidential review", + "recommendation_reason_code": "APPROVED", + "expected_gain": 0.2, + }, + } + ) + + safe = tmp_path / "safe.jsonl" + key = tmp_path / "privacy.key" + assert ( + main( + [ + "ledger-export", + str(source), + str(safe), + "--privacy-profile", + "safe_telemetry", + "--privacy-key-file", + str(key), + ] + ) + == 0 + ) + assert "exported" in capsys.readouterr().out + safe_text = safe.read_text(encoding="utf-8") + assert "customer-acme" not in safe_text + assert "termination clause" not in safe_text + + aggregate = tmp_path / "aggregate.jsonl" + assert ( + main( + [ + "ledger-export", + str(source), + str(aggregate), + "--privacy-profile", + "aggregate_export", + "--minimum-group-size", + "1", + ] + ) + == 0 + ) + rows = [json.loads(line) for line in aggregate.read_text(encoding="utf-8").splitlines()] + assert rows[0]["privacy_profile"] == "aggregate_export" + assert rows[0]["count"] == 1 + + +def test_cli_refuses_to_overwrite_privacy_export(tmp_path: Path, capsys) -> None: + source = tmp_path / "source.jsonl" + JsonlDecisionLedger(source, context=DecisionLedgerContext(run_id="run")).emit( + {"event": "custom"} + ) + destination = tmp_path / "existing.jsonl" + destination.write_text("do not replace", encoding="utf-8") + + assert ( + main( + [ + "ledger-export", + str(source), + str(destination), + "--privacy-profile", + "aggregate_export", + ] + ) + == 1 + ) + assert "already exists" in capsys.readouterr().err + assert destination.read_text(encoding="utf-8") == "do not replace" + + +def test_ledger_export_uses_owner_only_permissions(tmp_path: Path) -> None: + import os + + source = tmp_path / "source.jsonl" + ledger = JsonlDecisionLedger( + source, + context=DecisionLedgerContext(run_id="run", task_id="task"), + ) + ledger.emit({"event": "custom"}) + destination = tmp_path / "aggregate.jsonl" + + assert ( + main( + [ + "ledger-export", + str(source), + str(destination), + "--privacy-profile", + "aggregate_export", + ] + ) + == 0 + ) + + if os.name != "nt": + assert destination.stat().st_mode & 0o077 == 0 diff --git a/tests/test_decision_ledger.py b/tests/test_decision_ledger.py new file mode 100644 index 0000000..f48a74a --- /dev/null +++ b/tests/test_decision_ledger.py @@ -0,0 +1,449 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from marginal import Action, BudgetLimits, Cost, MarginalPolicy, PolicyConfig, Treasury +from marginal.ledger import DecisionLedgerContext, JsonlDecisionLedger, read_decision_ledger +from marginal.outcomes import Outcome + + +def test_ledger_enriches_events_with_versioned_context_and_sequence(tmp_path: Path) -> None: + path = tmp_path / "ledger.jsonl" + context = DecisionLedgerContext( + run_id="run-1", + task_id="task-1", + trajectory_id="trajectory-1", + engine="codex", + model="gpt-test", + ) + ledger = JsonlDecisionLedger(path, context=context) + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + trace_sink=ledger, + mode="shadow", + ) + action = Action(name="read", kind="file_read", cost=Cost(tokens=10), expected_gain=0.2) + treasury.authorize(action) + treasury.commit(action) + treasury.record_outcome(Outcome(task_id="task-1", reward=1.0, resolved=True, verifier="pytest")) + + records = read_decision_ledger(path) + assert [record["sequence"] for record in records] == [1, 2, 3] + assert all(record["schema_version"] == "2.0" for record in records) + assert all(record["run_id"] == "run-1" for record in records) + assert records[0]["policy"]["version"] == "2.0.0" + assert records[0]["estimator"]["version"] == "2.0.0" + assert records[-1]["event"] == "outcome" + + +def test_ledger_does_not_add_prompt_or_output_fields(tmp_path: Path) -> None: + path = tmp_path / "ledger.jsonl" + ledger = JsonlDecisionLedger(path, context=DecisionLedgerContext(run_id="run")) + ledger.emit({"event": "custom", "safe": "metadata"}) + record = json.loads(path.read_text(encoding="utf-8")) + assert "prompt" not in record + assert "output" not in record + + +def test_reader_rejects_non_monotonic_sequence(tmp_path: Path) -> None: + path = tmp_path / "bad.jsonl" + path.write_text( + "\n".join( + [ + json.dumps( + { + "schema_version": "2.0", + "event_id": "event-1", + "sequence": 2, + "timestamp": "2026-08-06T00:00:00+00:00", + "run_id": "run", + "event": "a", + } + ), + json.dumps( + { + "schema_version": "2.0", + "event_id": "event-2", + "sequence": 1, + "timestamp": "2026-08-06T00:00:01+00:00", + "run_id": "run", + "event": "b", + } + ), + ] + ) + + "\n", + encoding="utf-8", + ) + with pytest.raises(ValueError, match="sequence"): + read_decision_ledger(path) + + +def test_outcome_validates_reward_and_immutable_mappings() -> None: + outcome = Outcome(task_id="task", reward=0.5, evidence={"suite": "unit"}) + assert outcome.evidence["suite"] == "unit" + with pytest.raises(TypeError): + outcome.evidence["suite"] = "full" # type: ignore[index] + with pytest.raises(ValueError, match="finite"): + Outcome(task_id="task", reward=float("nan")) + + +def test_treasury_outcome_event_keeps_policy_and_estimator_identity(tmp_path) -> None: + from marginal import BudgetLimits, MarginalPolicy, Outcome, PolicyConfig, Treasury + + ledger = JsonlDecisionLedger( + tmp_path / "ledger.jsonl", + context=DecisionLedgerContext(run_id="run-identity", task_id="task"), + ) + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=1.0)), + trace_sink=ledger, + ) + + treasury.record_outcome(Outcome(task_id="task", resolved=True, reward=1.0)) + + record = read_decision_ledger(tmp_path / "ledger.jsonl")[0] + assert record["event"] == "outcome" + assert record["policy"]["name"] == "marginal-reference" + assert record["estimator"]["name"] == "historical-mean" + + +def test_ledger_rejects_reserved_envelope_field_overrides(tmp_path) -> None: + ledger = JsonlDecisionLedger( + tmp_path / "ledger.jsonl", + context=DecisionLedgerContext(run_id="trusted-run"), + ) + + with pytest.raises(ValueError, match="reserved ledger fields"): + ledger.emit({"event": "custom", "run_id": "spoofed-run"}) + + +def test_ledger_serialization_failure_does_not_advance_sequence(tmp_path) -> None: + ledger = JsonlDecisionLedger( + tmp_path / "ledger.jsonl", + context=DecisionLedgerContext(run_id="run"), + ) + + with pytest.raises(TypeError): + ledger.emit({"event": "bad", "unsafe": object()}) + + ledger.emit({"event": "good"}) + records = read_decision_ledger(tmp_path / "ledger.jsonl") + assert records[0]["sequence"] == 1 + + +def test_outcome_rejects_non_string_verifier_and_trajectory() -> None: + with pytest.raises(TypeError, match="verifier"): + Outcome(task_id="task", reward=1.0, verifier=123) # type: ignore[arg-type] + with pytest.raises(TypeError, match="trajectory_id"): + Outcome(task_id="task", reward=1.0, trajectory_id=123) # type: ignore[arg-type] + + +def test_reader_rejects_missing_required_envelope_fields(tmp_path: Path) -> None: + path = tmp_path / "missing-envelope.jsonl" + path.write_text( + json.dumps( + { + "schema_version": "2.0", + "sequence": 1, + "event": "authorization", + "run_id": "run", + } + ) + + "\n", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="event_id"): + read_decision_ledger(path) + + +def test_ledger_rejects_outcome_for_different_context_task(tmp_path: Path) -> None: + ledger = JsonlDecisionLedger( + tmp_path / "ledger.jsonl", + context=DecisionLedgerContext(run_id="run", task_id="task-a"), + ) + + with pytest.raises(ValueError, match="task_id"): + ledger.emit( + { + "event": "outcome", + "outcome": Outcome(task_id="task-b", reward=1.0).to_dict(), + } + ) + + +def test_outcome_trace_failure_does_not_increment_summary_count() -> None: + class FailingTrace: + def emit(self, event) -> None: + del event + raise OSError("ledger unavailable") + + treasury = Treasury(BudgetLimits(), trace_sink=FailingTrace()) + + with pytest.raises(OSError, match="ledger unavailable"): + treasury.record_outcome(Outcome(task_id="task", reward=1.0)) + + assert treasury.summary()["outcomes"] == 0 + + +def test_safe_telemetry_ledger_sanitizes_all_treasury_events(tmp_path: Path) -> None: + from marginal import PrivacyProfile + + path = tmp_path / "safe-ledger.jsonl" + ledger = JsonlDecisionLedger( + path, + context=DecisionLedgerContext( + run_id="customer-acme-contract-2026", + task_id="customer-acme-contract-2026", + trajectory_id="secret-trajectory", + engine="codex", + model="internal-legal-model", + ), + privacy_profile=PrivacyProfile.SAFE_TELEMETRY, + privacy_key=b"k" * 32, + ) + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + trace_sink=ledger, + mode="shadow", + ) + action = Action( + name="review termination clause", + kind="verification", + cost=Cost(tokens=10), + expected_gain=0.2, + metadata={"repository": "secret-merger-project"}, + ) + treasury.authorize(action) + treasury.commit(action) + treasury.record_outcome( + Outcome( + task_id="customer-acme-contract-2026", + reward=1.0, + resolved=True, + verifier="internal legal verifier", + trajectory_id="secret-trajectory", + evidence={"repository": "secret-merger-project"}, + ) + ) + + records = read_decision_ledger(path) + encoded = json.dumps(records, sort_keys=True) + assert all(record["privacy_profile"] == "safe_telemetry" for record in records) + assert records[0]["run_id"].startswith("psn_") + assert records[-1]["outcome"]["task_id"] == records[-1]["task_id"] + assert records[-1]["outcome"]["trajectory_id"] == records[-1]["trajectory_id"] + for secret in ( + "customer-acme", + "secret-trajectory", + "internal-legal-model", + "termination clause", + "secret-merger-project", + "internal legal verifier", + ): + assert secret not in encoded + + +def test_safe_telemetry_ledger_creates_a_local_key_file(tmp_path: Path) -> None: + path = tmp_path / "safe-ledger.jsonl" + key_path = tmp_path / "keys" / "ledger.key" + ledger = JsonlDecisionLedger( + path, + context=DecisionLedgerContext(run_id="run"), + privacy_profile="safe_telemetry", + privacy_key_path=key_path, + ) + ledger.emit({"event": "custom"}) + assert key_path.is_file() + assert len(key_path.read_bytes()) == 32 + + +def test_aggregate_export_cannot_be_used_as_an_operational_ledger_profile( + tmp_path: Path, +) -> None: + with pytest.raises(ValueError, match="aggregate_export"): + JsonlDecisionLedger( + tmp_path / "ledger.jsonl", + context=DecisionLedgerContext(run_id="run"), + privacy_profile="aggregate_export", + ) + + +def test_ledger_rejects_privacy_profile_override(tmp_path: Path) -> None: + ledger = JsonlDecisionLedger( + tmp_path / "ledger.jsonl", + context=DecisionLedgerContext(run_id="run"), + ) + with pytest.raises(ValueError, match="reserved ledger fields"): + ledger.emit({"event": "custom", "privacy_profile": "safe_telemetry"}) + + +def test_ledger_summary_reports_privacy_profiles(tmp_path: Path) -> None: + from marginal import summarize_decision_ledger + + path = tmp_path / "safe.jsonl" + ledger = JsonlDecisionLedger( + path, + context=DecisionLedgerContext(run_id="run"), + privacy_profile="safe_telemetry", + privacy_key=b"k" * 32, + ) + ledger.emit({"event": "custom"}) + summary = summarize_decision_ledger(read_decision_ledger(path)) + assert summary["privacy_profiles"] == ["safe_telemetry"] + + +def test_reader_rejects_unreviewed_fields_in_safe_telemetry(tmp_path: Path) -> None: + path = tmp_path / "safe.jsonl" + ledger = JsonlDecisionLedger( + path, + context=DecisionLedgerContext(run_id="run"), + privacy_profile="safe_telemetry", + privacy_key=b"k" * 32, + ) + ledger.emit({"event": "custom"}) + record = json.loads(path.read_text(encoding="utf-8")) + record["metadata"] = {"repository": "secret-merger"} + path.write_text(json.dumps(record) + "\n", encoding="utf-8") + + with pytest.raises(ValueError, match="safe telemetry"): + read_decision_ledger(path) + + +def test_reader_rejects_malformed_safe_telemetry_pseudonyms(tmp_path: Path) -> None: + path = tmp_path / "safe.jsonl" + ledger = JsonlDecisionLedger( + path, + context=DecisionLedgerContext(run_id="run"), + privacy_profile="safe_telemetry", + privacy_key=b"k" * 32, + ) + ledger.emit({"event": "custom"}) + record = json.loads(path.read_text(encoding="utf-8")) + record["run_id"] = "customer-acme" + path.write_text(json.dumps(record) + "\n", encoding="utf-8") + + with pytest.raises(ValueError, match="safe telemetry"): + read_decision_ledger(path) + + +def test_new_decision_ledger_uses_owner_only_permissions(tmp_path: Path) -> None: + import os + + path = tmp_path / "ledger.jsonl" + ledger = JsonlDecisionLedger(path, context=DecisionLedgerContext(run_id="run")) + ledger.emit({"event": "custom"}) + + if os.name != "nt": + assert path.stat().st_mode & 0o077 == 0 + + +def test_existing_ledger_rejects_symlink_append_target(tmp_path: Path) -> None: + target = tmp_path / "target.jsonl" + JsonlDecisionLedger(target, context=DecisionLedgerContext(run_id="run")).emit( + {"event": "custom"} + ) + link = tmp_path / "linked.jsonl" + try: + link.symlink_to(target) + except (OSError, NotImplementedError): + pytest.skip("symbolic links are unavailable") + + with pytest.raises(ValueError, match="symbolic link"): + JsonlDecisionLedger(link, context=DecisionLedgerContext(run_id="run")) + + +def test_existing_ledger_rejects_weak_permissions_before_append(tmp_path: Path) -> None: + import os + + if os.name == "nt": + pytest.skip("POSIX permission bits are unavailable") + path = tmp_path / "ledger.jsonl" + JsonlDecisionLedger(path, context=DecisionLedgerContext(run_id="run")).emit({"event": "custom"}) + path.chmod(0o644) + + with pytest.raises(PermissionError, match="group or others"): + JsonlDecisionLedger(path, context=DecisionLedgerContext(run_id="run")) + + +def test_aggregate_export_uses_privacy_preserving_group_threshold(tmp_path: Path) -> None: + from marginal import export_decision_ledger + + source = tmp_path / "source.jsonl" + ledger = JsonlDecisionLedger(source, context=DecisionLedgerContext(run_id="run")) + for index in range(4): + ledger.emit( + { + "event": "authorization", + "action": { + "name": f"secret action {index}", + "kind": "verification", + "cost": {"tokens": 100}, + }, + "decision": { + "allowed": True, + "recommended": True, + "reason_code": "APPROVED", + "expected_gain": 0.2, + }, + } + ) + + destination = tmp_path / "aggregate.jsonl" + assert ( + export_decision_ledger( + source, + destination, + privacy_profile="aggregate_export", + ) + == 0 + ) + assert destination.read_text(encoding="utf-8") == "" + + relaxed = tmp_path / "aggregate-relaxed.jsonl" + assert ( + export_decision_ledger( + source, + relaxed, + privacy_profile="aggregate_export", + minimum_group_size=4, + ) + == 1 + ) + row = json.loads(relaxed.read_text(encoding="utf-8")) + assert row["count"] == 4 + assert row["minimum_group_size"] == 4 + + +def test_export_never_overwrites_destination_during_exists_check_race( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from marginal import export_decision_ledger + + source = tmp_path / "source.jsonl" + JsonlDecisionLedger(source, context=DecisionLedgerContext(run_id="run")).emit( + {"event": "custom"} + ) + destination = tmp_path / "existing.jsonl" + destination.write_text("authoritative", encoding="utf-8") + original_exists = Path.exists + + def hide_destination_once(path: Path) -> bool: + return path != destination and original_exists(path) + + monkeypatch.setattr(Path, "exists", hide_destination_once) + + with pytest.raises(FileExistsError): + export_decision_ledger( + source, + destination, + privacy_profile="aggregate_export", + ) + assert destination.read_text(encoding="utf-8") == "authoritative" diff --git a/tests/test_estimator_v2.py b/tests/test_estimator_v2.py new file mode 100644 index 0000000..c3f9898 --- /dev/null +++ b/tests/test_estimator_v2.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +import pytest + +from marginal import Action, Cost +from marginal.estimator import EstimatorIdentity, ValueEstimator +from marginal.registry import EstimatorRegistry + + +def test_explicit_gain_returns_confident_versioned_estimate() -> None: + estimator = ValueEstimator(name="reference", version="2.1.0") + estimate = estimator.estimate_detail( + Action(name="verify", kind="verification", cost=Cost(), expected_gain=0.2) + ) + assert estimate.expected_gain == pytest.approx(0.2) + assert estimate.confidence == 1.0 + assert estimate.uncertainty == 0.0 + assert estimate.provenance == "action.expected_gain" + assert estimate.estimator.name == "reference" + assert estimate.estimator.version == "2.1.0" + + +def test_contextual_observations_are_preferred_over_kind_average() -> None: + estimator = ValueEstimator(context_fields=("engine", "phase")) + generic = Action(name="generic", kind="research") + codex = Action( + name="codex research", + kind="research", + metadata={"engine": "codex", "phase": "diagnose"}, + ) + estimator.observe("research", 0.1) + estimator.observe_action(codex, 0.5) + assert estimator.estimate(generic) == pytest.approx(0.3) + assert estimator.estimate(codex) == pytest.approx(0.5) + + +def test_historical_estimate_reports_sample_metadata() -> None: + estimator = ValueEstimator() + action = Action(name="search", kind="research") + estimator.observe_action(action, 0.1) + estimator.observe_action(action, 0.3) + estimate = estimator.estimate_detail(action) + assert estimate.expected_gain == pytest.approx(0.2) + assert estimate.sample_size == 2 + assert estimate.confidence > 0 + assert estimate.uncertainty > 0 + assert estimate.provenance.startswith("historical:") + + +def test_estimator_identity_hash_is_stable_for_same_configuration() -> None: + first = ValueEstimator(default_gain=0.1, context_fields=("engine",)) + second = ValueEstimator(default_gain=0.1, context_fields=("engine",)) + assert first.identity == second.identity + assert first.identity.config_hash + + +def test_registry_resolves_name_and_version_and_rejects_duplicates() -> None: + registry = EstimatorRegistry() + estimator = ValueEstimator(name="historical", version="2.0.0") + registry.register(estimator) + assert registry.resolve("historical", "2.0.0") is estimator + with pytest.raises(ValueError, match="already registered"): + registry.register(estimator) + + +def test_estimator_identity_requires_nonempty_version() -> None: + with pytest.raises(ValueError, match="version"): + EstimatorIdentity(name="historical", version="", config_hash="abc") + + +def test_estimator_identity_rejects_non_string_training_fingerprint() -> None: + with pytest.raises(TypeError, match="training_data_fingerprint"): + EstimatorIdentity( + name="historical", + version="2.0.0", + config_hash="abc", + training_data_fingerprint=123, # type: ignore[arg-type] + ) + + +def test_value_estimate_rejects_non_string_provenance() -> None: + from marginal.estimator import ValueEstimate + + with pytest.raises(TypeError, match="provenance"): + ValueEstimate( + expected_gain=0.1, + uncertainty=0.0, + confidence=1.0, + sample_size=1, + provenance=123, # type: ignore[arg-type] + estimator=EstimatorIdentity( + name="historical", + version="2.0.0", + config_hash="abc", + ), + ) + + +def test_estimator_rejects_non_string_identity_fields() -> None: + with pytest.raises(TypeError, match="name"): + ValueEstimator(name=123) # type: ignore[arg-type] + with pytest.raises(TypeError, match="version"): + ValueEstimator(version=123) # type: ignore[arg-type] + + +def test_estimator_rejects_invalid_context_fields_container() -> None: + with pytest.raises(TypeError, match="context_fields"): + ValueEstimator(context_fields="engine") # type: ignore[arg-type] + with pytest.raises(ValueError, match="unique"): + ValueEstimator(context_fields=("engine", "engine")) + + +def test_contextual_action_observation_also_updates_kind_fallback() -> None: + estimator = ValueEstimator(context_fields=("engine",)) + contextual = Action( + name="search in codex", + kind="research", + metadata={"engine": "codex"}, + ) + + estimator.observe_action(contextual, 0.4) + + assert estimator.estimate(Action(name="generic search", kind="research")) == pytest.approx(0.4) + + +def test_estimator_identity_tracks_observation_state_reproducibly() -> None: + first = ValueEstimator(name="historical", version="2.0.0") + second = ValueEstimator(name="historical", version="2.0.0") + initial = first.identity + + first.observe("research", 0.2) + second.observe("research", 0.2) + + assert first.identity.training_data_fingerprint + assert first.identity.training_data_fingerprint != initial.training_data_fingerprint + assert first.identity == second.identity diff --git a/tests/test_failure_settlement.py b/tests/test_failure_settlement.py new file mode 100644 index 0000000..71a7004 --- /dev/null +++ b/tests/test_failure_settlement.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import asyncio + +import pytest + +from marginal import Action, BudgetLimits, Cost, MarginalPolicy, PolicyConfig, Treasury +from marginal.adapters import async_budgeted_call, budgeted_call + + +def account() -> Treasury: + return Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + ) + + +def test_sync_failure_can_settle_measured_usage_and_preserve_original_error() -> None: + treasury = account() + action = Action(name="remote call", kind="llm", cost=Cost(tokens=50), expected_gain=0.5) + + def fail() -> None: + raise RuntimeError("provider disconnected") + + with pytest.raises(RuntimeError, match="provider disconnected"): + budgeted_call( + treasury, + fail, + action=action, + failure_usage_extractor=lambda _error, _estimate: Cost(tokens=30), + ) + assert treasury.usage.tokens == 30 + assert treasury.summary()["failed_settled"] == 1 + + +def test_failure_extractor_none_releases_reservation() -> None: + treasury = account() + action = Action(name="local failure", kind="tool", cost=Cost(tokens=50), expected_gain=0.5) + + def fail() -> None: + raise RuntimeError("no spend") + + with pytest.raises(RuntimeError, match="no spend"): + budgeted_call( + treasury, + fail, + action=action, + failure_usage_extractor=lambda _error, _estimate: None, + ) + assert treasury.usage.tokens == 0 + assert treasury.summary()["aborted"] == 1 + + +def test_async_failure_settles_usage() -> None: + treasury = account() + action = Action(name="async remote", kind="llm", cost=Cost(tokens=50), expected_gain=0.5) + + async def fail() -> None: + raise RuntimeError("async provider disconnected") + + with pytest.raises(RuntimeError, match="async provider disconnected"): + asyncio.run( + async_budgeted_call( + treasury, + fail, + action=action, + failure_usage_extractor=lambda _error, _estimate: Cost(tokens=25), + ) + ) + assert treasury.usage.tokens == 25 + + +def test_failed_settlement_does_not_mark_action_as_completed_duplicate() -> None: + treasury = account() + action = Action(name="retryable remote", kind="llm", cost=Cost(tokens=50), expected_gain=0.5) + + prepared = action + decision = treasury.authorize(prepared) + assert decision.allowed + treasury.settle_failure(prepared, Cost(tokens=20), reason="timeout") + + retry = treasury.authorize(action) + + assert retry.allowed + + +def test_failure_usage_extractor_error_settles_estimate_and_preserves_original_error() -> None: + treasury = account() + action = Action( + name="unknown remote failure", + kind="llm", + cost=Cost(tokens=50), + expected_gain=0.5, + ) + + def fail() -> None: + raise RuntimeError("provider failed") + + def broken_extractor(_error: Exception, _estimate: Cost) -> Cost | None: + raise ValueError("usage unavailable") + + with pytest.raises(RuntimeError, match="provider failed") as captured: + budgeted_call( + treasury, + fail, + action=action, + failure_usage_extractor=broken_extractor, + ) + + assert isinstance(captured.value.__cause__, ValueError) + assert treasury.usage.tokens == 50 + assert treasury.ledger.reserved_usage.tokens == 0 diff --git a/tests/test_killer_demo.py b/tests/test_killer_demo.py index 20cfe86..35c6d60 100644 --- a/tests/test_killer_demo.py +++ b/tests/test_killer_demo.py @@ -78,3 +78,21 @@ def test_committed_killer_demo_artifacts_are_current(tmp_path: Path) -> None: for name in ("result.json", "RESULTS.md", "index.html", "comparison.svg", "trace.jsonl"): assert (committed / name).read_bytes() == (tmp_path / name).read_bytes() + + +def test_killer_demo_html_uses_premium_results_layout() -> None: + result = run_killer_demo() + rendered = render_killer_demo_html(result) + + assert 'class="browser-shell"' in rendered + assert 'aria-label="Killer Demo navigation"' in rendered + assert "Same verified outcome. Far fewer tokens, lower cost, lower latency." in rendered + assert "Token reduction" in rendered + assert "Allocation decisions" in rendered + assert "What this demo proves" in rendered + assert "Build agents that spend compute deliberately." in rendered + assert "marginal-project-mark.png" in rendered + assert "72,800" in rendered + assert "4,300" in rendered + assert "$0.763" in rendered + assert "$0.026" in rendered diff --git a/tests/test_models_v2.py b/tests/test_models_v2.py new file mode 100644 index 0000000..6ace08a --- /dev/null +++ b/tests/test_models_v2.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import pytest + +from marginal.models import Decision, TokenUsage +from marginal.modes import ExecutionMode + + +def test_token_usage_calculates_total_when_omitted() -> None: + usage = TokenUsage(input_tokens=10, cached_input_tokens=3, output_tokens=4, reasoning_tokens=5) + assert usage.total_tokens == 22 + + +def test_token_usage_rejects_inconsistent_total() -> None: + with pytest.raises(ValueError, match="total_tokens"): + TokenUsage(input_tokens=10, output_tokens=2, total_tokens=11) + + +def test_token_usage_rejects_boolean_counters() -> None: + with pytest.raises(TypeError, match="input_tokens"): + TokenUsage(input_tokens=True) # type: ignore[arg-type] + + +def test_decision_defaults_preserve_v01_behavior() -> None: + decision = Decision(True, "approved") + assert decision.allowed is True + assert decision.recommended is True + assert decision.mode == "enforce" + assert decision.reason_code == "UNSPECIFIED" + + +def test_execution_mode_parses_case_insensitively() -> None: + assert ExecutionMode.parse("SHADOW") is ExecutionMode.SHADOW + assert ExecutionMode.parse(ExecutionMode.RECOMMEND) is ExecutionMode.RECOMMEND + + +def test_execution_mode_rejects_unknown_value() -> None: + with pytest.raises(ValueError, match="execution mode"): + ExecutionMode.parse("observe-only") + + +def test_common_token_usage_extractor_preserves_breakdown() -> None: + from marginal.adapters import extract_common_token_usage + + usage = extract_common_token_usage( + { + "usage": { + "input_tokens": 100, + "cached_input_tokens": 40, + "output_tokens": 20, + "reasoning_tokens": 10, + "total_tokens": 130, + } + } + ) + assert usage.input_tokens == 60 + assert usage.cached_input_tokens == 40 + assert usage.output_tokens == 20 + assert usage.reasoning_tokens == 10 + assert usage.total_tokens == 130 + + +def test_common_token_usage_handles_reasoning_as_output_subset() -> None: + from marginal.adapters import extract_common_token_usage + + usage = extract_common_token_usage( + { + "usage": { + "input_tokens": 100, + "cached_input_tokens": 40, + "output_tokens": 30, + "reasoning_tokens": 10, + "total_tokens": 130, + } + } + ) + + assert usage.input_tokens == 60 + assert usage.cached_input_tokens == 40 + assert usage.output_tokens == 20 + assert usage.reasoning_tokens == 10 + assert usage.total_tokens == 130 + + +def test_common_token_usage_without_total_treats_output_detail_reasoning_as_subset() -> None: + from marginal.adapters import extract_common_token_usage + + usage = extract_common_token_usage( + { + "usage": { + "input_tokens": 100, + "output_tokens": 30, + "output_tokens_details": {"reasoning_tokens": 10}, + } + } + ) + + assert usage.input_tokens == 100 + assert usage.output_tokens == 20 + assert usage.reasoning_tokens == 10 + assert usage.total_tokens == 130 diff --git a/tests/test_packaged_schemas_v2.py b/tests/test_packaged_schemas_v2.py new file mode 100644 index 0000000..29284d3 --- /dev/null +++ b/tests/test_packaged_schemas_v2.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import marginal + +ROOT = Path(__file__).resolve().parents[1] +EXPECTED = { + "aggregate-export-v1.json", + "agent-capabilities-v1.json", + "agent-decision-v1.json", + "agent-event-v1.json", + "decision-ledger-v2.json", + "outcome-v1.json", + "safe-telemetry-v1.json", + "token-usage-v2.json", +} + + +def test_public_schema_api_lists_and_loads_all_contracts() -> None: + assert set(marginal.available_schemas()) == EXPECTED + for name in EXPECTED: + payload = marginal.load_schema(name) + assert payload["$schema"] == "https://json-schema.org/draft/2020-12/schema" + assert payload == json.loads((ROOT / "schemas" / name).read_text(encoding="utf-8")) + + +def test_schema_api_rejects_unknown_or_unsafe_names() -> None: + for name in ("missing.json", "../pyproject.toml", "/etc/passwd"): + try: + marginal.load_schema(name) + except (KeyError, ValueError): + pass + else: + raise AssertionError(f"expected schema lookup to reject {name!r}") diff --git a/tests/test_policy_regression.py b/tests/test_policy_regression.py new file mode 100644 index 0000000..5f95544 --- /dev/null +++ b/tests/test_policy_regression.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import pytest + +from marginal.budget import BudgetLedger, BudgetLimits +from marginal.estimator import ValueEstimator +from marginal.models import Action, Cost +from marginal.policy import MarginalPolicy, PolicyConfig + + +def make_policy() -> MarginalPolicy: + return MarginalPolicy( + PolicyConfig( + outcome_value_usd=1.0, + token_shadow_price_per_million_usd=10.0, + minimum_roi=1.0, + minimum_expected_gain=0.01, + target_success_probability=0.95, + ) + ) + + +def test_accepts_action_with_positive_marginal_return() -> None: + decision = make_policy().evaluate( + Action( + name="run targeted test", + kind="verification", + cost=Cost(tokens=1_000, usd=0.02), + expected_gain=0.20, + is_verification=True, + ), + BudgetLedger(BudgetLimits(max_tokens=10_000, max_usd=2.0)), + ) + assert decision.allowed + assert decision.score > 0 + assert decision.reason.startswith("approved:") + + +def test_rejects_action_with_insufficient_marginal_return() -> None: + decision = make_policy().evaluate( + Action( + name="ask another reviewer", + kind="review", + cost=Cost(tokens=5_000, usd=0.10), + expected_gain=0.02, + ), + BudgetLedger(BudgetLimits(max_tokens=10_000, max_usd=2.0)), + ) + assert not decision.allowed + assert decision.reason.startswith("rejected: marginal ROI") + + +def test_estimator_uses_observed_mean_when_action_has_no_explicit_gain() -> None: + estimator = ValueEstimator(default_gain=0.03) + estimator.observe("research", 0.10) + estimator.observe("research", 0.20) + assert estimator.estimate(Action(name="search docs", kind="research")) == pytest.approx(0.15) + + +def test_expected_gain_is_capped_by_remaining_success_probability() -> None: + policy = MarginalPolicy(PolicyConfig(outcome_value_usd=1.0, target_success_probability=0.95)) + decision = policy.evaluate( + Action( + name="small remaining upside", + kind="reasoning", + cost=Cost(usd=0.06), + expected_gain=0.50, + current_success_probability=0.90, + ), + BudgetLedger(BudgetLimits(max_usd=1.0)), + ) + assert not decision.allowed + assert decision.expected_gain == pytest.approx(0.05) diff --git a/tests/test_policy_v2.py b/tests/test_policy_v2.py new file mode 100644 index 0000000..32791b5 --- /dev/null +++ b/tests/test_policy_v2.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from marginal import Action, BudgetLimits, Cost +from marginal.budget import BudgetLedger +from marginal.policy import MarginalPolicy, PolicyConfig +from marginal.profiles import PolicyProfile, build_policy, policy_config_for_profile + + +def test_policy_has_stable_versioned_identity() -> None: + first = MarginalPolicy(PolicyConfig(minimum_roi=1.2), name="reference", version="2.0.0") + second = MarginalPolicy(PolicyConfig(minimum_roi=1.2), name="reference", version="2.0.0") + assert first.identity == second.identity + assert first.identity.config_hash + + +def test_policy_decision_contains_structured_reason_and_estimator_metadata() -> None: + policy = MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)) + decision = policy.evaluate( + Action(name="verify", kind="verification", cost=Cost(tokens=10), expected_gain=0.2), + BudgetLedger(BudgetLimits(max_tokens=100)), + ) + assert decision.allowed + assert decision.reason_code == "APPROVED" + assert decision.confidence == 1.0 + assert decision.estimator_version + + +def test_budget_rejection_has_stable_reason_code() -> None: + policy = MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)) + decision = policy.evaluate( + Action(name="large", kind="tool", cost=Cost(tokens=101), expected_gain=0.2), + BudgetLedger(BudgetLimits(max_tokens=100)), + ) + assert not decision.allowed + assert decision.reason_code == "BUDGET_REJECTED" + + +def test_reference_profiles_are_distinct_and_buildable() -> None: + configs = [policy_config_for_profile(profile) for profile in PolicyProfile] + assert len({config.token_shadow_price_per_million_usd for config in configs}) == len(configs) + policy = build_policy("balanced") + assert policy.identity.name == "profile:balanced" + + +def test_policy_rejects_non_string_identity_fields() -> None: + import pytest + + with pytest.raises(TypeError, match="name"): + MarginalPolicy(name=123) # type: ignore[arg-type] + with pytest.raises(TypeError, match="version"): + MarginalPolicy(version=123) # type: ignore[arg-type] diff --git a/tests/test_privacy.py b/tests/test_privacy.py new file mode 100644 index 0000000..1738160 --- /dev/null +++ b/tests/test_privacy.py @@ -0,0 +1,469 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from marginal.privacy import ( + FIELD_CLASSIFICATION, + LocalPseudonymizer, + PrivacyClass, + PrivacyConfig, + PrivacyProfile, + aggregate_ledger_records, + classify_field, + generate_local_identifier, + load_or_create_privacy_key, + sanitize_ledger_record, + validate_safe_telemetry_record, +) + + +def _sensitive_record() -> dict[str, object]: + return { + "schema_version": "2.0", + "event_id": "event-123", + "sequence": 1, + "timestamp": "2026-08-06T08:37:42+00:00", + "run_id": "customer-acme-contract-2026", + "task_id": "customer-acme-contract-2026", + "trajectory_id": "secret-trajectory", + "engine": "codex", + "model": "internal-legal-model", + "event": "authorization", + "mode": "shadow", + "privacy_profile": "local_full", + "policy": {"name": "balanced", "version": "2.0.0", "config_hash": "abc"}, + "estimator": { + "name": "historical-mean", + "version": "2.0.0", + "config_hash": "def", + "training_data_fingerprint": "training-secret", + }, + "treasury": "customer-acme", + "action": { + "name": "review termination clause", + "kind": "verification", + "cost": {"tokens": 1200, "usd": 0.01, "latency_ms": 200, "risk": 0.0}, + "expected_gain": 0.2, + "current_success_probability": 0.5, + "is_verification": True, + "fingerprint": "guessable-fingerprint", + "metadata": { + "repository": "secret-merger-project", + "tool_arguments": {"file": "contracts/acme.txt"}, + }, + }, + "decision": { + "allowed": True, + "recommended": False, + "reason": "shadow mode allowed confidential review", + "reason_code": "SHADOW_OVERRIDE", + "recommendation_reason": "expected value too low for Acme contract", + "recommendation_reason_code": "ROI_BELOW_MINIMUM", + "mode": "shadow", + "score": -0.1, + "expected_gain": 0.2, + "estimated_cost_value": 0.3, + "uncertainty": 0.05, + "confidence": 0.8, + "estimator_name": "historical-mean", + "estimator_version": "2.0.0", + }, + "usage": {"tokens": 1200, "usd": 0.01, "latency_ms": 200, "risk": 0.0}, + "reason": "RuntimeError: customer Acme file unavailable", + } + + +def test_privacy_profile_parses_supported_values() -> None: + assert PrivacyProfile.parse("local-full") is PrivacyProfile.LOCAL_FULL + assert PrivacyProfile.parse("safe_telemetry") is PrivacyProfile.SAFE_TELEMETRY + assert PrivacyProfile.parse(PrivacyProfile.AGGREGATE_EXPORT) is PrivacyProfile.AGGREGATE_EXPORT + with pytest.raises(ValueError, match="unknown privacy profile"): + PrivacyProfile.parse("anonymous") + + +def test_field_classification_covers_representative_categories() -> None: + assert FIELD_CLASSIFICATION["action.kind"] is PrivacyClass.SAFE_BY_DEFAULT + assert FIELD_CLASSIFICATION["task_id"] is PrivacyClass.PSEUDONYMOUS + assert FIELD_CLASSIFICATION["action.name"] is PrivacyClass.POTENTIALLY_SENSITIVE + assert FIELD_CLASSIFICATION["outcome.verifier"] is PrivacyClass.POTENTIALLY_SENSITIVE + + +def test_privacy_config_repr_does_not_expose_key_material() -> None: + config = PrivacyConfig(profile="safe_telemetry", key=b"secret-key-material" * 2) + assert "secret-key-material" not in repr(config) + + +def test_local_identifier_is_random_opaque_and_namespaced() -> None: + first = generate_local_identifier("task") + second = generate_local_identifier("task") + assert first.startswith("task_") + assert second.startswith("task_") + assert first != second + assert len(first) >= 24 + with pytest.raises(ValueError, match="namespace"): + generate_local_identifier("customer acme") + + +def test_local_pseudonymizer_is_deterministic_and_field_separated() -> None: + pseudonymizer = LocalPseudonymizer(b"a" * 32) + first = pseudonymizer.pseudonymize("task_id", "customer-acme") + second = pseudonymizer.pseudonymize("task_id", "customer-acme") + different_field = pseudonymizer.pseudonymize("run_id", "customer-acme") + + assert first == second + assert first != different_field + assert first.startswith("psn_") + assert "customer" not in first + + +def test_different_keys_produce_unlinkable_pseudonyms() -> None: + left = LocalPseudonymizer(b"a" * 32) + right = LocalPseudonymizer(b"b" * 32) + assert left.pseudonymize("task_id", "same") != right.pseudonymize("task_id", "same") + + +def test_safe_telemetry_removes_free_text_and_pseudonymizes_identifiers() -> None: + sanitized = sanitize_ledger_record( + _sensitive_record(), + profile=PrivacyProfile.SAFE_TELEMETRY, + pseudonymizer=LocalPseudonymizer(b"k" * 32), + ) + + encoded = json.dumps(sanitized, sort_keys=True) + assert sanitized["privacy_profile"] == "safe_telemetry" + assert sanitized["run_id"].startswith("psn_") + assert sanitized["task_id"].startswith("psn_") + assert sanitized["trajectory_id"].startswith("psn_") + assert sanitized["event_id"].startswith("psn_") + assert sanitized["timestamp"] == "2026-08-06T00:00:00+00:00" + assert sanitized["engine"] == "codex" + assert "model" not in sanitized + assert "treasury" not in sanitized + assert "name" not in sanitized["action"] + assert "metadata" not in sanitized["action"] + assert sanitized["action"]["fingerprint"].startswith("psn_") + assert "reason" not in sanitized["decision"] + assert "recommendation_reason" not in sanitized["decision"] + assert sanitized["decision"]["reason_code"] == "SHADOW_OVERRIDE" + assert "reason" not in sanitized + for secret in ( + "customer-acme", + "internal-legal-model", + "termination clause", + "secret-merger-project", + "RuntimeError", + ): + assert secret not in encoded + + +def test_safe_telemetry_keeps_outcome_structure_but_removes_verifier_and_evidence() -> None: + record = { + **_sensitive_record(), + "event": "outcome", + "outcome": { + "task_id": "customer-acme-contract-2026", + "reward": 1.0, + "resolved": True, + "verifier": "internal legal verifier", + "trajectory_id": "secret-trajectory", + "evidence": {"document": "merger.pdf"}, + "metrics": {"clauses_reviewed": 12}, + }, + } + sanitized = sanitize_ledger_record( + record, + profile="safe_telemetry", + pseudonymizer=LocalPseudonymizer(b"k" * 32), + ) + outcome = sanitized["outcome"] + assert outcome == { + "task_id": sanitized["task_id"], + "reward": 1.0, + "resolved": True, + "trajectory_id": sanitized["trajectory_id"], + } + + +def test_local_full_returns_an_independent_complete_copy() -> None: + original = _sensitive_record() + sanitized = sanitize_ledger_record(original, profile="local_full") + assert sanitized == original + assert sanitized is not original + + +def test_safe_telemetry_requires_a_pseudonymizer() -> None: + with pytest.raises(ValueError, match="pseudonymizer"): + sanitize_ledger_record(_sensitive_record(), profile="safe_telemetry") + + +def test_local_key_is_created_once_with_32_bytes(tmp_path: Path) -> None: + path = tmp_path / "privacy.key" + first = load_or_create_privacy_key(path) + second = load_or_create_privacy_key(path) + assert first == second + assert len(first) == 32 + assert path.read_bytes() == first + if hasattr(path.stat(), "st_mode"): + assert path.stat().st_mode & 0o077 == 0 + + +def test_aggregate_export_groups_generalized_records_without_identifiers() -> None: + first = _sensitive_record() + second = _sensitive_record() + second["event_id"] = "event-456" + outcome = { + **_sensitive_record(), + "event_id": "event-outcome", + "event": "outcome", + "outcome": { + "task_id": "customer-acme-contract-2026", + "reward": 1.0, + "resolved": True, + "verifier": "pytest customer suite", + "trajectory_id": "secret-trajectory", + "evidence": {"repository": "secret-merger-project"}, + "metrics": {}, + }, + } + + rows = aggregate_ledger_records([first, second, outcome], minimum_group_size=1) + + assert rows == [ + { + "schema_version": "1.0", + "privacy_profile": "aggregate_export", + "record_type": "decision", + "action_kind": "verification", + "cost_bucket": "low", + "gain_bucket": "medium", + "recommendation": "deny", + "applied_decision": "allow", + "reason_code": "SHADOW_OVERRIDE", + "outcome_class": "not_applicable", + "count": 2, + "minimum_group_size": 1, + }, + { + "schema_version": "1.0", + "privacy_profile": "aggregate_export", + "record_type": "outcome", + "action_kind": "unknown", + "cost_bucket": "unknown", + "gain_bucket": "unknown", + "recommendation": "not_applicable", + "applied_decision": "not_applicable", + "reason_code": "not_applicable", + "outcome_class": "verified_success", + "count": 1, + "minimum_group_size": 1, + }, + ] + encoded = json.dumps(rows) + for secret in ( + "customer-acme", + "secret-trajectory", + "internal-legal-model", + "termination clause", + "secret-merger-project", + ): + assert secret not in encoded + + +def test_aggregate_export_suppresses_small_groups_by_default() -> None: + records = [_sensitive_record() for _ in range(4)] + for index, record in enumerate(records): + record["event_id"] = f"event-{index}" + + assert aggregate_ledger_records(records) == [] + + +def test_aggregate_export_records_and_validates_minimum_group_size() -> None: + records = [_sensitive_record() for _ in range(2)] + for index, record in enumerate(records): + record["event_id"] = f"event-{index}" + + rows = aggregate_ledger_records(records, minimum_group_size=2) + + assert rows[0]["count"] == 2 + assert rows[0]["minimum_group_size"] == 2 + with pytest.raises(TypeError, match="minimum_group_size"): + aggregate_ledger_records(records, minimum_group_size=True) + with pytest.raises(ValueError, match="at least 1"): + aggregate_ledger_records(records, minimum_group_size=0) + + +def test_existing_privacy_key_rejects_symlink_and_weak_permissions(tmp_path: Path) -> None: + import os + + target = tmp_path / "target.key" + target.write_bytes(b"k" * 32) + target.chmod(0o600) + link = tmp_path / "link.key" + try: + link.symlink_to(target) + except (OSError, NotImplementedError): + pytest.skip("symbolic links are not available") + with pytest.raises(ValueError, match="symbolic link"): + load_or_create_privacy_key(link) + + if os.name != "nt": + target.chmod(0o644) + with pytest.raises(PermissionError, match="group or others"): + load_or_create_privacy_key(target) + + +def test_existing_privacy_key_rejects_short_material(tmp_path: Path) -> None: + path = tmp_path / "short.key" + path.write_bytes(b"short") + path.chmod(0o600) + with pytest.raises(ValueError, match="at least 32 bytes"): + load_or_create_privacy_key(path) + + +def test_safe_telemetry_generalizes_unrecognized_labels_and_numeric_keys() -> None: + record = _sensitive_record() + record["event"] = "customer_acme_incident" + record["action"]["kind"] = "customer_acme_contract" # type: ignore[index] + record["action"]["cost"]["customer_id"] = 42 # type: ignore[index] + record["decision"]["reason_code"] = "CUSTOMER_ACME" # type: ignore[index] + record["decision"]["score"] = "secret-score" # type: ignore[index] + record["policy"]["version"] = "customer acme policy" # type: ignore[index] + + sanitized = sanitize_ledger_record( + record, + profile="safe_telemetry", + pseudonymizer=LocalPseudonymizer(b"k" * 32), + ) + + assert sanitized["event"] == "custom" + assert sanitized["action"]["kind"] == "other" + assert "customer_id" not in sanitized["action"]["cost"] + assert sanitized["decision"]["reason_code"] == "OTHER" + assert "score" not in sanitized["decision"] + assert sanitized["policy"]["version"] == "unknown" + assert "customer" not in json.dumps(sanitized).lower() + + +def test_every_field_has_a_classification_and_unknown_fields_default_sensitive() -> None: + from marginal.privacy import classify_field + + assert classify_field("decision.score") is PrivacyClass.SAFE_BY_DEFAULT + assert classify_field("run_id") is PrivacyClass.PSEUDONYMOUS + assert classify_field("custom.customer_name") is PrivacyClass.POTENTIALLY_SENSITIVE + with pytest.raises(TypeError): + FIELD_CLASSIFICATION["custom"] = PrivacyClass.SAFE_BY_DEFAULT # type: ignore[index] + + +def test_existing_privacy_key_is_read_from_validated_descriptor( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + path = tmp_path / "descriptor.key" + path.write_bytes(b"d" * 32) + path.chmod(0o600) + + def reject_path_read(_path: Path) -> bytes: + raise AssertionError("privacy keys must be read from the validated descriptor") + + monkeypatch.setattr(Path, "read_bytes", reject_path_read) + + assert load_or_create_privacy_key(path) == b"d" * 32 + + +def test_safe_telemetry_accepts_only_version_like_identity_strings() -> None: + record = _sensitive_record() + record["policy"]["version"] = "customer_acme_policy" # type: ignore[index] + record["estimator"]["version"] = "internal-legal-v1" # type: ignore[index] + record["decision"]["estimator_version"] = "private_model_2026" # type: ignore[index] + + sanitized = sanitize_ledger_record( + record, + profile="safe_telemetry", + pseudonymizer=LocalPseudonymizer(b"k" * 32), + ) + + assert sanitized["policy"]["version"] == "unknown" + assert sanitized["estimator"]["version"] == "unknown" + assert sanitized["decision"]["estimator_version"] == "unknown" + + +def test_field_classification_inherits_from_reviewed_parent_paths() -> None: + assert classify_field("action.cost.tokens") is PrivacyClass.SAFE_BY_DEFAULT + assert classify_field("usage.reasoning_tokens") is PrivacyClass.SAFE_BY_DEFAULT + assert classify_field("metadata.repository") is PrivacyClass.POTENTIALLY_SENSITIVE + assert classify_field("outcome.evidence.document") is PrivacyClass.POTENTIALLY_SENSITIVE + + +def test_safe_telemetry_retains_only_reviewed_safe_or_pseudonymous_fields() -> None: + record = _sensitive_record() + record["candidates"] = [ + { + "action": record["action"], + "decision": record["decision"], + "private_note": "customer-acme", + } + ] + sanitized = sanitize_ledger_record( + record, + profile="safe_telemetry", + pseudonymizer=LocalPseudonymizer(b"k" * 32), + ) + + def leaf_paths(value: object, prefix: str = "") -> list[str]: + if isinstance(value, dict): + paths: list[str] = [] + for name, item in value.items(): + child = f"{prefix}.{name}" if prefix else name + paths.extend(leaf_paths(item, child)) + return paths + if isinstance(value, list): + paths = [] + for item in value: + paths.extend(leaf_paths(item, f"{prefix}[]")) + return paths + return [prefix] + + retained = set(leaf_paths(sanitized)) + assert retained + assert { + path: classify_field(path) + for path in retained + if classify_field(path) is PrivacyClass.POTENTIALLY_SENSITIVE + } == {} + + +def test_safe_telemetry_validator_rejects_out_of_range_numeric_values() -> None: + base = sanitize_ledger_record( + _sensitive_record(), + profile="safe_telemetry", + pseudonymizer=LocalPseudonymizer(b"k" * 32), + ) + + negative_usage = json.loads(json.dumps(base)) + negative_usage["usage"]["tokens"] = -1 + with pytest.raises(ValueError, match="safe telemetry"): + validate_safe_telemetry_record(negative_usage) + + invalid_probability = json.loads(json.dumps(base)) + invalid_probability["action"]["expected_gain"] = 1.5 + with pytest.raises(ValueError, match="safe telemetry"): + validate_safe_telemetry_record(invalid_probability) + + invalid_confidence = json.loads(json.dumps(base)) + invalid_confidence["decision"]["confidence"] = 2.0 + with pytest.raises(ValueError, match="safe telemetry"): + validate_safe_telemetry_record(invalid_confidence) + + +def test_safe_telemetry_pseudonymizes_engine_instance_identifiers() -> None: + record = {**_sensitive_record(), "engine_instance": "internal-runner-acme-01"} + sanitized = sanitize_ledger_record( + record, + profile="safe_telemetry", + pseudonymizer=LocalPseudonymizer(b"k" * 32), + ) + + assert sanitized["engine_instance"].startswith("psn_") + assert "internal-runner-acme-01" not in json.dumps(sanitized) diff --git a/tests/test_protocol.py b/tests/test_protocol.py new file mode 100644 index 0000000..a4282aa --- /dev/null +++ b/tests/test_protocol.py @@ -0,0 +1,286 @@ +from __future__ import annotations + +from marginal.models import Cost, TokenUsage +from marginal.protocol import ( + AgentAction, + AgentCapabilities, + AgentEvent, + AgentEventType, + DeduplicationScope, +) + + +def test_agent_event_round_trip_preserves_normalized_action() -> None: + event = AgentEvent( + engine="codex", + session_id="session-1", + task_id="task-1", + event_type=AgentEventType.ACTION_BEFORE, + action=AgentAction( + action_id="action-1", + name="read file", + kind="file_read", + estimated_cost=Cost(tokens=100), + token_usage=TokenUsage(input_tokens=80, output_tokens=20), + expected_gain=0.2, + state_hash="state-a", + phase="diagnose", + deduplication_scope=DeduplicationScope.ONCE_PER_STATE, + ), + ) + restored = AgentEvent.from_dict(event.to_dict()) + assert restored == event + + +def test_capabilities_report_control_level() -> None: + observe = AgentCapabilities() + control = AgentCapabilities(block_actions=True, stop_agent=True) + full = AgentCapabilities( + observe_model_usage=True, + block_actions=True, + modify_actions=True, + stop_agent=True, + control_model_turns=True, + record_outcomes=True, + ) + assert observe.level == "observe" + assert control.level == "control" + assert full.level == "full" + + +def test_state_scoped_action_fingerprint_changes_with_state() -> None: + base = AgentAction( + action_id="action", + name="run test", + kind="verification", + state_hash="state-a", + deduplication_scope="once_per_state", + ) + changed = AgentAction( + action_id="action", + name="run test", + kind="verification", + state_hash="state-b", + deduplication_scope="once_per_state", + ) + assert base.core_fingerprint() != changed.core_fingerprint() + + +def test_agent_action_from_dict_rejects_string_booleans() -> None: + payload = AgentAction( + action_id="action", + name="verify", + kind="verification", + ).to_dict() + payload["is_verification"] = "false" + + try: + AgentAction.from_dict(payload) + except TypeError as exc: + assert "is_verification must be a boolean" in str(exc) + else: + raise AssertionError("expected string boolean to be rejected") + + +def test_agent_action_from_dict_rejects_coerced_numeric_fields() -> None: + payload = AgentAction( + action_id="action", + name="verify", + kind="verification", + ).to_dict() + payload["retry_number"] = "1" + + try: + AgentAction.from_dict(payload) + except TypeError as exc: + assert "retry_number must be an integer" in str(exc) + else: + raise AssertionError("expected string retry number to be rejected") + + +def test_core_decision_exposes_applied_and_recommended_directives() -> None: + from marginal.models import Decision + from marginal.protocol import AgentDecision, AgentDirective + + core = Decision( + allowed=True, + reason="shadow override", + recommended=False, + recommendation_reason="deny", + reason_code="SHADOW_OVERRIDE", + recommendation_reason_code="DENY", + mode="shadow", + ) + + decision = AgentDecision.from_core("action", core) + + assert decision.directive is AgentDirective.ALLOW + assert decision.recommended_directive is AgentDirective.DENY + + +def test_capabilities_report_support_for_protocol_directives() -> None: + from marginal.protocol import AgentDirective + + capabilities = AgentCapabilities(block_actions=True, modify_actions=True) + + assert capabilities.supports(AgentDirective.ALLOW) + assert capabilities.supports(AgentDirective.DENY) + assert capabilities.supports(AgentDirective.MODIFY) + assert not capabilities.supports(AgentDirective.STOP) + + +def test_agent_event_from_dict_rejects_coerced_identity_fields() -> None: + event = AgentEvent( + engine="codex", + session_id="session", + task_id="task", + event_type="session.start", + ).to_dict() + event["session_id"] = 123 + + try: + AgentEvent.from_dict(event) + except ValueError as exc: + assert "session_id" in str(exc) + else: + raise AssertionError("expected non-string session_id to be rejected") + + +def test_agent_event_rejects_unsupported_protocol_version() -> None: + try: + AgentEvent( + engine="codex", + session_id="session", + task_id="task", + event_type="session.start", + protocol_version="9.0", + ) + except ValueError as exc: + assert "protocol_version" in str(exc) + else: + raise AssertionError("expected unsupported protocol version to be rejected") + + +def test_agent_decision_to_dict_serializes_immutable_replacement() -> None: + from marginal.protocol import AgentDecision + + decision = AgentDecision( + action_id="action", + allowed=True, + recommended=True, + reason="approved", + reason_code="APPROVED", + recommendation_reason="approved", + recommendation_reason_code="APPROVED", + mode="shadow", + replacement={"scope": "lines"}, + ) + + payload = decision.to_dict() + + assert payload["replacement"] == {"scope": "lines"} + + +def test_agent_action_fingerprint_rejects_non_json_metadata() -> None: + action = AgentAction( + action_id="action", + name="read", + kind="file_read", + metadata={"opaque": object()}, + ) + + try: + action.core_fingerprint() + except TypeError as exc: + assert "JSON serializable" in str(exc) + else: + raise AssertionError("expected non-JSON metadata to be rejected") + + +def test_agent_decision_rejects_invalid_numeric_fields() -> None: + from marginal.protocol import AgentDecision + + try: + AgentDecision( + action_id="action", + allowed=True, + recommended=True, + reason="approved", + reason_code="APPROVED", + recommendation_reason="approved", + recommendation_reason_code="APPROVED", + mode="shadow", + confidence=2.0, + ) + except ValueError as exc: + assert "confidence" in str(exc) + else: + raise AssertionError("expected invalid confidence to be rejected") + + +def test_agent_decision_round_trip_preserves_directives_and_replacement() -> None: + from marginal.protocol import AgentDecision, AgentDirective + + original = AgentDecision( + action_id="action", + allowed=True, + recommended=False, + reason="shadow override", + reason_code="SHADOW_OVERRIDE", + recommendation_reason="deny", + recommendation_reason_code="LOW_VALUE", + mode="shadow", + directive=AgentDirective.MODIFY, + recommended_directive=AgentDirective.DENY, + replacement={"scope": "lines"}, + expected_gain=0.1, + confidence=0.8, + ) + + restored = AgentDecision.from_dict(original.to_dict()) + + assert restored == original + + +def test_agent_capabilities_round_trip_rejects_string_booleans() -> None: + original = AgentCapabilities(block_actions=True, record_outcomes=True) + restored = AgentCapabilities.from_dict(original.to_dict()) + assert restored == original + + payload = original.to_dict() + payload["block_actions"] = "true" + try: + AgentCapabilities.from_dict(payload) + except TypeError as exc: + assert "block_actions" in str(exc) + else: + raise AssertionError("expected string capability boolean to be rejected") + + +def test_agent_capabilities_rejects_inconsistent_derived_level() -> None: + payload = AgentCapabilities(block_actions=True).to_dict() + payload["level"] = "full" + + try: + AgentCapabilities.from_dict(payload) + except ValueError as exc: + assert "level" in str(exc) + else: + raise AssertionError("expected inconsistent capability level to be rejected") + + +def test_agent_event_from_dict_does_not_treat_empty_action_as_missing() -> None: + payload = AgentEvent( + engine="codex", + session_id="session", + task_id="task", + event_type="action.before", + ).to_dict() + payload["action"] = {} + + try: + AgentEvent.from_dict(payload) + except (KeyError, TypeError, ValueError): + pass + else: + raise AssertionError("expected an empty action object to be rejected") diff --git a/tests/test_public_api_v2.py b/tests/test_public_api_v2.py new file mode 100644 index 0000000..277a957 --- /dev/null +++ b/tests/test_public_api_v2.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import marginal + + +def test_v02_public_exports_and_version() -> None: + expected = { + "AgentAction", + "AgentCapabilities", + "AgentDecision", + "AgentDirective", + "AgentEvent", + "DecisionLedgerContext", + "EstimatorRegistry", + "ExecutionMode", + "JsonlDecisionLedger", + "Outcome", + "TokenUsage", + "UniversalRuntime", + "ValueEstimate", + } + assert expected.issubset(set(marginal.__all__)) + assert marginal.__version__ == "0.2.0" + + +def test_json_schemas_exist_and_are_valid() -> None: + root = Path(__file__).parents[1] + for name in [ + "agent-event-v1.json", + "agent-decision-v1.json", + "decision-ledger-v2.json", + "outcome-v1.json", + ]: + payload = json.loads((root / "schemas" / name).read_text(encoding="utf-8")) + assert payload["$schema"].startswith("https://json-schema.org/") + assert payload["title"] + + +def test_documentation_uses_consistent_v02_terms() -> None: + root = Path(__file__).parents[1] + readme = (root / "README.md").read_text(encoding="utf-8") + changelog = (root / "CHANGELOG.md").read_text(encoding="utf-8") + roadmap = (root / "ROADMAP.md").read_text(encoding="utf-8") + assert "Shadow Mode" in readme + assert "Decision Ledger" in readme + assert "0.2.0" in changelog + assert "v0.2 — Learning Loop Foundation" in roadmap + + +def test_capability_and_token_usage_schemas_are_published() -> None: + root = Path(__file__).parents[1] + for name in ["agent-capabilities-v1.json", "token-usage-v2.json"]: + payload = json.loads((root / "schemas" / name).read_text(encoding="utf-8")) + assert payload["$schema"].startswith("https://json-schema.org/") + assert payload["title"] + + +def test_privacy_public_exports_and_documentation_are_published() -> None: + expected = { + "FIELD_CLASSIFICATION", + "JsonlDecisionLedger", + "LocalPseudonymizer", + "PrivacyClass", + "PrivacyConfig", + "PrivacyProfile", + "aggregate_ledger_records", + "classify_field", + "export_decision_ledger", + "generate_local_identifier", + "load_or_create_privacy_key", + "sanitize_ledger_record", + "validate_safe_telemetry_record", + } + assert expected.issubset(set(marginal.__all__)) + + root = Path(__file__).parents[1] + privacy_doc = (root / "docs" / "privacy.md").read_text(encoding="utf-8") + readme = (root / "README.md").read_text(encoding="utf-8") + security = (root / "SECURITY.md").read_text(encoding="utf-8") + changelog = (root / "CHANGELOG.md").read_text(encoding="utf-8") + roadmap = (root / "ROADMAP.md").read_text(encoding="utf-8") + + for text in (privacy_doc, readme, security, changelog, roadmap): + assert "SAFE_TELEMETRY" in text or "safe_telemetry" in text + assert "AGGREGATE_EXPORT" in text or "aggregate_export" in text + assert "pseudonymization is not anonymization" in privacy_doc.lower() + assert (root / "examples" / "privacy_profiles.py").is_file() diff --git a/tests/test_public_eval.py b/tests/test_public_eval.py deleted file mode 100644 index a141588..0000000 --- a/tests/test_public_eval.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -from pathlib import Path - -from marginal.public_eval import compare_runs, load_runs, render_public_report - - -def _write(path: Path, rows: list[dict]) -> None: - path.write_text("\n".join(json.dumps(row) for row in rows) + "\n", encoding="utf-8") - - -def test_compare_public_runs_preserves_success_and_calculates_savings(tmp_path: Path) -> None: - baseline_path = tmp_path / "baseline.jsonl" - marginal_path = tmp_path / "marginal.jsonl" - _write( - baseline_path, - [ - { - "instance_id": "a", - "resolved": True, - "tokens": 1000, - "usd": 1.0, - "latency_ms": 100, - "tool_calls": 10, - }, - { - "instance_id": "b", - "resolved": False, - "tokens": 2000, - "usd": 2.0, - "latency_ms": 200, - "tool_calls": 20, - }, - ], - ) - _write( - marginal_path, - [ - { - "instance_id": "a", - "resolved": True, - "tokens": 500, - "usd": 0.5, - "latency_ms": 80, - "tool_calls": 6, - }, - { - "instance_id": "b", - "resolved": False, - "tokens": 1000, - "usd": 1.0, - "latency_ms": 120, - "tool_calls": 10, - }, - ], - ) - - result = compare_runs( - load_runs(baseline_path), load_runs(marginal_path), bootstrap_samples=200, seed=7 - ) - - assert result["tasks"] == 2 - assert result["baseline"]["resolved"] == 1 - assert result["marginal"]["resolved"] == 1 - assert result["savings"]["tokens_percent"] == 50.0 - assert result["quality"]["resolved_delta_pp"] == 0.0 - assert result["quality"]["preserved_within_one_pp"] is True - - -def test_compare_requires_matching_instance_ids(tmp_path: Path) -> None: - baseline_path = tmp_path / "baseline.jsonl" - marginal_path = tmp_path / "marginal.jsonl" - _write(baseline_path, [{"instance_id": "a", "resolved": True, "tokens": 1}]) - _write(marginal_path, [{"instance_id": "b", "resolved": True, "tokens": 1}]) - - try: - compare_runs(load_runs(baseline_path), load_runs(marginal_path)) - except ValueError as exc: - assert "instance IDs" in str(exc) - else: - raise AssertionError("expected ValueError") - - -def test_report_labels_results_as_measured(tmp_path: Path) -> None: - baseline_path = tmp_path / "baseline.jsonl" - marginal_path = tmp_path / "marginal.jsonl" - _write(baseline_path, [{"instance_id": "a", "resolved": True, "tokens": 100}]) - _write(marginal_path, [{"instance_id": "a", "resolved": True, "tokens": 60}]) - report = render_public_report( - compare_runs(load_runs(baseline_path), load_runs(marginal_path), bootstrap_samples=20) - ) - assert "Measured public benchmark comparison" in report - assert "40.00%" in report diff --git a/tests/test_public_eval_v2.py b/tests/test_public_eval_v2.py new file mode 100644 index 0000000..70c7e40 --- /dev/null +++ b/tests/test_public_eval_v2.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from marginal.public_eval import load_runs + + +def _write(path: Path, row: dict[str, object]) -> None: + path.write_text(json.dumps(row) + "\n", encoding="utf-8") + + +def test_public_eval_rejects_string_resolved_value(tmp_path: Path) -> None: + path = tmp_path / "runs.jsonl" + _write(path, {"instance_id": "task", "resolved": "false", "tokens": 1}) + + with pytest.raises(ValueError, match="invalid benchmark row"): + load_runs(path) + + +def test_public_eval_rejects_string_token_value(tmp_path: Path) -> None: + path = tmp_path / "runs.jsonl" + _write(path, {"instance_id": "task", "resolved": False, "tokens": "1"}) + + with pytest.raises(ValueError, match="invalid benchmark row"): + load_runs(path) + + +def test_public_eval_supports_configurable_quality_margin_and_confidence() -> None: + from marginal.public_eval import RunRecord, compare_runs + + baseline = { + f"task-{index}": RunRecord( + instance_id=f"task-{index}", + resolved=True, + tokens=100, + usd=1.0, + ) + for index in range(100) + } + marginal = { + key: RunRecord( + instance_id=key, + resolved=index >= 2, + tokens=50, + usd=0.5, + ) + for index, key in enumerate(baseline) + } + + strict = compare_runs( + baseline, + marginal, + bootstrap_samples=200, + confidence_level=0.90, + quality_margin_pp=1.0, + ) + relaxed = compare_runs( + baseline, + marginal, + bootstrap_samples=200, + confidence_level=0.90, + quality_margin_pp=2.0, + ) + + assert strict["quality"]["preserved_within_margin"] is False + assert relaxed["quality"]["preserved_within_margin"] is True + assert relaxed["quality"]["non_inferiority_margin_pp"] == 2.0 + assert relaxed["savings"]["confidence_level"] == 0.90 + assert relaxed["efficiency"]["baseline"]["tokens_per_resolved"] == 100.0 + assert relaxed["efficiency"]["marginal"]["tokens_per_resolved"] > 50.0 + + +def test_public_eval_rejects_invalid_statistical_configuration() -> None: + from marginal.public_eval import RunRecord, compare_runs + + runs = {"task": RunRecord(instance_id="task", resolved=True, tokens=1)} + for kwargs in ( + {"confidence_level": 1.0}, + {"confidence_level": 0.0}, + {"quality_margin_pp": -1.0}, + ): + with pytest.raises(ValueError): + compare_runs(runs, runs, bootstrap_samples=10, **kwargs) + + +def test_public_report_includes_efficiency_and_configured_criterion() -> None: + from marginal.public_eval import RunRecord, compare_runs, render_public_report + + baseline = {"task": RunRecord(instance_id="task", resolved=True, tokens=100, usd=1.0)} + marginal = {"task": RunRecord(instance_id="task", resolved=True, tokens=50, usd=0.5)} + + report = render_public_report( + compare_runs( + baseline, + marginal, + bootstrap_samples=20, + confidence_level=0.90, + quality_margin_pp=0.5, + ) + ) + + assert "Tokens per resolved task" in report + assert "USD per resolved task" in report + assert "90.0% bootstrap interval" in report + assert "0.50 pp non-inferiority margin" in report diff --git a/tests/test_replay.py b/tests/test_replay.py new file mode 100644 index 0000000..a640970 --- /dev/null +++ b/tests/test_replay.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from pathlib import Path + +from marginal import Action, BudgetLimits, Cost, MarginalPolicy, PolicyConfig, Treasury +from marginal.ledger import DecisionLedgerContext, JsonlDecisionLedger +from marginal.replay import render_replay_report, replay_ledger + + +def test_replay_compares_recorded_and_new_policy_without_causal_claim(tmp_path: Path) -> None: + path = tmp_path / "ledger.jsonl" + ledger = JsonlDecisionLedger(path, context=DecisionLedgerContext(run_id="run")) + permissive = MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)) + treasury = Treasury(BudgetLimits(max_tokens=100), policy=permissive, trace_sink=ledger) + action = Action(name="review", kind="review", cost=Cost(tokens=40), expected_gain=0.01) + treasury.authorize(action) + treasury.commit(action) + + strict = MarginalPolicy( + PolicyConfig( + outcome_value_usd=1.0, + token_shadow_price_per_million_usd=1000.0, + minimum_roi=2.0, + ) + ) + result = replay_ledger(path, strict, BudgetLimits(max_tokens=100)) + assert result.actions == 1 + assert result.recorded_allowed == 1 + assert result.replayed_allowed == 0 + assert result.estimated_avoided_tokens == 40 + report = render_replay_report(result) + assert "not causal proof" in report.lower() + + +def test_replay_rejects_non_boolean_recorded_recommendation(tmp_path: Path) -> None: + import json + + import pytest + + path = tmp_path / "ledger.jsonl" + path.write_text( + json.dumps( + { + "schema_version": "2.0", + "event_id": "event", + "sequence": 1, + "timestamp": "2026-08-06T00:00:00+00:00", + "run_id": "run", + "event": "authorization", + "action": { + "name": "review", + "kind": "review", + "cost": {"tokens": 10, "usd": 0.0, "latency_ms": 0, "risk": 0.0}, + "expected_gain": 0.1, + "current_success_probability": 0.0, + "is_verification": False, + "fingerprint": "fp", + "metadata": {}, + }, + "decision": {"allowed": True, "recommended": "false"}, + } + ) + + "\n", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="recommended"): + replay_ledger(path, MarginalPolicy()) + + +def test_replay_reports_malformed_authorization_as_value_error(tmp_path: Path) -> None: + import json + + import pytest + + path = tmp_path / "ledger.jsonl" + path.write_text( + json.dumps( + { + "schema_version": "2.0", + "event_id": "event", + "sequence": 1, + "timestamp": "2026-08-06T00:00:00+00:00", + "run_id": "run", + "event": "authorization", + "action": {"kind": "review", "cost": {}}, + "decision": {"allowed": True, "recommended": True}, + } + ) + + "\n", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="malformed authorization"): + replay_ledger(path, MarginalPolicy()) diff --git a/tests/test_repository_consistency_v2.py b/tests/test_repository_consistency_v2.py new file mode 100644 index 0000000..146862b --- /dev/null +++ b/tests/test_repository_consistency_v2.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def test_public_repository_identity_is_consistent() -> None: + forbidden = ("BlumFinancialLab", "github.com/BlumFinancialLab/marginal") + paths = [ + ROOT / "ACKNOWLEDGMENTS.md", + ROOT / "docs" / "governance.md", + ROOT / ".github" / "CODEOWNERS", + ROOT / ".github" / "ISSUE_TEMPLATE" / "config.yml", + ] + for path in paths: + text = path.read_text(encoding="utf-8") + assert not any(value in text for value in forbidden), path + assert "SignalLayer Labs" in text or "SignalLayerLabs" in text, path + + codemeta = json.loads((ROOT / "codemeta.json").read_text(encoding="utf-8")) + assert codemeta["version"] == "0.2.0" + assert codemeta["codeRepository"] == "https://github.com/SignalLayerLabs/Marginal" + assert codemeta["issueTracker"] == "https://github.com/SignalLayerLabs/Marginal/issues" + assert codemeta["author"]["name"] == "SignalLayer Labs" + + +def test_killer_demo_is_real_and_committed() -> None: + source = (ROOT / "src" / "marginal" / "killer_demo.py").read_text(encoding="utf-8") + assert "Local test stub" not in source + assert "render_killer_demo_html" in source + for name in ("result.json", "RESULTS.md", "index.html", "comparison.svg", "trace.jsonl"): + assert (ROOT / "demos" / "killer-demo" / name).is_file(), name + + +def test_release_workflow_matches_ci_scope() -> None: + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8") + assert "ruff format --check src tests examples" in workflow + assert "ruff check src tests examples" in workflow + + +def test_all_relative_markdown_links_resolve() -> None: + import re + + missing: list[tuple[str, str]] = [] + for path in ROOT.rglob("*.md"): + if any(part in {".git", ".pytest_cache", "dist"} for part in path.parts): + continue + text = path.read_text(encoding="utf-8") + for target in re.findall(r"\[[^\]]*\]\(([^)]+)\)", text): + normalized = target.strip().split()[0].strip("<>") + if normalized.startswith(("#", "http://", "https://", "mailto:")): + continue + normalized = normalized.split("#", 1)[0] + if normalized and not (path.parent / normalized).exists(): + missing.append((str(path.relative_to(ROOT)), normalized)) + assert not missing, missing + + +def test_apache_license_text_is_complete() -> None: + license_text = (ROOT / "LICENSE").read_text(encoding="utf-8") + assert "Apache License" in license_text + assert "Version 2.0, January 2004" in license_text + assert "TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION" in license_text + assert "END OF TERMS AND CONDITIONS" in license_text + assert len(license_text.splitlines()) > 150 + + +def test_readme_html_assets_are_committed() -> None: + import re + + readme = (ROOT / "README.md").read_text(encoding="utf-8") + sources = re.findall(r'<(?:img|source)\b[^>]*\bsrc="([^"]+)"', readme) + local_sources = [source for source in sources if not source.startswith(("http://", "https://"))] + assert local_sources + missing = [source for source in local_sources if not (ROOT / source).is_file()] + assert not missing, missing diff --git a/tests/test_runtime.py b/tests/test_runtime.py new file mode 100644 index 0000000..aae9aea --- /dev/null +++ b/tests/test_runtime.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +from pathlib import Path + +from marginal import BudgetLimits, Cost, MarginalPolicy, PolicyConfig, Treasury +from marginal.ledger import DecisionLedgerContext, JsonlDecisionLedger, read_decision_ledger +from marginal.outcomes import Outcome +from marginal.protocol import AgentAction, AgentCapabilities +from marginal.runtime import UniversalRuntime + + +def test_runtime_executes_complete_shadow_lifecycle(tmp_path: Path) -> None: + ledger = JsonlDecisionLedger( + tmp_path / "ledger.jsonl", + context=DecisionLedgerContext(run_id="run", engine="opencode"), + ) + treasury = Treasury( + BudgetLimits(max_tokens=5), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=1.0, minimum_roi=10.0)), + trace_sink=ledger, + mode="shadow", + ) + runtime = UniversalRuntime( + treasury, + engine="opencode", + session_id="session", + task_id="task", + capabilities=AgentCapabilities(block_actions=True, record_outcomes=True), + ) + action = AgentAction( + action_id="a1", + name="read repository", + kind="file_read", + estimated_cost=Cost(tokens=10), + expected_gain=0.01, + state_hash="state-1", + ) + decision = runtime.before_action(action) + assert decision.allowed + assert not decision.recommended + runtime.after_action("a1", actual_cost=Cost(tokens=8)) + runtime.record_outcome(Outcome(task_id="task", reward=1.0, resolved=True)) + assert treasury.usage.tokens == 8 + assert [record["event"] for record in read_decision_ledger(ledger.path)] == [ + "authorization", + "commit", + "outcome", + ] + + +def test_runtime_failure_with_measured_cost_settles_action() -> None: + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + ) + runtime = UniversalRuntime( + treasury, + engine="codex", + session_id="s", + task_id="t", + capabilities=AgentCapabilities(block_actions=True), + ) + runtime.before_action( + AgentAction( + action_id="a", + name="model turn", + kind="llm", + estimated_cost=Cost(tokens=50), + expected_gain=0.5, + ) + ) + runtime.fail_action("a", reason="timeout", actual_cost=Cost(tokens=20)) + assert treasury.usage.tokens == 20 + + +def test_enforce_runtime_requires_blocking_capability() -> None: + import pytest + + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + mode="enforce", + ) + + with pytest.raises(ValueError, match="block_actions"): + UniversalRuntime( + treasury, + engine="codex", + session_id="s", + task_id="t", + capabilities=AgentCapabilities(), + ) + + +def test_runtime_rejects_outcome_for_another_task() -> None: + import pytest + + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + mode="shadow", + ) + runtime = UniversalRuntime(treasury, engine="codex", session_id="s", task_id="task-a") + + with pytest.raises(ValueError, match="task_id"): + runtime.record_outcome(Outcome(task_id="task-b", reward=1.0, resolved=True)) + + +def test_runtime_adds_session_and_task_identity_to_core_action() -> None: + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + mode="shadow", + ) + runtime = UniversalRuntime(treasury, engine="codex", session_id="session", task_id="task") + + runtime.before_action( + AgentAction(action_id="a", name="read", kind="file_read", expected_gain=0.2) + ) + + pending = runtime._pending["a"] + assert pending.metadata["session_id"] == "session" + assert pending.metadata["task_id"] == "task" + + +def test_runtime_invalid_actual_cost_keeps_action_pending() -> None: + import pytest + + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + mode="shadow", + ) + runtime = UniversalRuntime(treasury, engine="codex", session_id="s", task_id="t") + runtime.before_action( + AgentAction(action_id="a", name="read", kind="file_read", expected_gain=0.2) + ) + + with pytest.raises(TypeError, match="actual_cost"): + runtime.after_action("a", actual_cost={}) # type: ignore[arg-type] + + assert runtime.pending_action_ids() == ("a",) + + +def test_runtime_invalid_failure_cost_keeps_action_pending() -> None: + import pytest + + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + mode="shadow", + ) + runtime = UniversalRuntime(treasury, engine="codex", session_id="s", task_id="t") + runtime.before_action( + AgentAction(action_id="a", name="read", kind="file_read", expected_gain=0.2) + ) + + with pytest.raises(TypeError, match="actual_cost"): + runtime.fail_action("a", reason="timeout", actual_cost={}) # type: ignore[arg-type] + + assert runtime.pending_action_ids() == ("a",) + + +def test_runtime_rejects_invalid_capabilities_type() -> None: + import pytest + + treasury = Treasury(BudgetLimits(max_tokens=100), mode="shadow") + + with pytest.raises(TypeError, match="capabilities"): + UniversalRuntime( + treasury, + engine="codex", + session_id="s", + task_id="t", + capabilities={}, # type: ignore[arg-type] + ) + + +def test_runtime_rejects_invalid_action_type_without_mutating_state() -> None: + import pytest + + runtime = UniversalRuntime( + Treasury(BudgetLimits(max_tokens=100), mode="shadow"), + engine="codex", + session_id="s", + task_id="t", + ) + + with pytest.raises(TypeError, match="action"): + runtime.before_action({}) # type: ignore[arg-type] + + assert runtime.pending_action_ids() == () diff --git a/tests/test_schema_conformance_v2.py b/tests/test_schema_conformance_v2.py new file mode 100644 index 0000000..c5a6f67 --- /dev/null +++ b/tests/test_schema_conformance_v2.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +import json +from dataclasses import asdict +from pathlib import Path + +from jsonschema import Draft202012Validator, FormatChecker + +from marginal import ( + AgentAction, + AgentCapabilities, + AgentDecision, + AgentEvent, + BudgetLimits, + Cost, + DecisionLedgerContext, + JsonlDecisionLedger, + MarginalPolicy, + Outcome, + PolicyConfig, + TokenUsage, + Treasury, + aggregate_ledger_records, +) +from marginal.ledger import read_decision_ledger + +ROOT = Path(__file__).parents[1] + + +def _schema(name: str) -> dict[str, object]: + return json.loads((ROOT / "schemas" / name).read_text(encoding="utf-8")) + + +def _validate(name: str, payload: object) -> None: + validator = Draft202012Validator(_schema(name), format_checker=FormatChecker()) + errors = sorted(validator.iter_errors(payload), key=lambda error: list(error.path)) + assert not errors, [error.message for error in errors] + + +def test_protocol_payloads_conform_to_published_schemas() -> None: + action = AgentAction( + action_id="action", + name="run test", + kind="verification", + estimated_cost=Cost(tokens=100), + token_usage=TokenUsage(input_tokens=60, cached_input_tokens=20, output_tokens=20), + expected_gain=0.2, + is_verification=True, + ) + event = AgentEvent( + engine="codex", + session_id="session", + task_id="task", + event_type="action.before", + action=action, + ) + decision = AgentDecision( + action_id="action", + allowed=True, + recommended=True, + reason="approved", + reason_code="APPROVED", + recommendation_reason="approved", + recommendation_reason_code="APPROVED", + mode="shadow", + expected_gain=0.2, + confidence=1.0, + ) + capabilities = AgentCapabilities(block_actions=True, record_outcomes=True) + + _validate("agent-event-v1.json", event.to_dict()) + _validate("agent-decision-v1.json", decision.to_dict()) + _validate("agent-capabilities-v1.json", capabilities.to_dict()) + assert action.token_usage is not None + _validate("token-usage-v2.json", asdict(action.token_usage)) + + +def test_outcome_and_ledger_records_conform_to_published_schemas(tmp_path: Path) -> None: + outcome = Outcome(task_id="task", reward=1.0, resolved=True, verifier="pytest") + _validate("outcome-v1.json", outcome.to_dict()) + + path = tmp_path / "ledger.jsonl" + ledger = JsonlDecisionLedger( + path, + context=DecisionLedgerContext(run_id="run", task_id="task", engine="codex"), + ) + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + trace_sink=ledger, + mode="shadow", + ) + treasury.record_outcome(outcome) + for record in read_decision_ledger(path): + _validate("decision-ledger-v2.json", record) + + +def test_aggregate_export_records_conform_to_published_schema() -> None: + records = [ + { + "event": "authorization", + "action": { + "kind": "verification", + "cost": {"tokens": 100, "usd": 0.0, "latency_ms": 0, "risk": 0.0}, + }, + "decision": { + "allowed": True, + "recommended": False, + "reason_code": "SHADOW_OVERRIDE", + "expected_gain": 0.2, + }, + } + ] + rows = aggregate_ledger_records(records, minimum_group_size=1) + assert len(rows) == 1 + _validate("aggregate-export-v1.json", rows[0]) + + +def test_safe_telemetry_ledger_record_conforms_to_published_schema( + tmp_path: Path, +) -> None: + path = tmp_path / "safe.jsonl" + ledger = JsonlDecisionLedger( + path, + context=DecisionLedgerContext( + run_id="customer-acme", + task_id="customer-acme", + model="internal-model", + ), + privacy_profile="safe_telemetry", + privacy_key=b"k" * 32, + ) + ledger.emit({"event": "custom", "metadata": {"repository": "secret"}}) + record = read_decision_ledger(path)[0] + _validate("decision-ledger-v2.json", record) + _validate("safe-telemetry-v1.json", record) + assert record["privacy_profile"] == "safe_telemetry" + + +def test_complete_safe_telemetry_lifecycle_conforms_to_strict_schema( + tmp_path: Path, +) -> None: + path = tmp_path / "safe-lifecycle.jsonl" + ledger = JsonlDecisionLedger( + path, + context=DecisionLedgerContext( + run_id="customer-acme", + task_id="customer-acme", + trajectory_id="secret-trajectory", + engine="codex", + model="internal-model", + ), + privacy_profile="safe_telemetry", + privacy_key=b"k" * 32, + ) + treasury = Treasury( + BudgetLimits(max_tokens=1_000), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + trace_sink=ledger, + mode="shadow", + ) + action = AgentAction( + action_id="customer-action", + name="review secret contract", + kind="verification", + estimated_cost=Cost(tokens=100), + expected_gain=0.2, + is_verification=True, + metadata={"repository": "secret-merger"}, + ).to_core_action(engine="codex") + + assert treasury.authorize(action).allowed + treasury.commit(action) + treasury.record_outcome( + Outcome( + task_id="customer-acme", + trajectory_id="secret-trajectory", + reward=1.0, + resolved=True, + verifier="internal-verifier", + evidence={"repository": "secret-merger"}, + ) + ) + + records = read_decision_ledger(path) + assert {record["event"] for record in records} == { + "authorization", + "commit", + "outcome", + } + for record in records: + _validate("safe-telemetry-v1.json", record) diff --git a/tests/test_shadow_mode.py b/tests/test_shadow_mode.py new file mode 100644 index 0000000..fe5fbc1 --- /dev/null +++ b/tests/test_shadow_mode.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import pytest + +from marginal import Action, BudgetLimits, Cost, MarginalPolicy, PolicyConfig, Treasury +from marginal.modes import ExecutionMode + + +def rejecting_policy() -> MarginalPolicy: + return MarginalPolicy( + PolicyConfig( + outcome_value_usd=1.0, + token_shadow_price_per_million_usd=100_000.0, + minimum_roi=10.0, + ) + ) + + +def test_shadow_mode_executes_policy_denial_and_preserves_recommendation() -> None: + treasury = Treasury(BudgetLimits(max_tokens=100), policy=rejecting_policy(), mode="shadow") + action = Action(name="expensive", kind="llm", cost=Cost(tokens=10), expected_gain=0.01) + decision = treasury.authorize(action) + assert decision.allowed is True + assert decision.recommended is False + assert decision.mode == "shadow" + assert decision.recommendation_reason_code == "MARGINAL_ROI_REJECTED" + treasury.commit(action) + assert treasury.usage.tokens == 10 + + +def test_shadow_mode_observes_hard_budget_violation_without_raising() -> None: + policy = MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)) + treasury = Treasury(BudgetLimits(max_tokens=5), policy=policy, mode=ExecutionMode.SHADOW) + action = Action(name="over", kind="tool", cost=Cost(tokens=10), expected_gain=0.5) + decision = treasury.authorize(action) + assert decision.allowed + assert decision.recommended is False + treasury.commit(action) + assert treasury.usage.tokens == 10 + assert treasury.summary()["observed_overruns"] == 1 + + +def test_shadow_pending_reservations_inform_later_recommendations() -> None: + policy = MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)) + treasury = Treasury(BudgetLimits(max_tokens=100), policy=policy, mode="shadow") + first = Action(name="first", kind="tool", cost=Cost(tokens=70), expected_gain=0.5) + second = Action(name="second", kind="tool", cost=Cost(tokens=40), expected_gain=0.5) + assert treasury.authorize(first).recommended is True + second_decision = treasury.authorize(second) + assert second_decision.allowed is True + assert second_decision.recommended is False + assert treasury.ledger.reserved_usage.tokens == 110 + + +def test_enforce_mode_preserves_denial_behavior() -> None: + treasury = Treasury(BudgetLimits(max_tokens=100), policy=rejecting_policy(), mode="enforce") + action = Action(name="expensive", kind="llm", cost=Cost(tokens=10), expected_gain=0.01) + decision = treasury.authorize(action) + assert decision.allowed is False + assert decision.recommended is False + assert not treasury.is_authorized(action) + + +def test_recommend_mode_is_nonblocking_and_identifiable() -> None: + treasury = Treasury(BudgetLimits(max_tokens=100), policy=rejecting_policy(), mode="recommend") + decision = treasury.authorize( + Action(name="review", kind="review", cost=Cost(tokens=10), expected_gain=0.01) + ) + assert decision.allowed + assert not decision.recommended + assert decision.mode == "recommend" + + +def test_observe_value_updates_estimator_without_inferring_from_outcome() -> None: + treasury = Treasury(BudgetLimits(), mode="shadow") + action = Action(name="search", kind="research") + treasury.observe_value(action, 0.4) + assert treasury.policy.estimator.estimate(action) == pytest.approx(0.4) + + +def test_shadow_mode_allows_concurrent_duplicate_actions_without_losing_accounting() -> None: + treasury = Treasury( + BudgetLimits(max_tokens=100), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + mode="shadow", + ) + action = Action(name="same call", kind="llm", cost=Cost(tokens=10), expected_gain=0.5) + + first = treasury.authorize(action) + second = treasury.authorize(action) + + assert first.allowed and first.recommended + assert second.allowed and not second.recommended + assert treasury.ledger.reserved_usage.tokens == 20 + + treasury.commit(action) + treasury.commit(action) + + assert treasury.usage.tokens == 20 + assert treasury.ledger.reserved_usage.tokens == 0 diff --git a/tests/test_trace_regression.py b/tests/test_trace_regression.py new file mode 100644 index 0000000..74074fc --- /dev/null +++ b/tests/test_trace_regression.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import json + +from marginal import Action, BudgetLimits, Cost, Treasury +from marginal.policy import MarginalPolicy, PolicyConfig +from marginal.trace import JsonlTraceSink + + +def test_jsonl_trace_writes_authorization_and_commit_events(tmp_path) -> None: + path = tmp_path / "trace.jsonl" + treasury = Treasury( + BudgetLimits(max_tokens=1_000), + policy=MarginalPolicy(PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0)), + trace_sink=JsonlTraceSink(path), + ) + action = Action(name="run tests", kind="verification", cost=Cost(tokens=10), expected_gain=0.2) + treasury.authorize(action) + treasury.commit(action) + events = [json.loads(line) for line in path.read_text().splitlines()] + assert [event["event"] for event in events] == ["authorization", "commit"] + assert events[1]["usage"]["tokens"] == 10 diff --git a/tests/test_treasury_regression.py b/tests/test_treasury_regression.py new file mode 100644 index 0000000..e0e0996 --- /dev/null +++ b/tests/test_treasury_regression.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import threading + +import pytest + +from marginal import Action, AuthorizationRequired, BudgetLimits, Cost, Treasury +from marginal.policy import MarginalPolicy, PolicyConfig + + +def permissive_policy() -> MarginalPolicy: + return MarginalPolicy( + PolicyConfig(outcome_value_usd=10.0, minimum_roi=0.0, target_success_probability=1.0) + ) + + +def test_authorize_then_commit_consumes_budget_and_blocks_duplicate() -> None: + treasury = Treasury(BudgetLimits(max_tokens=1_000), policy=permissive_policy()) + action = Action(name="search docs", kind="research", cost=Cost(tokens=100), expected_gain=0.1) + assert treasury.authorize(action).allowed + treasury.commit(action) + assert treasury.usage.tokens == 100 + assert not treasury.authorize(action).allowed + + +def test_child_commit_is_charged_to_child_and_parent() -> None: + parent = Treasury(BudgetLimits(max_tokens=1_000), policy=permissive_policy()) + child = parent.child("research", BudgetLimits(max_tokens=300)) + action = Action(name="first", kind="research", cost=Cost(tokens=200), expected_gain=0.1) + assert child.authorize(action).allowed + child.commit(action) + assert child.usage.tokens == 200 + assert parent.usage.tokens == 200 + + +def test_parallel_authorizations_cannot_oversubscribe_shared_budget() -> None: + account = Treasury(BudgetLimits(max_tokens=100), policy=permissive_policy()) + barrier = threading.Barrier(3) + decisions: list[bool] = [] + lock = threading.Lock() + + def authorize(action: Action) -> None: + barrier.wait() + result = account.authorize(action).allowed + with lock: + decisions.append(result) + + threads = [ + threading.Thread( + target=authorize, + args=( + Action( + name="parallel 70", + kind="tool", + cost=Cost(tokens=70), + expected_gain=0.1, + ), + ), + ), + threading.Thread( + target=authorize, + args=( + Action( + name="parallel 40", + kind="tool", + cost=Cost(tokens=40), + expected_gain=0.1, + ), + ), + ), + ] + for thread in threads: + thread.start() + barrier.wait() + for thread in threads: + thread.join() + assert sorted(decisions) == [False, True] + + +def test_sibling_cannot_settle_another_treasurys_action() -> None: + root = Treasury(BudgetLimits(max_tokens=1_000), policy=permissive_policy()) + first = root.child("first", BudgetLimits(max_tokens=500)) + second = root.child("second", BudgetLimits(max_tokens=500)) + action = Action(name="owned", kind="tool", cost=Cost(tokens=100), expected_gain=0.1) + assert first.authorize(action).allowed + with pytest.raises(AuthorizationRequired): + second.commit(action) From 3b5ee9f8e3f866e5f699d792a4cce8c804ec6b32 Mon Sep 17 00:00:00 2001 From: Renato Date: Thu, 6 Aug 2026 10:40:35 +0200 Subject: [PATCH 2/3] docs: emphasize hard budget slogan in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3ac1078..d44bd86 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ MARGINAL is an open-source decision, accounting, and evidence layer for AI agents. It evaluates proposed model calls, tool calls, searches, retries, reviewers, and sub-agents before they run, then accounts for what actually happened. +> **Hard budgets prevent bankruptcy. MARGINAL optimizes investment returns.** + > **Hard budgets ask “can we afford this?” MARGINAL also asks “is this worth funding?”** Version `0.2.0` adds the **Learning Loop Foundation**: Shadow Mode, a versioned Decision Ledger, explicit privacy profiles, measured outcome contracts, versioned value estimators, policy replay, and a universal engine-neutral runtime for future Codex, Claude Code, GitHub Copilot, OpenCode, and other adapters. From 1a1a3aa5b1b5a906a1d12e96fea5469905e73b01 Mon Sep 17 00:00:00 2001 From: Renato Date: Thu, 6 Aug 2026 11:17:14 +0200 Subject: [PATCH 3/3] docs: redesign README and launch product website --- .github/workflows/pages.yml | 65 ++ CHANGELOG.md | 6 +- MARGINAL-readme-github-pages.zip | Bin 0 -> 21449 bytes PACKAGE_MANIFEST.json | 22 + README.md | 609 +++++------------- VISUAL_STUDIO_COMMIT_PROMPT.md | 47 ++ .../plans/2026-08-06-readme-pages.md | 17 + .../specs/2026-08-06-readme-pages-design.md | 19 + docs/website.md | 50 ++ scripts/validate_readme_pages.py | 126 ++++ site/404.html | 1 + site/app.js | 2 + site/index.html | 146 +++++ site/robots.txt | 4 + site/sitemap.xml | 2 + site/styles.css | 19 + 16 files changed, 701 insertions(+), 434 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 MARGINAL-readme-github-pages.zip create mode 100644 PACKAGE_MANIFEST.json create mode 100644 VISUAL_STUDIO_COMMIT_PROMPT.md create mode 100644 docs/superpowers/plans/2026-08-06-readme-pages.md create mode 100644 docs/superpowers/specs/2026-08-06-readme-pages-design.md create mode 100644 docs/website.md create mode 100644 scripts/validate_readme_pages.py create mode 100644 site/404.html create mode 100644 site/app.js create mode 100644 site/index.html create mode 100644 site/robots.txt create mode 100644 site/sitemap.xml create mode 100644 site/styles.css diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..99f457a --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,65 @@ +name: Deploy MARGINAL website + +on: + push: + branches: ["main"] + paths: + - "site/**" + - "assets/marginal-readme-hero.png" + - "demos/killer-demo/**" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Configure Pages + uses: actions/configure-pages@v5 + - name: Assemble website + shell: bash + run: | + set -euo pipefail + rm -rf _site + mkdir -p _site/assets _site/demo + cp -R site/. _site/ + cp assets/marginal-readme-hero.png _site/assets/marginal-readme-hero.png + cp -R demos/killer-demo/. _site/demo/ + touch _site/.nojekyll + - name: Verify generated website + shell: bash + run: | + set -euo pipefail + test -f _site/index.html + test -f _site/styles.css + test -f _site/app.js + test -f _site/robots.txt + test -f _site/sitemap.xml + test -f _site/assets/marginal-readme-hero.png + test -f _site/demo/RESULTS.md + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fcc882..c144e26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,6 @@ All notable changes to MARGINAL are documented here. The project follows Semanti - provider-neutral `Outcome` contract separated from action-level realized gain; - schema-versioned `JsonlDecisionLedger` with strict envelope validation, run/task/trajectory/engine/model correlation, task/outcome consistency checks, and monotonic sequencing; - `PrivacyProfile` with `local_full`, keyed `safe_telemetry`, and separate `aggregate_export` modes; -- privacy-preserving aggregate export with a configurable minimum group size of five by default; - field classification for safe-by-default, pseudonymous, and potentially sensitive evidence; - HMAC-SHA-256 identifier pseudonymization, UTC-day timestamp generalization, strict free-text removal, and local 256-bit key management; - opaque random local identifier generation for runs, tasks, and other caller-defined namespaces; @@ -35,6 +34,11 @@ All notable changes to MARGINAL are documented here. The project follows Semanti ### Changed +- redesigned the repository README as a concise, SEO-oriented technical landing page; +- added a dependency-free, responsive, accessible GitHub Pages product website; +- consolidated the product website, hero asset, and Killer Demo into one Pages deployment; +- documented website ownership, deployment, accessibility, privacy, and evidence guardrails. + - `Decision` is backward compatible but now carries recommendation, mode, reason-code, uncertainty, confidence, and estimator metadata; - `MarginalPolicy` now has a stable identity and supports both versioned and legacy custom estimators; - `Treasury.summary()` includes mode, policy, estimator, observed overruns, failed settlements, outcomes, and estimator observations; diff --git a/MARGINAL-readme-github-pages.zip b/MARGINAL-readme-github-pages.zip new file mode 100644 index 0000000000000000000000000000000000000000..f8032b1826eb1c090302096d0d9abfc02c2681c2 GIT binary patch literal 21449 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 literal 0 HcmV?d00001 diff --git a/PACKAGE_MANIFEST.json b/PACKAGE_MANIFEST.json new file mode 100644 index 0000000..aa212ac --- /dev/null +++ b/PACKAGE_MANIFEST.json @@ -0,0 +1,22 @@ +{ + "package_type": "repository overlay", + "repository": "SignalLayerLabs/Marginal", + "generated_on": "2026-08-06", + "files": [ + ".github/workflows/pages.yml", + "CHANGELOG.md", + "README.md", + "VISUAL_STUDIO_COMMIT_PROMPT.md", + "docs/superpowers/plans/2026-08-06-readme-pages.md", + "docs/superpowers/specs/2026-08-06-readme-pages-design.md", + "docs/website.md", + "scripts/validate_readme_pages.py", + "site/404.html", + "site/app.js", + "site/index.html", + "site/robots.txt", + "site/sitemap.xml", + "site/styles.css" + ], + "important": "Ensure exactly one workflow invokes actions/deploy-pages." +} \ No newline at end of file diff --git a/README.md b/README.md index d44bd86..032edf0 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,94 @@
-MARGINAL — Compute capital allocation for AI agents +MARGINAL — compute governance and token optimization for AI agents -
+# MARGINAL + +### Compute governance and token optimization for AI agents + +**MARGINAL helps AI agents decide whether the next model call, tool call, search, retry, review, or sub-agent is worth its compute cost.** + +Open source · Local first · Provider neutral · Zero mandatory runtime dependencies + +[Website](https://signallayerlabs.github.io/Marginal/) · +[Quickstart](docs/quickstart.md) · +[Architecture](docs/architecture.md) · +[Roadmap](ROADMAP.md) · +[Contributing](CONTRIBUTING.md) [![CI](https://github.com/SignalLayerLabs/Marginal/actions/workflows/ci.yml/badge.svg)](https://github.com/SignalLayerLabs/Marginal/actions/workflows/ci.yml) [![CodeQL](https://github.com/SignalLayerLabs/Marginal/actions/workflows/codeql.yml/badge.svg)](https://github.com/SignalLayerLabs/Marginal/actions/workflows/codeql.yml) [![Release](https://img.shields.io/github/v/release/SignalLayerLabs/Marginal?style=flat-square)](https://github.com/SignalLayerLabs/Marginal/releases) [![Python 3.10–3.13](https://img.shields.io/badge/python-3.10--3.13-blue.svg?style=flat-square)](https://www.python.org/) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-green.svg?style=flat-square)](LICENSE) -[![Zero runtime dependencies](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen.svg?style=flat-square)](pyproject.toml) +[![Runtime dependencies: 0](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen.svg?style=flat-square)](pyproject.toml)
--- -MARGINAL is an open-source decision, accounting, and evidence layer for AI agents. It evaluates proposed model calls, tool calls, searches, retries, reviewers, and sub-agents before they run, then accounts for what actually happened. - -> **Hard budgets prevent bankruptcy. MARGINAL optimizes investment returns.** - -> **Hard budgets ask “can we afford this?” MARGINAL also asks “is this worth funding?”** - -Version `0.2.0` adds the **Learning Loop Foundation**: Shadow Mode, a versioned Decision Ledger, explicit privacy profiles, measured outcome contracts, versioned value estimators, policy replay, and a universal engine-neutral runtime for future Codex, Claude Code, GitHub Copilot, OpenCode, and other adapters. - -MARGINAL does not compress prompts or replace an agent framework. It can eliminate entire low-value actions before they consume tokens, or observe them without interference while evidence is collected. - -## Why this exists +## Why MARGINAL -Agent runtimes commonly execute the next step because it appears in a workflow, a model requested it, or a hard limit has not yet been reached. Those mechanisms answer whether work is permitted; they do not compare the expected improvement of the next action with its total economic cost. +Most agent runtimes ask: -MARGINAL adds that allocation layer while keeping execution and evidence explicit: +> **Can this action run?** -- rank candidate actions by marginal value; -- reserve budget before execution and settle actual usage afterward; -- protect verification capacity; -- prevent concurrent double-spend and state-insensitive duplicates; -- observe recommendations without blocking through Shadow Mode; -- record versioned policy, estimator, cost, failure, and outcome evidence; -- protect quasi-identifiers and free text through explicit privacy profiles; -- support provider-neutral synchronous, asynchronous, and engine-adapter integrations; -- keep the runtime core free of mandatory dependencies. +MARGINAL adds the economic question: -## What changed in v0.2 +> **Is this action worth funding now?** -### Shadow Mode - -Shadow Mode evaluates every proposed action but never blocks it: +It evaluates expected improvement against tokens, direct cost, latency, risk, remaining budget, verification reserves, and prior evidence. It then records what actually happened so future policies can be evaluated against real outcomes. ```text -agent proposes action - ↓ -MARGINAL recommends allow or deny - ↓ -action still executes - ↓ -actual cost and verified outcome are recorded +observe decisions + ↓ +measure actual cost and outcomes + ↓ +estimate marginal value + ↓ +allocate compute + ↓ +measure calibration and regret + ↓ +improve the policy ``` -This is the safe default for collecting evidence before enforcement. - -### Decision Ledger - -`JsonlDecisionLedger` records schema-versioned, append-only evidence with: - -- run, task, trajectory, engine, and model identity; -- policy and estimator versions; -- recommended versus applied decisions; -- estimated and actual costs; -- failures, overruns, observations, and outcomes; -- deterministic sequence numbers for replay and audit. - -Prompts and model outputs are not recorded by default. That alone is not sufficient for safe -sharing: task IDs, action names, model identity, repository metadata, verifier details, and -error text can still reveal sensitive information. - -### Privacy profiles - -Every Decision Ledger field is treated as safe-by-default, pseudonymous, or potentially -sensitive. MARGINAL provides three explicit profiles: - -- `LOCAL_FULL` preserves the complete operational ledger on a trusted local filesystem; -- `SAFE_TELEMETRY` removes free text and metadata, pseudonymizes identifiers with a local - HMAC-SHA-256 key, and generalizes exact timestamps; -- `generate_local_identifier(...)` creates opaque random local IDs when correlation with - external names is unnecessary; -- `AGGREGATE_EXPORT` creates grouped generalized rows with no identifiers or timestamps and - suppresses groups smaller than five records by default. - -`aggregate_export` is a separate export path, not an operational ledger mode. Its default -minimum group size is five and can be raised for more conservative sharing. Pseudonymization -is not anonymization; inspect data before sharing it. See [Privacy profiles](docs/privacy.md). +The goal is not to make an agent merely cheaper. It is to make autonomous work **economically disciplined, measurable, and auditable**. -### Versioned Value Estimator +MARGINAL also supports Shadow Mode, where recommendations can be observed without blocking execution, letting teams collect evidence before enforcement. -`ValueEstimator` now returns `ValueEstimate` objects with expected gain, uncertainty, confidence, sample size, provenance, and a stable estimator identity. Explicit caller estimates remain supported, and historical observations can be contextualized by engine, phase, task type, language, and model. +## How it works -MARGINAL does **not** infer that every action caused a successful task. Action-level realized gain must be supplied explicitly through `Treasury.observe_value(...)`. - -### Universal Agent Protocol - -`AgentAction`, `AgentEvent`, `AgentDecision`, `AgentCapabilities`, and `UniversalRuntime` provide the shared contract for thin engine adapters. Engine-specific code translates native events; the economic policy remains in one core. +```text +Agent proposes an action + ↓ +MARGINAL estimates value and total cost + ↓ +ALLOW · DENY · RECOMMEND · SHADOW + ↓ +Budget is reserved before execution + ↓ +Actual usage and verified outcome are settled + ↓ +Versioned evidence is written to the Decision Ledger +``` -The versioned JSON contracts ship inside the installed package as well as in the repository: +MARGINAL can sit around model calls, tool calls, searches, tests, reviewers, retries, and sub-agents. The core remains engine-neutral; thin adapters translate native events from coding agents into one universal protocol. -```python -from marginal import available_schemas, load_schema +## What makes it different -print(available_schemas()) -event_schema = load_schema("agent-event-v1.json") -``` +| Capability | What it adds | +|---|---| +| **Transactional accounting** | Atomic reserve, settle, abort, overrun, hierarchy, and verification reserves—not a token counter wrapper. | +| **Shadow-to-enforce lifecycle** | Observe recommendations safely before allowing the policy to block work. | +| **Learning Loop Foundation** | Versioned estimators, outcomes, replay, and evidence for progressively better allocation. | +| **Privacy by design** | Local ledgers, pseudonymous telemetry, aggregate exports, and no prompt/output logging by default. | +| **Universal agent protocol** | One core for Codex, Claude Code, GitHub Copilot, OpenCode, and future compatible runtimes. | +| **Scientific honesty** | Synthetic demonstrations are labeled as demonstrations; public claims require measured telemetry and preserved quality. | ## Install -Install the tagged GitHub release: +Install the tagged release: ```bash pip install "marginal-ai @ git+https://github.com/SignalLayerLabs/Marginal.git@v0.2.0" @@ -126,23 +102,22 @@ cd Marginal python -m pip install -e ".[dev]" ``` -## Five-minute enforced integration +## Quickstart ```python from marginal import Action, BudgetLimits, Cost, Treasury, budgeted_call, build_policy -policy = build_policy("balanced") treasury = Treasury( BudgetLimits( max_tokens=100_000, max_usd=2.00, verification_reserve_tokens=10_000, ), - policy=policy, + policy=build_policy("balanced"), mode="enforce", ) -response = budgeted_call( +result = budgeted_call( treasury, your_expensive_function, "input", @@ -155,136 +130,41 @@ response = budgeted_call( ) ``` -The wrapped function is never called when enforcement denies the action. Approved estimates are reserved immediately, preventing parallel actions from oversubscribing the same treasury. - -## Fund the best next action - -```python -allocation = treasury.fund_best( - [ - Action( - name="search the web", - kind="research", - cost=Cost(tokens=6_000, usd=0.06), - expected_gain=0.05, - ), - Action( - name="run the targeted test", - kind="verification", - cost=Cost(tokens=800, usd=0.002), - expected_gain=0.18, - is_verification=True, - ), - ] -) - -if allocation is not None: - result = funded_call(treasury, allocation, execute, allocation.action) -``` - -`fund_best` evaluates candidates against one locked state, records the full ranking, and reserves only the highest-value affordable candidate. It remains an active selection API in every execution mode. +When enforcement denies the action, the wrapped function is not called. Approved estimates are reserved immediately, preventing concurrent actions from oversubscribing the same treasury. -## Use measured provider usage +Start with [`shadow` mode](docs/quickstart.md) when collecting evidence for a new workflow. -```python -response = budgeted_call( - treasury, - client.responses.create, - action=action, - usage_extractor=extract_common_llm_usage, - model="YOUR_MODEL", - input="Draft the answer.", -) -``` +## Execution modes -The total-token extractor preserves direct USD, latency, and risk values that a provider response does not expose consistently. `extract_common_token_usage` provides the additive input, cached-input, non-reasoning output, reasoning, and total breakdown. +| Mode | Applied behavior | Best for | +|---|---|---| +| `shadow` | Executes every proposed action and records the recommendation | Safe observation and calibration | +| `recommend` | Executes while surfacing the policy recommendation | Human or agent advisory workflows | +| `enforce` | Applies allow/deny and hard-budget decisions | Validated production control | -Async callables use the same lifecycle: +`fund_best(...)` remains an active selection API in every mode because the caller is explicitly asking MARGINAL to choose among alternatives. -```python -response = await async_budgeted_call( - treasury, - client.responses.create, - action=action, - usage_extractor=extract_common_llm_usage, - **request, -) -``` +## The Learning Loop Foundation -## Start safely with Shadow Mode +MARGINAL v0.2 moves beyond manually supplied expected gain without pretending causal inference is already solved. -```python -from marginal import ( - Action, - BudgetLimits, - Cost, - DecisionLedgerContext, - JsonlDecisionLedger, - Treasury, - budgeted_call, - build_policy, -) +The current foundation provides: -ledger = JsonlDecisionLedger( - "marginal-ledger.jsonl", - context=DecisionLedgerContext( - run_id="run-001", - task_id="task-042", - trajectory_id="baseline-a", - engine="codex", - model="your-model", - ), - privacy_profile="safe_telemetry", - privacy_key_path=".marginal/privacy.key", -) +- versioned `ValueEstimator` identities and configurations; +- contextual observations by engine, phase, task type, language, and model; +- uncertainty, confidence, sample size, and provenance; +- task-level verified outcomes; +- separately supplied action-level realized gain; +- policy replay over recorded actions; +- deterministic Decision Ledger records for audit and comparison. -treasury = Treasury( - BudgetLimits(max_tokens=50_000, verification_reserve_tokens=5_000), - policy=build_policy("quality-first"), - trace_sink=ledger, - mode="shadow", -) +A successful task does **not** prove that every action in its trajectory caused success. MARGINAL keeps task outcomes and action attribution separate so correlation is not mislabeled as causal value. -result = budgeted_call( - treasury, - your_expensive_function, - action=Action( - name="ask another reviewer", - kind="review", - cost=Cost(tokens=5_000), - expected_gain=0.01, - ), -) -``` +Read [Learning and replay](docs/concepts.md) and [Benchmarking](docs/benchmarking.md). -Even when the policy recommendation is deny, Shadow Mode executes the callable, records the non-blocking override, and accounts for actual usage. +## Universal Agent Runtime -## Record verified outcomes and action-level learning - -```python -from marginal import Action, Outcome - -# Task outcome: evidence about the trajectory as a whole. -treasury.record_outcome( - Outcome( - task_id="task-042", - reward=1.0, - resolved=True, - verifier="pytest", - evidence={"suite": "tests/test_payment.py"}, - ) -) - -# Action-level realized gain: supplied only when the application can justify it. -treasury.observe_value( - Action(name="run targeted test", kind="verification"), - realized_gain=0.18, -) -``` - -Task outcomes and action-level realized gain are deliberately separate. A passing task alone does not establish the causal value of every action in its trajectory. - -## Universal runtime for engine adapters +The universal runtime gives engine adapters one shared contract: ```python from marginal import AgentAction, AgentCapabilities, Cost, UniversalRuntime @@ -313,282 +193,145 @@ decision = runtime.before_action( deduplication_scope="once_per_state", ) ) - -# The adapter applies the decision, executes when allowed, then settles actual usage. -runtime.after_action("read-1", actual_cost=Cost(tokens=6_400)) ``` -`UniversalRuntime` in `enforce` mode requires an adapter that declares `block_actions=True`; MARGINAL refuses to advertise enforcement through an observe-only integration. - -Protocol v1 defines `allow`, `deny`, `modify`, `defer`, `reuse`, `stop`, and `force_verify` directives so adapters can negotiate future control surfaces consistently. The v0.2 reference runtime currently derives `allow` and `deny` from the core decision; richer directives remain adapter and policy extension points and are not claimed as automatic behavior. - -## Measured token breakdown - -```python -from marginal import extract_common_token_usage - -usage = extract_common_token_usage(response) -print(usage.input_tokens) -print(usage.cached_input_tokens) -print(usage.output_tokens) -print(usage.reasoning_tokens) -print(usage.total_tokens) -``` +The protocol defines consistent events, capabilities, usage fields, decisions, settlement, and outcome contracts. Vendor-specific adapters are developed as thin integrations; they do not duplicate the economic policy. -The normalized fields are additive: `input_tokens` means uncached input, while cached input is reported separately. +### Integration status -## Failed calls that still consumed resources - -Some provider calls fail after compute was consumed. Supply a failure usage extractor to keep accounting truthful while preserving the original exception: - -```python -result = budgeted_call( - treasury, - client.responses.create, - action=action, - failure_usage_extractor=lambda error, estimate: measured_or_best_known_cost(error), - **request, -) -``` - -Returning `None` means no external spend was observed and releases the reservation. Returning `Cost` settles the failed action. - -## Policy replay - -```bash -marginal ledger-validate marginal-ledger.jsonl -marginal ledger-report marginal-ledger.jsonl -marginal replay marginal-ledger.jsonl --profile balanced -marginal ledger-export marginal-ledger.jsonl safe-export.jsonl \ - --privacy-profile safe_telemetry --privacy-key-file .marginal/export.key -marginal ledger-export marginal-ledger.jsonl aggregate.jsonl \ - --privacy-profile aggregate_export --minimum-group-size 5 -``` - -Replay re-evaluates recorded proposed actions under another policy. It is an off-policy diagnostic based on recorded actions and costs. It is **not causal proof**, does not simulate missing trajectories, and does not establish preserved task quality. - -## Execution modes - -| Mode | Applied behavior | Intended use | +| Environment | Current status | Intended capability | |---|---|---| -| `shadow` | Execute every proposed action; record recommendation | Safe data collection and calibration | -| `recommend` | Execute every proposed action; surface recommendation | Human/agent advisory integrations | -| `enforce` | Apply allow/deny and hard-budget decisions | Validated production control | - -`fund_best` remains an active selection API in every mode because MARGINAL is explicitly being asked to choose among candidates. - -## Reference policy profiles - -```python -from marginal import build_policy - -quality_first = build_policy("quality-first") -balanced = build_policy("balanced") -token_saver = build_policy("token-saver") -strict_budget = build_policy("strict-budget") -``` - -These are transparent reference defaults, not universally calibrated guarantees. Production policies should be validated against representative tasks and verifiers. - -## Decision model - -The reference policy converts configured dimensions into a common USD-denominated value: - -```text -expected value = capped expected success gain × outcome value -cost value = direct USD + token shadow cost + latency shadow cost + risk shadow cost -marginal score = expected value − cost value -ROI = expected value ÷ cost value -``` - -A recommended action must remain affordable, preserve verification reserves, avoid an exact duplicate under the chosen fingerprint scope, remain below the success target, and clear expected-gain and ROI thresholds. - -## Reliable accounting - -```text -propose → evaluate → reserve → execute → settle actual cost - ↘ abort when no spend occurred - ↘ settle failure when spend occurred -``` - -If actual usage exceeds the reservation, MARGINAL records the real spend first. Enforce mode then raises `BudgetOverrun`. Shadow and recommend modes record the observed overrun without changing caller behavior. - -If a failure usage extractor itself fails, MARGINAL conservatively settles the reserved estimate, releases the reservation, and keeps the original execution exception primary. A measured failed action is not marked as a completed duplicate, so a legitimate retry remains possible. +| Core Python runtime | Available | Full allocation, accounting, ledger, replay | +| Universal Agent Protocol | Available | Adapter contract and capability negotiation | +| Codex | Roadmap | Reference integration and measured benchmark | +| OpenCode | Roadmap | Open-source adapter and research environment | +| Claude Code | Roadmap | Hook-based integration | +| GitHub Copilot CLI / coding agent | Roadmap | Integration where official control surfaces permit | -## Duplicate protection and state-aware retries +See the [full roadmap](ROADMAP.md). Planned adapters are not presented as completed integrations. -Guarded call fingerprints include the action, callable identity, arguments, and keyword arguments. Universal-agent actions additionally support: +## Privacy by design -- `exact`; -- `once_per_state`; -- `once_per_phase`; -- `allow_retry`. +Not recording prompts is not enough: identifiers, action names, model names, metadata, verifier details, and exception text can still expose sensitive information. -This lets an adapter distinguish an accidental repeated read from a legitimate test rerun after the workspace changes. Shadow Mode can observe concurrent semantic duplicates without blocking them while still reserving and settling each execution separately. +MARGINAL therefore separates operational evidence from shareable telemetry: -## Hierarchical agent budgets - -```python -root = Treasury(BudgetLimits(max_tokens=200_000, max_usd=5.0), mode="shadow") -research = root.child("research", BudgetLimits(max_tokens=40_000, max_usd=1.0)) -verification = root.child( - "verification", - BudgetLimits(max_tokens=30_000, max_usd=0.75), -) -``` - -A child authorization reserves capacity from every ancestor under one shared lock. Settlement charges every level, preventing parallel sub-agents from oversubscribing a parent budget. - -## Trace and inspect decisions - -`JsonlTraceSink` preserves the v0.1 trace format. `JsonlDecisionLedger` is the strict v0.2 evidence format with schema, identity, sequencing, and correlation fields. - -```bash -marginal validate marginal-trace.jsonl -marginal report marginal-trace.jsonl -marginal ledger-validate marginal-ledger.jsonl -marginal ledger-report marginal-ledger.jsonl -marginal ledger-export marginal-ledger.jsonl aggregate.jsonl \ - --privacy-profile aggregate_export --minimum-group-size 5 -``` +| Profile | Purpose | +|---|---| +| `LOCAL_FULL` | Full operational ledger on a trusted local filesystem | +| `SAFE_TELEMETRY` | Removes free text and pseudonymizes identifiers with a local key | +| `AGGREGATE_EXPORT` | Produces generalized grouped rows with no identifiers or timestamps | -`LOCAL_FULL` is the backward-compatible ledger default. Use `SAFE_TELEMETRY` for strict -local telemetry and `AGGREGATE_EXPORT` for grouped sharing. Aggregate groups smaller than five -records are suppressed by default, and export destinations are never overwritten automatically. +Aggregate exports suppress groups smaller than five records by default to reduce re-identification risk. Pseudonymization is not anonymization; exports must still be reviewed before sharing. -A `CompositeTraceSink` can fan events to multiple sinks in order, but writes across different sinks are not an atomic distributed transaction. Use one authoritative ledger when atomic evidence is required. +Read the [privacy model](docs/privacy.md) and [security policy](SECURITY.md). -## Killer Demo +## Evidence, not hype -The bundled deterministic demo still demonstrates the allocation mechanism: +MARGINAL includes a deterministic Killer Demo that fixes the same code defect with a baseline workflow and a MARGINAL-funded workflow. ```bash -marginal killer-demo --output killer-demo-output +marginal killer-demo --output-dir demo-output ``` -It uses declared action-cost estimates and a deterministic verifier. It is not provider telemetry, a production benchmark, or a universal savings claim. - -[Open the committed demo report →](demos/killer-demo/RESULTS.md) +The demo uses declared action-cost estimates. It is useful for understanding allocation behavior, but it is **not provider telemetry and not a production savings claim**. -## Synthetic benchmark - -The bundled deterministic benchmark exercises policy, reservation, accounting, and reproducibility: +Public evaluation compares matched runs with the same agent, model, prompt, tools, limits, task order, and verifier: ```bash -marginal demo +marginal public-eval baseline.jsonl marginal.jsonl ``` -Its declared token, USD, and latency values are synthetic. The result tests mechanics and must not be presented as provider-measured savings. - -## Public benchmarking - -Real evaluations should compare the same model, task, tools, limits, and verifier with and without MARGINAL. The comparator accepts an explicit confidence level and preregistered quality margin: - -```bash -marginal public-eval baseline.jsonl marginal.jsonl \ - --confidence-level 0.95 --quality-margin-pp 1.0 -``` +The report covers: -Report: - -- resolved rate and confidence intervals; -- input, cached input, output, reasoning, and total tokens where available; -- direct cost, latency, tool calls, and sub-agent calls; -- cost per verified successful task; +- resolve-rate delta and quality non-inferiority; +- total and per-resolved-task token cost; +- USD, latency, and tool calls; - regressions and recoveries; -- policy and estimator identities; -- raw paired evidence without dropped failures. - -Savings without preserved quality are not optimization. - -See [`docs/public-benchmarks.md`](docs/public-benchmarks.md) and [`docs/benchmarking.md`](docs/benchmarking.md). +- bootstrap uncertainty. -## How MARGINAL differs - -| Category | Primary question | MARGINAL relationship | -|---|---|---| -| Hard budget / circuit breaker | Can this session spend more? | Complementary; MARGINAL also evaluates expected value | -| Model router | Which model should answer? | A model choice can be represented as a candidate action | -| Prompt compressor | Can this call use fewer tokens? | Complementary; MARGINAL may avoid the entire action | -| Workflow optimizer | Can a fixed flow be simplified? | MARGINAL makes state-aware online decisions | -| Observability | What was spent? | MARGINAL decides before spending and settles afterward | -| Decision Ledger | Why did policy behavior change? | Records versioned recommendation, application, cost, and outcome evidence | +A token reduction without preserved verified outcomes is not considered a successful result. -## Core primitives - -| Primitive | Responsibility | -|---|---| -| `Action`, `Cost`, `TokenUsage` | Describe proposed work and estimated or measured resources | -| `Decision`, `Allocation` | Expose applied behavior, recommendation, reason, score, and funded candidate | -| `BudgetLimits`, `BudgetLedger` | Enforce hard limits, reservations, and verification reserves | -| `MarginalPolicy`, `ValueEstimator` | Score expected marginal value with versioned identities | -| `Treasury` | Coordinate ranking, authorization, settlement, hierarchy, evidence, and outcomes | -| `JsonlDecisionLedger` | Persist strict, append-only learning-loop evidence with an explicit privacy profile | -| `PrivacyProfile`, `export_decision_ledger` | Pseudonymize safe telemetry or create grouped aggregate exports | -| `AgentAction`, `AgentDecision`, `AgentEvent` | Normalize engine-adapter communication | -| `UniversalRuntime` | Correlate one engine session with transactional core operations | -| `replay_ledger` | Compare policy recommendations over recorded actions without causal claims | +See [Public benchmark protocol](docs/public-benchmarks.md) and [Killer Demo results](demos/killer-demo/RESULTS.md). ## Architecture ```text -AI development agent - │ native hook/event - ▼ -thin engine adapter - │ universal protocol - ▼ -UniversalRuntime → Treasury → policy → versioned estimator - │ │ - │ ├─ reserve / settle / abort / failure settlement - │ └─ Decision Ledger → privacy profile → outcome / replay / export - ▼ -allow / deny today; richer negotiated directives through protocol extensions +AI development agents +Codex · Claude Code · Copilot · OpenCode · others + │ + thin engine adapters + │ + Universal Agent Protocol + │ + ┌────────────────┼────────────────┐ + │ │ │ + Value Estimator Treasury Decision Ledger + │ │ │ + └────────── Decision Policy ──────┘ + │ + observe · recommend · enforce ``` -See [`docs/architecture.md`](docs/architecture.md). +MARGINAL is deliberately modular: -## Project status +- the **core** evaluates and accounts; +- the **runtime** coordinates sessions and adapters; +- the **ledger** preserves evidence; +- privacy exports transform data for controlled sharing; +- external frameworks retain ownership of execution. -MARGINAL `v0.2.0` is the **Learning Loop Foundation**. It provides a universal protocol, local runtime, non-blocking shadow evaluation, schema-versioned evidence, explicit privacy profiles, outcome recording, contextual historical estimates, failure settlement, and off-policy replay. +Read the [architecture](docs/architecture.md) and [API reference](docs/api.md). -It does not yet claim complete vendor-specific adapters, causal marginal-value estimation, automatic regret minimization, or guaranteed savings on arbitrary workloads. The next validation milestone is a real paired Codex integration using measured telemetry and a predefined quality non-inferiority criterion. +## Project status -## Roadmap +`v0.2.0` provides the Learning Loop Foundation, privacy profiles, Universal Agent Protocol, versioned evidence, policy replay, and a dependency-free core. -The project remains one product and one repository. Future Codex, OpenCode, Claude Code, GitHub Copilot, and other integrations will be thin adapters over the same protocol and core. +The active milestone is **v0.3 — Codex Reference Integration**: measured token telemetry, matched baseline runs, a 10-task canary, and a preregistered public evaluation before broader enforcement claims. -[View the full product roadmap →](ROADMAP.md) +[View the roadmap →](ROADMAP.md) ## Documentation -- [Quickstart](docs/quickstart.md) -- [Concepts](docs/concepts.md) -- [Architecture](docs/architecture.md) -- [API reference](docs/api.md) -- [Learning loop](docs/learning-loop.md) -- [Universal runtime](docs/universal-runtime.md) -- [Privacy profiles](docs/privacy.md) -- [Integrations](docs/integrations.md) -- [Benchmarking](docs/benchmarking.md) -- [Public benchmark protocol](docs/public-benchmarks.md) -- [Research and prior art](docs/research.md) -- [FAQ](docs/faq.md) -- [Governance](docs/governance.md) -- [Contributing](CONTRIBUTING.md) -- [Security](SECURITY.md) - -## Research lineage - -MARGINAL is an independent open-source reference implementation inspired by the research direction described in Siqi Zhu’s position paper, “Agentic AI Systems Should Be Designed as Marginal Token Allocators.” The paper proposes an economic framing; this repository focuses on a usable runtime contract, accounting, evidence, tests, integrations, and honest validation. +| Start here | Deep dives | +|---|---| +| [Quickstart](docs/quickstart.md) | [Architecture](docs/architecture.md) | +| [Core concepts](docs/concepts.md) | [API reference](docs/api.md) | +| [Integration guide](docs/integrations.md) | [Privacy profiles](docs/privacy.md) | +| [Benchmarking](docs/benchmarking.md) | [Public benchmark protocol](docs/public-benchmarks.md) | +| [Roadmap](ROADMAP.md) | [Security](SECURITY.md) | +| [Changelog](CHANGELOG.md) | [Governance](docs/governance.md) | + +The [project website](https://signallayerlabs.github.io/Marginal/) provides the product-level overview; GitHub remains the source of truth for code, evidence, releases, and technical documentation. ## Contributing -Contributions are welcome for adapters, estimator implementations, benchmark scenarios, schemas, documentation, and independent validation. Read [`CONTRIBUTING.md`](CONTRIBUTING.md) before opening a pull request. +Contributions are welcome across the core, protocol, adapters, privacy, benchmarks, and documentation. + +Before opening a pull request: + +```bash +ruff format --check . +ruff check . +mypy src/marginal +pytest -q +python -m build +python -m twine check dist/* +``` + +Read [`CONTRIBUTING.md`](CONTRIBUTING.md) and the [governance model](docs/governance.md). ## Citation -Research and technical work can cite the repository using [`CITATION.cff`](CITATION.cff). +```bibtex +@software{marginal2026, + title = {MARGINAL: Compute Governance for AI Agents}, + author = {SignalLayer Labs and contributors}, + year = {2026}, + url = {https://github.com/SignalLayerLabs/Marginal}, + license = {Apache-2.0} +} +``` ## License diff --git a/VISUAL_STUDIO_COMMIT_PROMPT.md b/VISUAL_STUDIO_COMMIT_PROMPT.md new file mode 100644 index 0000000..07eca37 --- /dev/null +++ b/VISUAL_STUDIO_COMMIT_PROMPT.md @@ -0,0 +1,47 @@ +# Visual Studio / GitHub Copilot Commit Prompt + +Apply this ZIP to the root of the checked-out `SignalLayerLabs/Marginal` repository, preserving all paths. + +## Procedure + +1. Create branch `docs/readme-pages-redesign`. +2. Copy every ZIP file into the repository. +3. Search `.github/workflows/` for `actions/deploy-pages`. Exactly one active Pages deployer must remain. Keep `.github/workflows/pages.yml`; remove or consolidate any older Pages/Killer Demo deployer. +4. Confirm these existing files are present: + `assets/marginal-readme-hero.png`, `demos/killer-demo/RESULTS.md`, `docs/quickstart.md`, `docs/architecture.md`, `docs/privacy.md`, `docs/api.md`, `docs/integrations.md`, `docs/benchmarking.md`, `docs/public-benchmarks.md`, `ROADMAP.md`, `SECURITY.md`, `CONTRIBUTING.md`, `LICENSE`. +5. Preserve the claim guardrails: + - vendor adapters remain labeled roadmap/planned; + - the Killer Demo remains deterministic and estimate-based; + - no guaranteed per-request savings; + - measured savings require preserved verified quality; + - pseudonymization is not anonymization. +6. Run: + ```bash + python scripts/validate_readme_pages.py + ruff format --check . + ruff check . + mypy src/marginal + pytest -q + python -m build + python -m twine check dist/* + ``` +7. Preview: + ```bash + python -m http.server 8000 --directory site + ``` + Check desktop/mobile, keyboard navigation, links, and contrast. +8. In GitHub Settings → Pages, choose **GitHub Actions**. +9. Update repository About: + - Description: `Open-source compute governance and token optimization for AI agents. Observe, measure, learn and enforce which model calls, tools, retries and sub-agents are worth the cost.` + - Website: `https://signallayerlabs.github.io/Marginal/` + - Topics: `ai-agents`, `llm`, `token-optimization`, `cost-optimization`, `compute-governance`, `agent-observability`, `codex`, `claude-code`, `github-copilot`, `opencode`, `python`, `local-first` +10. Commit: `docs: redesign README and launch product website` +11. Push and open PR: `docs: redesign README and launch GitHub Pages website`. + +PR summary: +- concise SEO-oriented technical README; +- accessible dependency-free product website; +- one consolidated Pages deployment including Killer Demo; +- website operations and claim guardrails documented. + +Include exact verification outputs in the PR. Do not merge if a second workflow still deploys Pages. diff --git a/docs/superpowers/plans/2026-08-06-readme-pages.md b/docs/superpowers/plans/2026-08-06-readme-pages.md new file mode 100644 index 0000000..300ba09 --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-readme-pages.md @@ -0,0 +1,17 @@ +# README and GitHub Pages Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the long README with a concise technical landing page and add a dependency-free GitHub Pages website. + +**Architecture:** README, website, docs, and roadmap have distinct responsibilities. One Pages workflow assembles the static site, existing hero asset, and Killer Demo. + +**Tech Stack:** GitHub Markdown, semantic HTML5, CSS, vanilla JavaScript, GitHub Actions Pages. + +## Tasks + +1. Replace `README.md`, preserving install, quickstart, evidence, privacy, roadmap, docs, contribution, citation, and license. +2. Add `site/index.html`, styles, navigation script, robots, sitemap, and 404 page. +3. Add one `.github/workflows/pages.yml` deployment that also publishes the Killer Demo. +4. Add `docs/website.md` and update `CHANGELOG.md`. +5. Run `scripts/validate_readme_pages.py`, repository CI checks, local preview, and final diff review. diff --git a/docs/superpowers/specs/2026-08-06-readme-pages-design.md b/docs/superpowers/specs/2026-08-06-readme-pages-design.md new file mode 100644 index 0000000..e833ca6 --- /dev/null +++ b/docs/superpowers/specs/2026-08-06-readme-pages-design.md @@ -0,0 +1,19 @@ +# README and GitHub Pages Redesign + +## Goal + +Turn the repository entrance into a fast technical landing page and provide a separate product website for discovery, positioning, and adoption. + +## Information architecture + +- README: concise developer-facing landing page. +- GitHub Pages: marketing-oriented product page. +- Existing docs: detailed technical source of truth. +- Roadmap: delivery status and future milestones. +- GitHub: code, evidence, releases, issues, and contribution workflow. + +## Guardrails + +Explain the product in the first screen, use search-relevant terminology naturally, keep claims tied to evidence, separate deterministic demonstrations from measured benchmarks, and label unfinished engine adapters as planned. + +The website uses static HTML, CSS, and minimal JavaScript, with no third-party runtime dependencies, tracking, or analytics. diff --git a/docs/website.md b/docs/website.md new file mode 100644 index 0000000..f0dc582 --- /dev/null +++ b/docs/website.md @@ -0,0 +1,50 @@ +# MARGINAL website + +The product website is a dependency-free static site published through GitHub Pages at: + +`https://signallayerlabs.github.io/Marginal/` + +## Responsibility split + +- `README.md` is the fast technical entry point for developers. +- `site/` is the product-level marketing and discovery experience. +- `docs/` remains the source for detailed technical documentation. +- GitHub remains the source of truth for code, releases, benchmarks, issues, and evidence. +- `demos/killer-demo/` is published under `/demo/` by the Pages workflow. + +The website must not make claims stronger than the repository evidence. Planned engine adapters remain labeled as planned. The Killer Demo remains labeled as a deterministic demonstration based on declared cost estimates. + +## Deployment + +GitHub Pages must use **GitHub Actions** as its source. Only one workflow in the repository may call `actions/deploy-pages`. Before merging, search `.github/workflows/` and remove or consolidate any previous Pages workflow. + +The workflow assembles: + +```text +site/ → / +assets/marginal-readme-hero.png → /assets/ +demos/killer-demo/ → /demo/ +``` + +## Local preview + +```bash +python -m http.server 8000 --directory site +``` + +To reproduce the deployment layout: + +```bash +rm -rf _site +mkdir -p _site/assets _site/demo +cp -R site/. _site/ +cp assets/marginal-readme-hero.png _site/assets/marginal-readme-hero.png +cp -R demos/killer-demo/. _site/demo/ +python -m http.server 8000 --directory _site +``` + +## SEO, accessibility, and privacy guardrails + +The site includes one descriptive `h1`, canonical and social metadata, `robots.txt`, sitemap, semantic structure, keyboard navigation, responsive layouts, and reduced-motion support. + +It contains no analytics or third-party runtime scripts. Any future analytics integration requires a documented privacy review. diff --git a/scripts/validate_readme_pages.py b/scripts/validate_readme_pages.py new file mode 100644 index 0000000..88606f5 --- /dev/null +++ b/scripts/validate_readme_pages.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +import re +from html.parser import HTMLParser +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +class Parser(HTMLParser): + def __init__(self): + super().__init__() + self.title = "" + self.in_title = False + self.h1 = 0 + self.meta = {} + self.scripts = [] + self.styles = [] + + def handle_starttag(self, tag, attrs): + data = {k: v or "" for k, v in attrs} + if tag == "title": + self.in_title = True + elif tag == "h1": + self.h1 += 1 + elif tag == "meta": + key = data.get("name") or data.get("property") + if key: + self.meta[key] = data.get("content", "") + elif tag == "script": + self.scripts.append(data.get("src", "")) + elif tag == "link" and data.get("rel") == "stylesheet": + self.styles.append(data.get("href", "")) + + def handle_endtag(self, tag): + if tag == "title": + self.in_title = False + + def handle_data(self, data): + if self.in_title: + self.title += data + + +readme = (ROOT / "README.md").read_text(encoding="utf-8") +words = len(re.findall(r"\b[\w\'-]+\b", readme)) +assert 1200 <= words <= 2200, f"README words: {words}" +for heading in [ + "# MARGINAL", + "## Why MARGINAL", + "## How it works", + "## Install", + "## Quickstart", + "## The Learning Loop Foundation", + "## Universal Agent Runtime", + "## Privacy by design", + "## Evidence, not hype", + "## Project status", + "## Documentation", +]: + assert heading in readme, heading +for forbidden in [ + "guarantees fewer tokens", + "saves tokens on every request", + "Codex adapter is available", + "Claude Code adapter is available", +]: + assert forbidden.lower() not in readme.lower(), forbidden + +required = [ + "docs/quickstart.md", + "docs/architecture.md", + "docs/privacy.md", + "docs/api.md", + "docs/integrations.md", + "docs/benchmarking.md", + "docs/public-benchmarks.md", + "ROADMAP.md", + "SECURITY.md", + "CONTRIBUTING.md", + "LICENSE", + "assets/marginal-readme-hero.png", + "demos/killer-demo/RESULTS.md", +] +missing = [p for p in required if not (ROOT / p).exists()] +assert not missing, missing + +parser = Parser() +parser.feed((ROOT / "site/index.html").read_text(encoding="utf-8")) +assert parser.title.strip() +assert parser.h1 == 1 +for key in ["description", "robots", "og:title", "og:description", "og:url", "twitter:card"]: + assert parser.meta.get(key), key +assert parser.styles == ["styles.css"] +assert parser.scripts == ["app.js"] +assert not any(x.startswith(("http://", "https://")) for x in parser.styles + parser.scripts) + +for path in [ + "site/styles.css", + "site/app.js", + "site/robots.txt", + "site/sitemap.xml", + "site/404.html", +]: + assert (ROOT / path).is_file(), path + +workflow = (ROOT / ".github/workflows/pages.yml").read_text(encoding="utf-8") +for token in [ + "actions/configure-pages@v5", + "actions/upload-pages-artifact@v3", + "actions/deploy-pages@v4", + "demos/killer-demo", + "assets/marginal-readme-hero.png", +]: + assert token in workflow, token + +deployers = [] +for pattern in ("*.yml", "*.yaml"): + for path in (ROOT / ".github/workflows").glob(pattern): + if "actions/deploy-pages" in path.read_text(encoding="utf-8"): + deployers.append(path.relative_to(ROOT).as_posix()) +assert deployers == [".github/workflows/pages.yml"], deployers + +print(f"README words: {words}") +print("README structure and claims: PASS") +print("Referenced files: PASS") +print("Website SEO and dependency audit: PASS") +print("Pages workflow consolidation: PASS") diff --git a/site/404.html b/site/404.html new file mode 100644 index 0000000..99ade7e --- /dev/null +++ b/site/404.html @@ -0,0 +1 @@ +Page not found — MARGINAL

404

Page not found.

The requested page does not exist.

Return to MARGINAL
diff --git a/site/app.js b/site/app.js new file mode 100644 index 0000000..baad58e --- /dev/null +++ b/site/app.js @@ -0,0 +1,2 @@ +const button=document.querySelector(".nav-toggle");const links=document.querySelector(".nav-links"); +if(button&&links){button.addEventListener("click",()=>{const open=links.classList.toggle("open");button.setAttribute("aria-expanded",String(open))});links.addEventListener("click",event=>{if(event.target instanceof HTMLAnchorElement){links.classList.remove("open");button.setAttribute("aria-expanded","false")}})} \ No newline at end of file diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..2588ae2 --- /dev/null +++ b/site/index.html @@ -0,0 +1,146 @@ + + + + + + MARGINAL — Compute Governance and Token Optimization for AI Agents + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Open-source compute governance for AI agents

+

Fund only the next action worth taking.

+

MARGINAL helps AI agents decide whether the next model call, tool call, search, retry, review or sub-agent is worth its token cost.

+ +
    +
  • Local first
  • Provider neutral
  • Zero mandatory dependencies
  • Apache-2.0
  • +
+
+
+
Agent proposes action
↓
+
Estimate value versus cost
↓
+
ALLOWDENYSHADOW
+
↓
Settle actual usage + outcome
+
+
+ +
+

The missing layer

+

Budgets say what an agent can afford. MARGINAL asks what deserves funding.

+

Agent runtimes often execute work because a model requested it or a hard limit has not been reached. MARGINAL compares expected improvement with tokens, cost, latency, risk, remaining budget and verification needs before compute is committed.

+
+ +
+

How it works

Transactional governance, not token counting

+
+
01

Observe

Capture proposed actions and context through one engine-neutral protocol.

+
02

Evaluate

Estimate marginal gain, uncertainty and full economic cost.

+
03

Reserve

Atomically protect budget and verification capacity before execution.

+
04

Settle

Record measured usage, failures, overruns and verified outcomes.

+
05

Learn

Replay versioned policies and improve calibration without overstating causality.

+
+
+ +
+

Why it is different

Built for accountable autonomy

+
+

Shadow before enforcement

Observe recommendations without changing agent behavior, then enforce only after representative validation.

+

Real accounting lifecycle

Reserve, execute, settle, abort and report overruns across hierarchical treasuries.

+

Protected verification

Keep capacity available for tests and evidence instead of optimizing an agent into premature confidence.

+

Versioned evidence

Decision Ledger records correlate policy, estimator, cost, outcome and runtime identity for audit and replay.

+

Universal protocol

Thin adapters connect coding agents while the economic policy remains in one core.

+

Measured claims

Token reduction counts only when verified outcomes remain within a predefined quality constraint.

+
+
+ +
+

The learning loop

From static policy to evidence-driven allocation

+

MARGINAL's defensible advantage is not a single ROI formula. It is the closed loop between observed decisions, measured outcomes, versioned estimators, policy replay and calibration.

+

Task success is kept separate from action-level causal attribution. Recorded correlation is not presented as causal proof.

+
+
  1. Observe decisions
  2. Measure actual cost and outcomes
  3. Estimate marginal value with uncertainty
  4. Allocate budget
  5. Measure calibration and regret
  6. Improve the policy
+
+ +
+

Privacy by design

Operational evidence stays local. Shareable data is transformed deliberately.

+

Prompt-free telemetry can still leak through identifiers, names and metadata. MARGINAL classifies fields and provides explicit export profiles.

+
+
+
LOCAL_FULLComplete operational ledger for trusted local storage.
+
SAFE_TELEMETRYFree text removed and identifiers pseudonymized with a local key.
+
AGGREGATE_EXPORTGeneralized groups without identifiers; small groups suppressed by default.
+
+
+ +
+

One product, multiple engines

Universal foundation first. Thin adapters next.

+
+
Available

Core runtime

Policy, treasury, ledger, privacy, replay and universal protocol.

+
Next

Codex

Reference adapter and matched measured benchmark.

+
Planned

OpenCode

Open-source adapter and experimentation environment.

+
Planned

Claude Code

Hook-based integration using shared protocol contracts.

+
Planned

GitHub Copilot

Integration where official control surfaces permit enforcement.

+
+ +
+ +
+

Evidence, not hype

Optimization is successful only when quality is preserved.

+

The bundled Killer Demo is deterministic and uses declared action costs. It explains the mechanism; it is not provider telemetry or a universal savings claim.

+

Public evaluation requires matched model, prompt, tools, limits and verifier, then reports tokens per verified successful task, quality delta and uncertainty.

+ Read the benchmark protocol → +
+
+ +
+

Build economically disciplined agents

+

Observe first. Measure honestly. Enforce what the evidence supports.

+ +
+
+ + + + diff --git a/site/robots.txt b/site/robots.txt new file mode 100644 index 0000000..d4f285d --- /dev/null +++ b/site/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://signallayerlabs.github.io/Marginal/sitemap.xml diff --git a/site/sitemap.xml b/site/sitemap.xml new file mode 100644 index 0000000..e9beff8 --- /dev/null +++ b/site/sitemap.xml @@ -0,0 +1,2 @@ + +https://signallayerlabs.github.io/Marginal/weekly1.0https://signallayerlabs.github.io/Marginal/demo/monthly0.7 diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..692786a --- /dev/null +++ b/site/styles.css @@ -0,0 +1,19 @@ +:root{--bg:#080b10;--surface:#10151d;--surface2:#151c26;--text:#f4f7fb;--muted:#aab5c4;--line:#273140;--accent:#91ff63;--max:1180px} +*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.65}a{color:inherit} +.skip-link{position:absolute;left:-9999px}.skip-link:focus{left:1rem;top:1rem;z-index:100;background:var(--text);color:var(--bg);padding:.75rem 1rem} +.shell{width:min(calc(100% - 2rem),var(--max));margin-inline:auto}.narrow{max-width:790px}.center{text-align:center;justify-content:center} +.site-header{position:sticky;top:0;z-index:20;background:rgba(8,11,16,.9);border-bottom:1px solid var(--line);backdrop-filter:blur(16px)} +.nav{min-height:72px;display:flex;align-items:center;justify-content:space-between;gap:2rem}.brand{display:inline-flex;align-items:center;gap:.7rem;text-decoration:none;font-weight:800;letter-spacing:.08em}.brand-mark{display:grid;place-items:center;width:34px;height:34px;color:var(--bg);background:var(--accent);border-radius:8px} +.nav-links{display:flex;align-items:center;gap:1.35rem}.nav-links a{text-decoration:none;color:var(--muted);font-size:.94rem}.nav-links a:hover,.nav-links a:focus{color:var(--text)}.nav-toggle{display:none} +.button{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:.7rem 1.15rem;border-radius:10px;background:var(--accent);color:#071006;text-decoration:none;font-weight:750;border:1px solid var(--accent)}.button:hover,.button:focus{filter:brightness(1.08);transform:translateY(-1px)}.button-secondary{background:transparent;color:var(--text);border-color:var(--line)}.button-small{min-height:38px;padding:.45rem .8rem;color:#071006!important} +.hero{min-height:720px;display:grid;grid-template-columns:1.25fr .75fr;align-items:center;gap:4rem;padding-block:7rem 5rem}.eyebrow{color:var(--accent);text-transform:uppercase;letter-spacing:.14em;font-size:.78rem;font-weight:800} +h1{font-size:clamp(3rem,7vw,6.4rem);line-height:.96;letter-spacing:-.055em;margin:.5rem 0 1.4rem;max-width:900px}h2{font-size:clamp(2rem,4.2vw,4rem);line-height:1.05;letter-spacing:-.04em;margin:.45rem 0 1.2rem}h3{line-height:1.2}.hero-lead{color:var(--muted);font-size:clamp(1.1rem,2vw,1.35rem);max-width:720px}.hero-actions{display:flex;gap:.8rem;margin-top:2rem;flex-wrap:wrap}.trust-row{display:flex;flex-wrap:wrap;gap:.7rem 1.25rem;list-style:none;padding:0;margin:2.2rem 0 0;color:var(--muted);font-size:.9rem}.trust-row li:before{content:"✓";color:var(--accent);margin-right:.4rem} +.hero-panel{border:1px solid var(--line);background:linear-gradient(160deg,var(--surface2),var(--surface));padding:2rem;border-radius:20px;box-shadow:0 30px 80px rgba(0,0,0,.35)}.flow-node{padding:1rem;border:1px solid var(--line);border-radius:10px;text-align:center;background:rgba(255,255,255,.02)}.flow-node.accent{border-color:var(--accent)}.flow-arrow{text-align:center;color:var(--accent);padding:.4rem}.decision-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem}.decision-grid span{border:1px solid var(--line);border-radius:8px;padding:.7rem .3rem;text-align:center;font-size:.75rem;font-weight:800} +.section{padding-block:6.5rem;border-top:1px solid var(--line)}.problem,.feature-section,.privacy-section,.evidence-section{background:var(--surface)}.problem{text-align:center}.problem p:last-child{color:var(--muted)}.section-heading{max-width:760px;margin-bottom:3rem} +.steps{display:grid;grid-template-columns:repeat(5,1fr);gap:1rem}.steps article,.feature-grid article,.roadmap-grid article{border:1px solid var(--line);border-radius:14px;padding:1.35rem;background:var(--surface)}.steps span,.roadmap-grid span{color:var(--accent);font-size:.8rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em}.steps p,.feature-grid p,.roadmap-grid p{color:var(--muted);font-size:.94rem} +.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}.split{display:grid;grid-template-columns:1fr 1fr;gap:5rem;align-items:start}.loop-list{list-style:none;padding:0;margin:0;counter-reset:loop}.loop-list li{counter-increment:loop;padding:1rem 0;border-bottom:1px solid var(--line);font-weight:700}.loop-list li:before{content:"0" counter(loop);color:var(--accent);margin-right:1rem;font-size:.8rem}.fine-print{color:var(--muted);font-size:.9rem} +.privacy-cards{display:grid;gap:.8rem}.privacy-cards article{display:grid;gap:.2rem;padding:1.2rem;border:1px solid var(--line);border-radius:12px}.privacy-cards strong{color:var(--accent)}.privacy-cards span{color:var(--muted)}.roadmap-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1rem;margin-bottom:2.5rem}.roadmap-grid .complete{border-color:var(--accent)}.text-link{color:var(--accent);font-weight:750;text-decoration:none}.cta{background:radial-gradient(circle at 50% 20%,rgba(145,255,99,.13),transparent 45%)} +footer{padding-block:3rem;border-top:1px solid var(--line);color:var(--muted)}.footer-grid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:2rem}.footer-grid div{display:flex;flex-direction:column;gap:.4rem;align-items:flex-start}.footer-grid p{margin:0}.footer-grid a{text-decoration:none} +@media(max-width:900px){.hero,.split{grid-template-columns:1fr}.hero{min-height:auto;gap:2.5rem;padding-top:5rem}.steps{grid-template-columns:repeat(2,1fr)}.feature-grid{grid-template-columns:repeat(2,1fr)}.roadmap-grid{grid-template-columns:repeat(2,1fr)}.nav-toggle{display:inline-flex;background:transparent;border:1px solid var(--line);color:var(--text);padding:.55rem .75rem;border-radius:8px}.nav-links{display:none;position:absolute;left:1rem;right:1rem;top:72px;background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:1rem;flex-direction:column;align-items:stretch}.nav-links.open{display:flex}} +@media(max-width:600px){h1{font-size:3.2rem}.section{padding-block:4.5rem}.steps,.feature-grid,.roadmap-grid,.footer-grid{grid-template-columns:1fr}.decision-grid{grid-template-columns:1fr}} +@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*:before,*:after{transition:none!important;animation:none!important}}

f1~M+B0PqP%O}1-Cy(zayWv5 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 ^JOSBOU*ck^8xbm`zfsybhLEQ((Kd; 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 JPDHLkV1kdeZ07&~ diff --git a/codemeta.json b/codemeta.json index 1a2c004..5920f56 100644 --- a/codemeta.json +++ b/codemeta.json @@ -2,12 +2,12 @@ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "MARGINAL", - "description": "Open-source compute capital allocator for AI agents that funds only actions whose expected marginal value justifies token, direct cost, latency, and risk.", + "description": "Open-source, local-first compute-governance and learning-loop foundation for AI agents.", "codeRepository": "https://github.com/SignalLayerLabs/Marginal", "issueTracker": "https://github.com/SignalLayerLabs/Marginal/issues", "license": "https://spdx.org/licenses/Apache-2.0", - "version": "0.1.0", - "datePublished": "2026-08-04", + "version": "0.2.0", + "datePublished": "2026-08-06", "programmingLanguage": "Python", "runtimePlatform": "Python 3.10-3.13", "author": { @@ -19,8 +19,10 @@ "AI agents", "agentic AI", "token optimization", - "cost optimization", - "compute capital allocation", + "compute governance", + "decision ledger", + "shadow mode", + "learning loop", "AI FinOps", "LLM infrastructure" ] diff --git a/demos/killer-demo/README.md b/demos/killer-demo/README.md deleted file mode 100644 index b45ed4f..0000000 --- a/demos/killer-demo/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Killer Demo artifacts - -This directory contains the reproducible output of: - -```bash -marginal killer-demo --output demos/killer-demo -``` - -Start with [RESULTS.md](RESULTS.md). Open `index.html` locally for the standalone visual -report. `result.json` contains the complete structured comparison and `trace.jsonl` contains -the provider-neutral candidate rankings, authorizations, and commits. - -The fixture and workload are deterministic. Token, USD, and latency values are declared -action-cost estimates used to exercise the allocator, not provider telemetry. The result -demonstrates MARGINAL's allocation mechanism; it is not a production benchmark or a -universal savings claim. - -A GitHub Pages workflow publishes this directory as a standalone site after Pages is set to -**GitHub Actions** in the repository settings. diff --git a/demos/killer-demo/index.html b/demos/killer-demo/index.html index 7914704..cc19d2c 100644 --- a/demos/killer-demo/index.html +++ b/demos/killer-demo/index.html @@ -1505,7 +1505,7 @@