bantamkit 0.29.2__tar.gz → 0.30.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bantamkit-0.29.2 → bantamkit-0.30.0}/PKG-INFO +2 -2
- bantamkit-0.30.0/_assets/tools/memory_dream.json +20 -0
- bantamkit-0.30.0/_assets/tools/repo_map.json +44 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/__init__.py +1 -1
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/eventlog.py +5 -4
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/mcpserver.py +169 -2
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/__init__.py +15 -1
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/__main__.py +1 -1
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/component.py +206 -4
- bantamkit-0.30.0/src/bantamkit/memory/dream.py +751 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/store.py +94 -3
- bantamkit-0.30.0/src/bantamkit/repomap.py +1026 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/served-tool-surface.json +68 -1
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_bantamkit_read_tool.py +2 -0
- bantamkit-0.30.0/tests/test_conformance_suite_table_gate.py +124 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_eventlog.py +4 -2
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mcpserver.py +11 -5
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory.py +168 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_compact_tool.py +3 -1
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_component.py +72 -1
- bantamkit-0.30.0/tests/test_memory_dream.py +875 -0
- bantamkit-0.30.0/tests/test_repo_map_tool.py +296 -0
- bantamkit-0.30.0/tests/test_repomap.py +649 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_served_tool_count_records.py +98 -14
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_skillaudit.py +6 -1
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_status_surface.py +1 -1
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_tool_manifest.py +2 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/.gitignore +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/README.md +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/contracts/default.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/manifest.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/HISTORY.md +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/README.md +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/fixtures/.gitkeep +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/fixtures/catalog.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/perturbations/task-completion.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/.gitkeep +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-contact.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-invoice.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-order.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-schedule.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-versions.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-db-port.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-deploy.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-oncall.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-owner.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-compare.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-total.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/profiles/default.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/profiles/patient.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/rubrics/.gitkeep +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/rubrics/code-quality.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/rubrics/grounded-completion.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/rubrics/task-completion.yaml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/schemas/shiftwork-checkpoint.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/skills/.gitkeep +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/skills/file-graph.md +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/skills/memory.md +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/.gitkeep +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/bantamkit_read.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/bantamkit_status.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/build_identity.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/document_list.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/document_read.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/file_graph.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/memory_compact.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/memory_recall.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/memory_save.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/shiftwork_clock_in.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/shiftwork_clock_out.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/shiftwork_status.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/skill_audit.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/validate_json.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/hatch_build.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/pyproject.toml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/agent.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/assets.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/budget.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/client.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/contract.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/criticreplay.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/critique.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/docmanifest.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/docread.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/evalrun.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/filegraph.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/hostinstall.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/loopguard.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/mcpreport.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/divergence.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/layers.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/pdfread.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/profile.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/shiftwork.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/skillaudit.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/statusline.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/structured.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/textutil.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/cli_exit_status_probe.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/conftest.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/bad-crc.docx +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/charref-4301-digits.html +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/charset-table.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/compression-method-9.docx +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/corrupt-deflate.docx +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/encrypted-member.docx +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/encrypted-mimetype.odt +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/internal-dtd-entity.docx +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/rfc2231-charset.eml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/rfc822-nested-twice.eml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/x-uuencode.eml +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/f8404ab-perturbation-baseline.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/platform-assumption-baseline.json +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/docread_fixtures.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/perturbation_baseline_harness.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/rbp16_effect_probe.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/rbp18_payload_probe.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_adapter.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_agent.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_amendguard.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_budget.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_build_identity.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_client.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_compaction_corpus_survey.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_conformance.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_contract_fanout.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_criticreplay.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_critique.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_doc_commands_gate.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_docread.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_docread_ceilings.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_document_manifest_parity.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_document_setup.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_document_tasks.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_document_tools.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_encoding_gate.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_evalrun.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_field_program_gates.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_field_programs.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_filegraph.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_hostinstall.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_ladder_statistics.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_launcher_which.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_layers.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_loopguard.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mcp_endpoint.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mcpdrift.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mcpreport.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_divergence.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_layers.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_store_tripwire.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mutmatrix.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_newline_gate.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_packaging.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_pdfread.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_pinharness_ledger.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_platform_assumption_gate.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_shiftwork.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_statusline.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_structured.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_thread_exception_gate.py +0 -0
- {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_version_agreement.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memory_dream",
|
|
3
|
+
"description": "Consolidate the memories the project layer and the machine-wide profile layer hold under the SAME NAME, and say exactly what changed. Byte-identical copies collapse to one; a pair that has diverged is UNIONED — every claim from both copies survives, never a winner-takes-all — and a relative date in a body is annotated with the day it resolves to against that fact's own mtime, never against today. The survivor stays in the writable project layer and the profile copy MOVES to that store's archive/: nothing is deleted and any consumed memory can be restored by name. This is a correctness pass, not a token saving: the profile index is derived at read time and is not loaded from a prompt, so consolidating it frees approximately no bytes — what it buys is one copy of a ruling instead of two that can disagree. It defaults to a DRY RUN because the profile store is machine-wide: a memory archived out of it stops answering for every other project on this machine that has no store of its own. Read the plan first, then call it again with dry_run false to apply it.",
|
|
4
|
+
"surfaces": ["mcp"],
|
|
5
|
+
"parameters": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"dry_run": {
|
|
9
|
+
"type": "boolean",
|
|
10
|
+
"description": "Preview only. True (the default) performs every read, merge and projection and writes nothing. Pass false to apply the plan."
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"output_schema": {
|
|
15
|
+
"properties": { "result": { "title": "Result", "type": "string" } },
|
|
16
|
+
"required": ["result"],
|
|
17
|
+
"type": "object",
|
|
18
|
+
"title": "memory_dreamOutput"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "repo_map",
|
|
3
|
+
"description": "A ranked map of a source tree: every file's definitions, ordered by how central that file is to the files you name in `focus`, truncated to a byte budget. Call it when you are about to work on a file and want to know which OTHER files matter to it — the ports, the callers, the module it reaches through a private helper — rather than reading a directory listing and guessing. `focus` is the file or files you are editing, relative to `root` and POSIX-separated; they are excluded from the listing because you already have them open, and an empty focus gives plain centrality over the whole tree. This is a PRECISION pass and it is not a token saving: the gate this feature was supposed to clear was refuted by measurement — discovery is 0.114% of real prompt tokens, because 97.8% of the bill is cache_read — so a map does not make a session cheaper. What it buys is the right file found sooner. Nothing the scanner cannot read is dropped silently: every unread file resolves into a named omission (unknown-language, unreadable-bytes, size-cap, no-definitions, unreachable, per-file-cap, budget) counted in a footer that is NOT charged to the budget. The budget is UTF-8 BYTES of the listing, not tokens; there is no model tokenizer in either runtime and this tool will not pretend to one.",
|
|
4
|
+
"surfaces": [
|
|
5
|
+
"mcp"
|
|
6
|
+
],
|
|
7
|
+
"parameters": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"required": [
|
|
10
|
+
"root"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"root": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Directory to map, absolute or relative to the server's working directory"
|
|
16
|
+
},
|
|
17
|
+
"focus": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
22
|
+
"description": "The files you are working on, relative to `root` and POSIX-separated. They are excluded from the listing. A name that is not a scanned source is ignored rather than refused."
|
|
23
|
+
},
|
|
24
|
+
"budget": {
|
|
25
|
+
"type": "integer",
|
|
26
|
+
"minimum": 0,
|
|
27
|
+
"description": "UTF-8 bytes of listing to render; default 4000. The omission footer is not charged against it."
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"output_schema": {
|
|
32
|
+
"properties": {
|
|
33
|
+
"result": {
|
|
34
|
+
"title": "Result",
|
|
35
|
+
"type": "string"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": [
|
|
39
|
+
"result"
|
|
40
|
+
],
|
|
41
|
+
"type": "object",
|
|
42
|
+
"title": "repo_mapOutput"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -29,4 +29,4 @@ from bantamkit.structured import StructuredOutputError, extract_json, structured
|
|
|
29
29
|
# file as its dynamic version source, so the wheel's metadata and the string the MCP
|
|
30
30
|
# server advertises are the same committed bytes, and neither is a function of when
|
|
31
31
|
# someone last ran `pip`.
|
|
32
|
-
__version__ = "0.
|
|
32
|
+
__version__ = "0.30.0"
|
|
@@ -30,10 +30,11 @@ THREE HARD RULES, each with the failure it prevents:
|
|
|
30
30
|
byte-compares both runtimes' streams; one stray write breaks the wire suite. Nothing
|
|
31
31
|
in this module touches `sys.stderr` or `sys.stdout`.
|
|
32
32
|
* **Metadata only.** Never a tool argument's value, never a memory body, never a
|
|
33
|
-
validated output, never a query, never a document row.
|
|
34
|
-
unbounded free text and
|
|
35
|
-
`skill_audit`'s `root` is another
|
|
36
|
-
set, never the path, never a part
|
|
33
|
+
validated output, never a query, never a document row. Seven of the thirteen tools take
|
|
34
|
+
unbounded free text and six take absolute paths (`bantamkit_read`'s `path` is one,
|
|
35
|
+
`skill_audit`'s `root` is another and `repo_map`'s `root` and `focus` are the third;
|
|
36
|
+
each record carries counts and tokens from a closed set, never the path, never a part
|
|
37
|
+
name, never a skill id, never a mapped file).
|
|
37
38
|
Every value written here is an ASCII token from a closed
|
|
38
39
|
set, an `int`, or a `bool`.
|
|
39
40
|
* **Never `str(exception)`.** Only `type(exc).__name__`. This is not hypothetical: the
|
|
@@ -17,7 +17,15 @@ from pathlib import Path
|
|
|
17
17
|
from typing import Annotated, Any
|
|
18
18
|
|
|
19
19
|
import bantamkit
|
|
20
|
-
from bantamkit import
|
|
20
|
+
from bantamkit import (
|
|
21
|
+
__version__,
|
|
22
|
+
docmanifest,
|
|
23
|
+
docread,
|
|
24
|
+
hostinstall,
|
|
25
|
+
repomap,
|
|
26
|
+
shiftwork,
|
|
27
|
+
skillaudit,
|
|
28
|
+
)
|
|
21
29
|
from bantamkit.assets import AssetNotFound, assets_root, load_skill, load_tool_asset
|
|
22
30
|
from bantamkit.client import BantamError
|
|
23
31
|
from bantamkit.contract import (
|
|
@@ -861,6 +869,59 @@ def _record_result(log: EventLog, tool: str, call: Callable[[], dict[str, Any]])
|
|
|
861
869
|
return answer
|
|
862
870
|
|
|
863
871
|
|
|
872
|
+
#: The last paragraph of every `repo_map` reply, refusal excepted. FIXED AND MANDATORY.
|
|
873
|
+
#:
|
|
874
|
+
#: Roadmap row 10's build gate was "build only after #4 shows discovery tokens dominate",
|
|
875
|
+
#: and #4 REFUTED it: discovery is 0.114 % of real prompt tokens because 97.8 % of the
|
|
876
|
+
#: bill is `cache_read`. The feature ships on an explicit ruling to build it anyway, as a
|
|
877
|
+
#: PRECISION feature. A surface that let a caller believe the map is a token saving would
|
|
878
|
+
#: say the one thing the measurement forbids, so the refutation travels with every answer
|
|
879
|
+
#: rather than living only in a doc nobody reads at call time.
|
|
880
|
+
#:
|
|
881
|
+
#: The second sentence is the budget's unit, for the same reason: `DEFAULT_BUDGET = 4000`
|
|
882
|
+
#: is "1 K tokens" only at the char/4 convention, whose error bar is unmeasured because
|
|
883
|
+
#: measuring it needs the tokenizer the pure-node ruling forbids. Bytes are what is
|
|
884
|
+
#: enforced, so bytes are what the reply says.
|
|
885
|
+
REPO_MAP_TAIL = (
|
|
886
|
+
"This is a precision pass, not a token saving: this feature's build gate was REFUTED "
|
|
887
|
+
"by measurement — discovery is 0.114% of real prompt tokens, because 97.8% of the "
|
|
888
|
+
"bill is cache_read — so a map does not make a session cheaper. What it buys is the "
|
|
889
|
+
"right file found sooner.\n"
|
|
890
|
+
"The budget above is UTF-8 BYTES of listing, not tokens: neither runtime carries a "
|
|
891
|
+
"model tokenizer and this tool will not pretend to one."
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
#: What a listing says when there is nothing to list. An empty string with two blank lines
|
|
895
|
+
#: around it is not an answer, and "0 files" is already on the header line — this names the
|
|
896
|
+
#: reason, which is the same discipline the omission footer is built on.
|
|
897
|
+
REPO_MAP_EMPTY = "(nothing listed: no file under this root scanned into a definition)"
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def repo_map_reply(result: repomap.RepoMap) -> str:
|
|
901
|
+
"""The `repo_map` tool's prose, byte for byte, from the structured result.
|
|
902
|
+
|
|
903
|
+
Split out of the handler so the two runtimes have ONE shape to reproduce rather than a
|
|
904
|
+
format string embedded in a `case`, and so the `repomap` conformance suite can compare
|
|
905
|
+
the rendered reply without standing up a server. No float is ever rendered here — see
|
|
906
|
+
`repomap`'s trap (8); every number on the header line is an int.
|
|
907
|
+
"""
|
|
908
|
+
focus = (
|
|
909
|
+
", ".join(result.focus)
|
|
910
|
+
if result.focus
|
|
911
|
+
else "(none) — plain centrality over the whole tree"
|
|
912
|
+
)
|
|
913
|
+
head = (
|
|
914
|
+
f"repo map: {result.nodes} files scanned, {result.definitions} definitions, "
|
|
915
|
+
f"{result.edges} edges.\n"
|
|
916
|
+
f"focus: {focus}\n"
|
|
917
|
+
f"budget: {result.budget} UTF-8 bytes; listing {result.listing_bytes} bytes; "
|
|
918
|
+
f"rendered {result.files_rendered} files, "
|
|
919
|
+
f"{result.definitions_rendered} definitions."
|
|
920
|
+
)
|
|
921
|
+
body = result.text if result.text else REPO_MAP_EMPTY
|
|
922
|
+
return f"{head}\n\n{body}\n\n{REPO_MAP_TAIL}"
|
|
923
|
+
|
|
924
|
+
|
|
864
925
|
def build_server(memory: Memory, log: EventLog | None = None) -> Any:
|
|
865
926
|
"""Assemble the MCP server around one Memory instance (the per-person state).
|
|
866
927
|
|
|
@@ -984,6 +1045,34 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
|
|
|
984
1045
|
)
|
|
985
1046
|
return _noted(outcome.reply)
|
|
986
1047
|
|
|
1048
|
+
def memory_dream(dry_run: bool | None = None) -> str:
|
|
1049
|
+
"""Consolidate what the project and the machine-wide profile layer both hold.
|
|
1050
|
+
|
|
1051
|
+
`dry_run` DEFAULTS TO TRUE and the default lives HERE rather than in the
|
|
1052
|
+
component: a client that omits the argument gets `None` through pydantic's
|
|
1053
|
+
`bool | None = None`, and turning that into the safe answer is this handler's
|
|
1054
|
+
job. It is the only tool on this surface that writes into the user's home
|
|
1055
|
+
directory, and the only one whose effect is machine-wide — a fact archived out
|
|
1056
|
+
of the profile store stops answering for every other project on this machine
|
|
1057
|
+
with no store of its own — so the short call is the preview.
|
|
1058
|
+
|
|
1059
|
+
The status is a decision the pass already made (`DreamResult.applied`,
|
|
1060
|
+
`.over_budget`, `.changes`), never a match on the reply.
|
|
1061
|
+
"""
|
|
1062
|
+
with _record_raise(log, "memory_dream"):
|
|
1063
|
+
outcome = memory.dream_outcome(True if dry_run is None else bool(dry_run))
|
|
1064
|
+
log.record(
|
|
1065
|
+
"memory_dream",
|
|
1066
|
+
outcome.status,
|
|
1067
|
+
{
|
|
1068
|
+
"absolutised": outcome.absolutised,
|
|
1069
|
+
"consumed": outcome.consumed,
|
|
1070
|
+
"dry_run": outcome.dry_run,
|
|
1071
|
+
"merged": outcome.merged,
|
|
1072
|
+
},
|
|
1073
|
+
)
|
|
1074
|
+
return _noted(outcome.reply)
|
|
1075
|
+
|
|
987
1076
|
def validate_json(output: str, schema: dict[str, Any]) -> dict[str, Any]:
|
|
988
1077
|
with _record_raise(log, "validate_json"):
|
|
989
1078
|
error = schema_error(output, schema)
|
|
@@ -1215,13 +1304,89 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
|
|
|
1215
1304
|
)
|
|
1216
1305
|
return _noted(result.as_json())
|
|
1217
1306
|
|
|
1307
|
+
def repo_map(
|
|
1308
|
+
root: str,
|
|
1309
|
+
focus: list[str] | None = None,
|
|
1310
|
+
budget: int | None = None,
|
|
1311
|
+
) -> str:
|
|
1312
|
+
"""The ranked definition map on the MCP surface: `repomap` measures, this serves it.
|
|
1313
|
+
|
|
1314
|
+
THE THREE REFUSALS LIVE HERE AND NOT IN `repomap.py`, and that is deliberate.
|
|
1315
|
+
`repo_map()` over a root that does not exist answers an EMPTY map on both runtimes
|
|
1316
|
+
— `os.walk` yields nothing for a missing directory and `walkSources`' `readdirSync`
|
|
1317
|
+
catch does the same — which is the right answer for a library and the wrong one for
|
|
1318
|
+
a tool: a caller who typed the path wrong would be told the tree holds no source.
|
|
1319
|
+
So the argument checks are the SURFACE's, the way `bantamkit_read`'s
|
|
1320
|
+
`refused-offset` is, and the engine J45-9/J45-10 proved byte-identical is not
|
|
1321
|
+
touched by this unit.
|
|
1322
|
+
|
|
1323
|
+
`focus` names the files the caller already has open; they are excluded from the
|
|
1324
|
+
listing. A focus entry that is not a scanned source is IGNORED, not refused —
|
|
1325
|
+
`repo_map` documents that, and refusing would make the tool useless the moment a
|
|
1326
|
+
caller named a file the scanner has no dialect for.
|
|
1327
|
+
|
|
1328
|
+
THE LAST PARAGRAPH IS FIXED AND MANDATORY. Row 10's build gate was refuted by
|
|
1329
|
+
measurement (0.114 % of real prompt tokens) and the feature ships on an explicit
|
|
1330
|
+
ruling to build it anyway; a surface that let a caller believe the map is a saving
|
|
1331
|
+
would be the one sentence this whole feature is not allowed to say.
|
|
1332
|
+
|
|
1333
|
+
THE RECORD IS A DECISION AND HOLDS NO PATH. `root` is what the operator typed and
|
|
1334
|
+
`focus` is the name of the file they are editing; neither is a decision this
|
|
1335
|
+
handler made, so neither is written down. The counts are.
|
|
1336
|
+
"""
|
|
1337
|
+
with _record_raise(log, "repo_map"):
|
|
1338
|
+
names = [str(f) for f in (focus or [])]
|
|
1339
|
+
wanted = repomap.DEFAULT_BUDGET if budget is None else int(budget)
|
|
1340
|
+
base = Path(root)
|
|
1341
|
+
if not root:
|
|
1342
|
+
log.record("repo_map", "refused")
|
|
1343
|
+
return _noted(
|
|
1344
|
+
tool_failed(
|
|
1345
|
+
"repo_map", "root must not be empty; name the directory to map"
|
|
1346
|
+
)
|
|
1347
|
+
)
|
|
1348
|
+
if wanted < 0:
|
|
1349
|
+
log.record("repo_map", "refused")
|
|
1350
|
+
return _noted(
|
|
1351
|
+
tool_failed("repo_map", f"budget must not be negative; got {wanted}")
|
|
1352
|
+
)
|
|
1353
|
+
# `Path.exists()` and `Path.is_dir()` both SWALLOW the not-here errno family
|
|
1354
|
+
# (ENOENT, ENOTDIR, ELOOP, EBADF) and re-raise anything else, so a dangling
|
|
1355
|
+
# symlink and `a-file.py/sub` are both "no such directory" while a permission
|
|
1356
|
+
# failure flies to `_record_raise` rather than being dressed up as a missing
|
|
1357
|
+
# tree. Node's `statSync` arm reproduces exactly that split; there is no
|
|
1358
|
+
# `except OSError` here because `repomap.repo_map` raises none — its walk and
|
|
1359
|
+
# its reads each already resolve a failure into a counted Omission.
|
|
1360
|
+
if not base.exists():
|
|
1361
|
+
log.record("repo_map", "refused")
|
|
1362
|
+
return _noted(tool_failed("repo_map", f"no such directory: {root}"))
|
|
1363
|
+
if not base.is_dir():
|
|
1364
|
+
log.record("repo_map", "refused")
|
|
1365
|
+
return _noted(
|
|
1366
|
+
tool_failed("repo_map", f"{root} is a file, not a directory to map")
|
|
1367
|
+
)
|
|
1368
|
+
result = repomap.repo_map(base, focus=names, budget=wanted)
|
|
1369
|
+
log.record(
|
|
1370
|
+
"repo_map",
|
|
1371
|
+
"mapped",
|
|
1372
|
+
{
|
|
1373
|
+
"definitions": result.definitions,
|
|
1374
|
+
"edges": result.edges,
|
|
1375
|
+
"files_rendered": result.files_rendered,
|
|
1376
|
+
"listing_bytes": result.listing_bytes,
|
|
1377
|
+
"nodes": result.nodes,
|
|
1378
|
+
},
|
|
1379
|
+
)
|
|
1380
|
+
return _noted(repo_map_reply(result))
|
|
1381
|
+
|
|
1218
1382
|
# The served surface, in one place, read out of the asset pack. Adding a tool here
|
|
1219
1383
|
# without an asset raises AssetNotFound at startup — the manifest cannot drift behind
|
|
1220
1384
|
# the server, because the server cannot start without it.
|
|
1221
1385
|
#
|
|
1222
1386
|
# `bantamkit_status` went LAST rather than first, `memory_compact` after it rather
|
|
1223
1387
|
# than beside `memory_save` where a reader would look for it, `bantamkit_read`
|
|
1224
|
-
# after that
|
|
1388
|
+
# after that, `skill_audit` after that, `memory_dream` after that and `repo_map`
|
|
1389
|
+
# after that. Registration order IS the served order
|
|
1225
1390
|
# (`test_tool_manifest.py::test_the_golden_records_the_order_the_wire_actually_
|
|
1226
1391
|
# serves`), and appending is the only edit that leaves the others where every
|
|
1227
1392
|
# existing declaration says they are.
|
|
@@ -1237,6 +1402,8 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
|
|
|
1237
1402
|
_from_manifest(memory_compact, "memory_compact"),
|
|
1238
1403
|
_from_manifest(bantamkit_read, "bantamkit_read"),
|
|
1239
1404
|
_from_manifest(skill_audit, "skill_audit"),
|
|
1405
|
+
_from_manifest(memory_dream, "memory_dream"),
|
|
1406
|
+
_from_manifest(repo_map, "repo_map"),
|
|
1240
1407
|
]
|
|
1241
1408
|
|
|
1242
1409
|
server = MCPServer(
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
from bantamkit.memory.component import
|
|
1
|
+
from bantamkit.memory.component import (
|
|
2
|
+
CompactOutcome,
|
|
3
|
+
DreamOutcome,
|
|
4
|
+
Memory,
|
|
5
|
+
RecallOutcome,
|
|
6
|
+
SaveOutcome,
|
|
7
|
+
)
|
|
2
8
|
from bantamkit.memory.divergence import (
|
|
3
9
|
BodyDiff,
|
|
4
10
|
DivergenceReport,
|
|
@@ -10,6 +16,13 @@ from bantamkit.memory.divergence import (
|
|
|
10
16
|
parse_fact,
|
|
11
17
|
read_store,
|
|
12
18
|
)
|
|
19
|
+
from bantamkit.memory.dream import (
|
|
20
|
+
DreamMerge,
|
|
21
|
+
DreamResult,
|
|
22
|
+
SimilarPair,
|
|
23
|
+
Superseded,
|
|
24
|
+
dream,
|
|
25
|
+
)
|
|
13
26
|
from bantamkit.memory.layers import (
|
|
14
27
|
MEMORY_DIR_ENV,
|
|
15
28
|
StoreBinding,
|
|
@@ -18,6 +31,7 @@ from bantamkit.memory.layers import (
|
|
|
18
31
|
)
|
|
19
32
|
from bantamkit.memory.store import (
|
|
20
33
|
DEFAULT_INDEX_BUDGET,
|
|
34
|
+
RECALL_MIN_SCORE_RATIO,
|
|
21
35
|
ArchivedFact,
|
|
22
36
|
CompactResult,
|
|
23
37
|
Fact,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`docs/memory.md` states, as a design decision, that `lint`, `archived` and
|
|
4
4
|
`restore` are **not** agent tools — "lifecycle is an operator decision, not a
|
|
5
|
-
model decision" — and the
|
|
5
|
+
model decision" — and the thirteen-tool surface holds to it; `compact` is the one
|
|
6
6
|
exception since job42 (`memory_compact`, the on-refusal path the model reaches
|
|
7
7
|
itself). That position is only coherent if the operator can actually make the
|
|
8
8
|
decision. Measured
|
|
@@ -10,6 +10,8 @@ from pathlib import Path
|
|
|
10
10
|
from bantamkit.agent import Agent, ToolDef
|
|
11
11
|
from bantamkit.assets import load_skill, load_tool
|
|
12
12
|
from bantamkit.client import BantamError
|
|
13
|
+
from bantamkit.memory.dream import SUPERSEDED_HEADING, DreamResult
|
|
14
|
+
from bantamkit.memory.dream import dream as _dream
|
|
13
15
|
from bantamkit.memory.layers import (
|
|
14
16
|
MEMORY_DIR_ENV,
|
|
15
17
|
PROJECT_STORE,
|
|
@@ -20,6 +22,7 @@ from bantamkit.memory.layers import (
|
|
|
20
22
|
)
|
|
21
23
|
from bantamkit.memory.store import (
|
|
22
24
|
DEFAULT_INDEX_BUDGET,
|
|
25
|
+
RECALL_MIN_SCORE_RATIO,
|
|
23
26
|
Fact,
|
|
24
27
|
MemoryBudgetExceeded,
|
|
25
28
|
MemoryStore,
|
|
@@ -114,6 +117,32 @@ class CompactOutcome:
|
|
|
114
117
|
budget: int
|
|
115
118
|
|
|
116
119
|
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class DreamOutcome:
|
|
122
|
+
"""What `dream` DID, beside the sentence it says about it.
|
|
123
|
+
|
|
124
|
+
The same seam as `SaveOutcome`, `RecallOutcome` and `CompactOutcome`: `status` is read
|
|
125
|
+
off a decision the pass already made — `DreamResult.applied`, `.over_budget`,
|
|
126
|
+
`.changes` — and never off the reply. `result` carries the whole diff for a caller that
|
|
127
|
+
wants the numbers rather than the prose.
|
|
128
|
+
|
|
129
|
+
`status` is one of `consolidated`, `previewed`, `nothing-to-consolidate`,
|
|
130
|
+
`refused-budget`, `no-profile-layer`.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
reply: str
|
|
134
|
+
status: str
|
|
135
|
+
dry_run: bool
|
|
136
|
+
merged: int
|
|
137
|
+
consumed: int
|
|
138
|
+
absolutised: int
|
|
139
|
+
superseded: int
|
|
140
|
+
index_before: int
|
|
141
|
+
index_after: int
|
|
142
|
+
budget: int
|
|
143
|
+
result: DreamResult | None = None
|
|
144
|
+
|
|
145
|
+
|
|
117
146
|
def _profile_store() -> Path:
|
|
118
147
|
"""The last layer `layered` appends, named once so other code can recognise it.
|
|
119
148
|
|
|
@@ -259,7 +288,12 @@ class Memory:
|
|
|
259
288
|
)
|
|
260
289
|
return SaveOutcome(reply=f"saved '{result.name}'", status="saved")
|
|
261
290
|
|
|
262
|
-
def recall(
|
|
291
|
+
def recall(
|
|
292
|
+
self,
|
|
293
|
+
query: str,
|
|
294
|
+
k: int | None = None,
|
|
295
|
+
min_ratio: float = RECALL_MIN_SCORE_RATIO,
|
|
296
|
+
) -> str:
|
|
263
297
|
"""`k` is the model asking for *more*, never for less than the store's default.
|
|
264
298
|
|
|
265
299
|
Measured cause (RB-P1, qwen2.5:14b-instruct): 57 of 60 `memory_recall` calls
|
|
@@ -273,9 +307,14 @@ class Memory:
|
|
|
273
307
|
The floor is the operator's configured default, not a constant, so a consumer
|
|
274
308
|
who really wants top-1 says so once at construction (`Memory(store, k=1)`).
|
|
275
309
|
"""
|
|
276
|
-
return self.recall_outcome(query, k).reply
|
|
310
|
+
return self.recall_outcome(query, k, min_ratio).reply
|
|
277
311
|
|
|
278
|
-
def recall_outcome(
|
|
312
|
+
def recall_outcome(
|
|
313
|
+
self,
|
|
314
|
+
query: str,
|
|
315
|
+
k: int | None = None,
|
|
316
|
+
min_ratio: float = RECALL_MIN_SCORE_RATIO,
|
|
317
|
+
) -> RecallOutcome:
|
|
279
318
|
"""`recall`, carrying the walk it performed as numbers rather than as prose.
|
|
280
319
|
|
|
281
320
|
The reply is byte-for-byte what `recall` has always returned; every field beside
|
|
@@ -291,6 +330,17 @@ class Memory:
|
|
|
291
330
|
layer whose directory refuses to list contributes nothing and raises the
|
|
292
331
|
`unreadable` count instead of being scored as empty; that distinction is the
|
|
293
332
|
whole subject of `_nothing_to_report` below and must not be undone here.
|
|
333
|
+
|
|
334
|
+
`min_ratio` (roadmap #6) rides through to every layer's `MemoryStore.recall`
|
|
335
|
+
unchanged, so the gate is measured against EACH LAYER's own best score and never
|
|
336
|
+
across layers: a profile fact does not have to out-score the project store's top
|
|
337
|
+
hit to be admitted, because the two stores are answering as two stores. Its
|
|
338
|
+
default is `RECALL_MIN_SCORE_RATIO` = 0.0, which keeps every fact `recall` was
|
|
339
|
+
going to return, and nothing on the tool path passes anything else today. A ratio
|
|
340
|
+
outside `[0.0, 1.0]` raises out of the FIRST layer, which is the writable project
|
|
341
|
+
store, so it surfaces as the error it is rather than as an `unreadable` count —
|
|
342
|
+
the read-only-layer `except` below would otherwise file a caller's bad argument as
|
|
343
|
+
a corrupt grant.
|
|
294
344
|
"""
|
|
295
345
|
budget = self.k if k is None else max(k, self.k)
|
|
296
346
|
picked: list[tuple[str, Fact]] = []
|
|
@@ -303,7 +353,7 @@ class Memory:
|
|
|
303
353
|
break # budget spent: later (read-only) layers are never even read
|
|
304
354
|
reached += 1
|
|
305
355
|
try:
|
|
306
|
-
facts = store.recall(query, budget, stamp=writable)
|
|
356
|
+
facts = store.recall(query, budget, stamp=writable, min_ratio=min_ratio)
|
|
307
357
|
except (BantamError, OSError, UnicodeDecodeError):
|
|
308
358
|
if writable:
|
|
309
359
|
raise # the project layer failing is a real error, as in v1
|
|
@@ -560,6 +610,158 @@ class Memory:
|
|
|
560
610
|
budget=result.budget,
|
|
561
611
|
)
|
|
562
612
|
|
|
613
|
+
def dream(self, dry_run: bool = True) -> str:
|
|
614
|
+
"""Consolidate what the project and profile layers hold under the same name."""
|
|
615
|
+
return self.dream_outcome(dry_run).reply
|
|
616
|
+
|
|
617
|
+
def dream_outcome(self, dry_run: bool = True) -> DreamOutcome:
|
|
618
|
+
"""`dream`, with the decision it took carried beside the sentence it wrote.
|
|
619
|
+
|
|
620
|
+
ONLY THE PROFILE LAYER IS CONSUMED. `_layers` also carries read-only GRANTS, and a
|
|
621
|
+
grant is another operator's store: consolidating a fact out of one is not this
|
|
622
|
+
person's move to make, so `dream` never looks at them. The label is matched exactly
|
|
623
|
+
(`profile`), never by prefix, because a grant is labelled `extra:<name>` and a
|
|
624
|
+
prefix match on a directory called `profile-something` would reach one.
|
|
625
|
+
|
|
626
|
+
A `Memory` constructed directly — not through `layered` — has no profile layer at
|
|
627
|
+
all, and that is `no-profile-layer` rather than an error: there is nothing to
|
|
628
|
+
consolidate ACROSS when only one layer is bound.
|
|
629
|
+
|
|
630
|
+
`dry_run` DEFAULTS TO TRUE. This is the only op in this component that writes into
|
|
631
|
+
the user's home directory, and it is the only one whose effect is machine-wide: a
|
|
632
|
+
fact archived out of the profile store stops answering for every other project on
|
|
633
|
+
this machine that has no store of its own. A destructive consolidation nobody can
|
|
634
|
+
preview is not shippable, so the safe call is the short one.
|
|
635
|
+
"""
|
|
636
|
+
profile = next((store for label, store, _ in self._layers if label == "profile"), None)
|
|
637
|
+
if profile is None:
|
|
638
|
+
return DreamOutcome(
|
|
639
|
+
reply=(
|
|
640
|
+
"nothing to consolidate: no profile layer is bound, so the project "
|
|
641
|
+
f"store {self.store.root} is the only layer there is."
|
|
642
|
+
),
|
|
643
|
+
status="no-profile-layer",
|
|
644
|
+
dry_run=dry_run,
|
|
645
|
+
merged=0,
|
|
646
|
+
consumed=0,
|
|
647
|
+
absolutised=0,
|
|
648
|
+
superseded=0,
|
|
649
|
+
index_before=0,
|
|
650
|
+
index_after=0,
|
|
651
|
+
budget=self.store.index_budget,
|
|
652
|
+
)
|
|
653
|
+
result = _dream(self.store, profile, dry_run)
|
|
654
|
+
if result.over_budget:
|
|
655
|
+
status = "refused-budget"
|
|
656
|
+
elif not result.changes:
|
|
657
|
+
status = "nothing-to-consolidate"
|
|
658
|
+
elif result.applied:
|
|
659
|
+
status = "consolidated"
|
|
660
|
+
else:
|
|
661
|
+
status = "previewed"
|
|
662
|
+
return DreamOutcome(
|
|
663
|
+
reply=self._dream_reply(status, result),
|
|
664
|
+
status=status,
|
|
665
|
+
dry_run=result.dry_run,
|
|
666
|
+
merged=len(result.merged),
|
|
667
|
+
consumed=len(result.consumed),
|
|
668
|
+
absolutised=len(result.absolutised),
|
|
669
|
+
superseded=len(result.superseded),
|
|
670
|
+
index_before=result.index_before,
|
|
671
|
+
index_after=result.index_after,
|
|
672
|
+
budget=result.budget,
|
|
673
|
+
result=result,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
@staticmethod
|
|
677
|
+
def _dream_reply(status: str, result: DreamResult) -> str:
|
|
678
|
+
"""The whole diff as prose: what merged, what was dated, what moved, what it cost.
|
|
679
|
+
|
|
680
|
+
THE HONESTY SENTENCE AT THE END IS NOT DECORATION. Row 5 was planned as a token
|
|
681
|
+
saving and J45-1 measured that it is not one: the profile store has no `index.md`
|
|
682
|
+
on disk, its index is derived at read time, and nothing loads it into a prompt — so
|
|
683
|
+
deduplicating it frees approximately zero prompt bytes. The value is that one
|
|
684
|
+
ruling now has one copy instead of two that can disagree, and the reply says which
|
|
685
|
+
of those two things the operator just bought.
|
|
686
|
+
"""
|
|
687
|
+
lines: list[str] = []
|
|
688
|
+
if status == "nothing-to-consolidate":
|
|
689
|
+
lines.append(
|
|
690
|
+
f"nothing to consolidate: {result.project_root} and {result.profile_root} "
|
|
691
|
+
f"hold no fact under the same name, and no project fact carries a relative "
|
|
692
|
+
f"date this pass will resolve."
|
|
693
|
+
)
|
|
694
|
+
else:
|
|
695
|
+
identical = sum(1 for merge in result.merged if merge.kind == "identical")
|
|
696
|
+
diverged = len(result.merged) - identical
|
|
697
|
+
verb = {
|
|
698
|
+
"consolidated": "consolidated",
|
|
699
|
+
"previewed": "would consolidate",
|
|
700
|
+
"refused-budget": "would consolidate",
|
|
701
|
+
}[status]
|
|
702
|
+
lines.append(
|
|
703
|
+
f"{verb} {len(result.merged)} cross-layer name collision(s) — {identical} "
|
|
704
|
+
f"byte-identical, {diverged} diverged and unioned — and rewrote "
|
|
705
|
+
f"{len(result.rewritten)} project fact(s). The survivor stays in the "
|
|
706
|
+
f"project layer; the profile copy moves to {result.archive_dir} and is NOT "
|
|
707
|
+
f"deleted: restore it by name."
|
|
708
|
+
)
|
|
709
|
+
for merge in result.merged:
|
|
710
|
+
lines.append(
|
|
711
|
+
f"- {merge.name} ({merge.kind}, name+description similarity "
|
|
712
|
+
f"{merge.jaccard:.3f}) — body {merge.body_before} -> {merge.body_after} "
|
|
713
|
+
f"bytes, {merge.blocks_added} block(s) kept from the {merge.consumed_layer} "
|
|
714
|
+
f"copy, survivor in {merge.survivor_layer}"
|
|
715
|
+
)
|
|
716
|
+
for record in result.superseded:
|
|
717
|
+
lines.append(
|
|
718
|
+
f"- superseded '{record.subject}': the {record.lost_layer} copy "
|
|
719
|
+
f"({record.lost_date}) lost to the {record.kept_layer} copy "
|
|
720
|
+
f"({record.kept_date}); the older claim is kept verbatim under "
|
|
721
|
+
f"'{SUPERSEDED_HEADING}'"
|
|
722
|
+
)
|
|
723
|
+
for hit in result.absolutised:
|
|
724
|
+
lines.append(
|
|
725
|
+
f"- dated {hit.name} ({hit.layer}): '{hit.term}' -> {hit.resolved}, "
|
|
726
|
+
f"resolved against that fact's own mtime {hit.basis}, not today"
|
|
727
|
+
)
|
|
728
|
+
for hit in result.unresolved:
|
|
729
|
+
lines.append(
|
|
730
|
+
f"- left alone in {hit.name} ({hit.layer}): '{hit.term}' — no exact day "
|
|
731
|
+
f"follows from an mtime, so nothing was substituted"
|
|
732
|
+
)
|
|
733
|
+
for name, reason in result.refused:
|
|
734
|
+
lines.append(f"- refused {name}: {reason}")
|
|
735
|
+
for pair in result.similar_unmerged:
|
|
736
|
+
lines.append(
|
|
737
|
+
f"- similar but NOT merged: {pair.project_name} (project) and "
|
|
738
|
+
f"{pair.profile_name} (profile) score {pair.jaccard:.3f}; this pass merges "
|
|
739
|
+
f"on name equality only, so nothing was done about it"
|
|
740
|
+
)
|
|
741
|
+
lines.append(
|
|
742
|
+
f"project index {result.index_before} -> {result.index_after} bytes against a "
|
|
743
|
+
f"{result.budget}-byte budget; the profile index (derived, no file on disk) "
|
|
744
|
+
f"{result.profile_index_before} -> {result.profile_index_after}; fact bytes "
|
|
745
|
+
f"across both layers {result.fact_bytes_before} -> {result.fact_bytes_after}."
|
|
746
|
+
)
|
|
747
|
+
if status == "refused-budget":
|
|
748
|
+
lines.append(
|
|
749
|
+
f"NOTHING WAS WRITTEN: the merged index would be {result.index_after} bytes "
|
|
750
|
+
f"against a {result.budget}-byte budget. Call `memory_compact` to archive "
|
|
751
|
+
f"the stalest facts, then run this again."
|
|
752
|
+
)
|
|
753
|
+
elif result.dry_run:
|
|
754
|
+
lines.append(
|
|
755
|
+
"DRY RUN: nothing was written. Re-run with dry_run false to apply it."
|
|
756
|
+
)
|
|
757
|
+
lines.append(
|
|
758
|
+
"This is a correctness pass, not a token saving: the profile index is derived "
|
|
759
|
+
"at read time and is not loaded from a file, so consolidating it frees "
|
|
760
|
+
"approximately no prompt bytes. What it buys is one copy of a ruling instead "
|
|
761
|
+
"of two that can diverge."
|
|
762
|
+
)
|
|
763
|
+
return "\n".join(lines)
|
|
764
|
+
|
|
563
765
|
# Back-compat aliases: the component's API predates the public names.
|
|
564
766
|
_save = save
|
|
565
767
|
_recall = recall
|