memleaf 0.2.40__tar.gz → 0.2.41__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.
- {memleaf-0.2.40 → memleaf-0.2.41}/CHANGELOG.md +9 -0
- {memleaf-0.2.40/src/memleaf.egg-info → memleaf-0.2.41}/PKG-INFO +2 -2
- {memleaf-0.2.40 → memleaf-0.2.41}/README.en.md +1 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/README.md +1 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/pyproject.toml +1 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/admission.py +23 -249
- memleaf-0.2.41/src/memleaf/batch_review.py +205 -0
- memleaf-0.2.41/src/memleaf/evidence_structure.py +206 -0
- memleaf-0.2.41/src/memleaf/evidence_syntax.py +71 -0
- memleaf-0.2.41/src/memleaf/hermes_provider/__init__.py +34 -0
- memleaf-0.2.41/src/memleaf/hermes_provider/_mcp_client.py +399 -0
- memleaf-0.2.40/src/memleaf/hermes_provider/__init__.py → memleaf-0.2.41/src/memleaf/hermes_provider/_provider.py +18 -1359
- memleaf-0.2.41/src/memleaf/hermes_provider/_shared.py +994 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/installer.py +1 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/llm/base.py +7 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/llm/router.py +20 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/llm/thinking.py +1 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/memory_planner.py +17 -6
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/model_execution.py +288 -22
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/planning_context.py +156 -16
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/process_common.py +28 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/process_jobs.py +3 -2
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/processing.py +2 -2
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/prompts.py +113 -20
- memleaf-0.2.41/src/memleaf/single_pass_memory_planner.py +681 -0
- memleaf-0.2.41/src/memleaf/single_pass_plan.py +516 -0
- memleaf-0.2.41/src/memleaf/summary_batch.py +204 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/update_coordinator.py +73 -42
- {memleaf-0.2.40 → memleaf-0.2.41/src/memleaf.egg-info}/PKG-INFO +2 -2
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf.egg-info/SOURCES.txt +36 -1
- memleaf-0.2.41/tests/hermes_provider_support.py +189 -0
- memleaf-0.2.41/tests/stage_b1_support.py +110 -0
- memleaf-0.2.41/tests/stage_b2a_support.py +139 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_admission_noise.py +3 -2
- memleaf-0.2.41/tests/test_audit_followup_v041.py +496 -0
- memleaf-0.2.41/tests/test_b3_activation.py +131 -0
- memleaf-0.2.41/tests/test_b3_closeout.py +302 -0
- memleaf-0.2.41/tests/test_b3_planning_context.py +74 -0
- memleaf-0.2.41/tests/test_b3_scope_provenance.py +187 -0
- memleaf-0.2.41/tests/test_b3_single_pass_memory_planner.py +313 -0
- memleaf-0.2.41/tests/test_b3_single_pass_plan.py +436 -0
- memleaf-0.2.41/tests/test_batch_review.py +215 -0
- memleaf-0.2.41/tests/test_batch_review_integration.py +146 -0
- memleaf-0.2.41/tests/test_gate_output_protocol_v041.py +222 -0
- memleaf-0.2.41/tests/test_gate_output_references_v041.py +49 -0
- memleaf-0.2.41/tests/test_gate_protocol_matrix_v041.py +112 -0
- memleaf-0.2.41/tests/test_hermes_provider.py +9 -0
- memleaf-0.2.41/tests/test_hermes_provider_01.py +624 -0
- memleaf-0.2.41/tests/test_hermes_provider_02.py +705 -0
- memleaf-0.2.41/tests/test_hermes_provider_03.py +663 -0
- memleaf-0.2.41/tests/test_hermes_provider_04.py +331 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_hermes_windows_subprocess.py +1 -1
- memleaf-0.2.41/tests/test_model_invalid_response_metrics_v041.py +87 -0
- memleaf-0.2.41/tests/test_p3_summary_batch.py +233 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_prompt_role_slim_v040.py +2 -1
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b1.py +8 -601
- memleaf-0.2.41/tests/test_stage_b1_01.py +497 -0
- memleaf-0.2.41/tests/test_stage_b2a.py +9 -0
- memleaf-0.2.41/tests/test_stage_b2a_01.py +530 -0
- memleaf-0.2.41/tests/test_stage_b2a_02.py +674 -0
- memleaf-0.2.41/tests/test_stage_b2a_03.py +641 -0
- memleaf-0.2.41/tests/test_stage_b2a_04.py +629 -0
- memleaf-0.2.41/tests/test_stage_b2a_05.py +317 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_update_review.py +6 -4
- memleaf-0.2.40/docs/performance.md +0 -99
- memleaf-0.2.40/tests/test_hermes_provider.py +0 -2503
- memleaf-0.2.40/tests/test_stage_b2a.py +0 -2918
- {memleaf-0.2.40 → memleaf-0.2.41}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/LICENSE +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/MANIFEST.in +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/config-migrations.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/core-refactor.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/general-processing.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/examples/README.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/examples/basic_usage.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/examples/live_core_lifecycle_acceptance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/examples/live_processing_acceptance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/install.ps1 +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/install.sh +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/setup.cfg +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/config.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/index.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/models.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/service.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/__init__.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/semantic_fixtures.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_automatic_duplicate_noop_collision.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_candidate_polarity.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_config_migrations_v028.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_conversation_only.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_cross_turn_dedupe_regressions.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_due_date_grounding.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_due_date_grounding_retry.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_email_actionable_coverage.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_evidence_budget.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_evidence_retention_policy.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_external_source_dates.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_extraction_quality_regressions.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_gate_capacity.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_gate_schema_repair.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_gate_scope_latency_v038.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_general_evidence_admission.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_general_tool_provenance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_global_todo_acceptance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_global_todo_query_no_write.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_global_todo_retrieval.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_hermes_native_registration.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_hermes_runtime_install.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_hermes_transport_evidence.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_host_events.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_inspection_state_v028.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_install.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_long_run_hygiene.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_model_owned_fields.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_new_scope_source_grounding.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_partial_retry_idempotency.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_phase2_model_decisions.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_process_jobs.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_process_owner_locking.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_processing_contract_v026.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_processing_observability_concurrency.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_provider_neutral_thinking_v039.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_pypi_install.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_read_only_deferred_isolation.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_review_source_context.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_revision_digest.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_shared_memory_refactor.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_source_neutral_todos_v028.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_c1_mcp.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_stage_c3_packaging.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_state_layout_v028.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_summary_date_grounding_integration.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_target_reconciliation.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_target_reconciliation_integration.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_todo_state_recovery.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_update_target_recovery.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_v023_scope_correction.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_v2_search_gate_acceptance.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_whole_unit_bindings.py +0 -0
- {memleaf-0.2.40 → memleaf-0.2.41}/tests/test_windows_public_mcp_launcher.py +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.41 — 2026-09-11
|
|
6
|
+
|
|
7
|
+
- Promote the B3 single-pass automatic memory planner for explicitly `single_pass_safe` API backends. Core prepares bounded local retrieval/context once, one semantic planner stage decides CREATE / UPDATE / NO_CHANGE / DEFERRED, deterministic validation remains authoritative, and model-output repair is bounded to at most one retry (`max_attempts=2`). Host/custom/host-backed providers keep the proven P3 fallback, while explicit `remember` retains its existing single-summary path.
|
|
8
|
+
- Make project Scope provenance Core-owned in the single-pass path. The model can no longer authorize `scope_source`; Core derives `user`, `session_context`, `insufficient_context`, or `model` from the selected scope set and applies deterministic grounding for model-derived project affiliation. Legacy model-provided `scope_source` is tolerated only for compatibility and ignored.
|
|
9
|
+
- Carry forward the P0/P2/P3 transport reductions used by the fallback path, including bounded batch semantic review and CREATE-summary batching, while preserving Gate admission, target reconciliation, history/audit/commit semantics, idempotency, provider-neutral `thinking=low`, and fail-closed handling for invalid or unresolved evidence.
|
|
10
|
+
- Remove committed long-run benchmark scripts/results/docs from `main` and split oversized admission, Hermes provider, and large regression-test modules by responsibility. This is repository-maintainability cleanup rather than a search-performance claim; regression coverage is retained rather than deleted.
|
|
11
|
+
- Preserve the product architecture: Markdown under `knowledge/` remains the active-memory source of truth, `history/` remains historical state, permanent memory stays globally shared across agents using the same Vault, provenance/session fields do not become visibility filters, and no database, Redis, vector service, daemon, background resident service, or local-model dependency is introduced.
|
|
12
|
+
- Validation for the integrated production tree passed the full Linux Python 3.11/3.12/3.13, Windows Python 3.11/3.12/3.13, macOS Python 3.11/3.13, wheel/sdist, installed-entry-point, Hermes/Codex host acceptance, and native Codex Windows/macOS matrices before release.
|
|
13
|
+
|
|
5
14
|
## 0.2.40 — 2026-09-09
|
|
6
15
|
|
|
7
16
|
- Slim the automatic extraction stage prompts without changing output schemas, parsers, evidence segmentation, model routing, the provider-neutral `thinking=low` policy, target/revision handling, idempotency, or commit semantics. Gate owns admission, atomic splitting, attribution and duplicate/update selection; Summary writes one already-admitted current-state memory; semantic review verifies fidelity; Core keeps deterministic validation and write safety.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.41
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
25
25
|
|
|
26
|
-
> **版本:0.2.
|
|
26
|
+
> **版本:0.2.41。**
|
|
27
27
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[中文](README.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **Version: 0.2.
|
|
7
|
+
> **Version: 0.2.41.**
|
|
8
8
|
> Automatic extraction now uses only the current turn's visible user input and final assistant reply. Raw tool output, attachments, web/file/terminal payloads and legacy tool-evidence bodies are not new source evidence; existing or retrieved memory remains comparison context rather than source authority. Background processing is persisted as a local job and can be checked through the read-only `process_status` MCP tool; failed work remains retryable and fail closed. The release also tightens source/date grounding, target reconciliation, duplicate/no-op handling and semantic review before writes. Markdown remains the sole source of truth with no SQLite runtime dependency. Acceptance covers deterministic regression suites and synthetic inputs; it does not claim real-mail or customer-business acceptance.
|
|
9
9
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **版本:0.2.
|
|
7
|
+
> **版本:0.2.41。**
|
|
8
8
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.41"
|
|
4
4
|
|
|
5
5
|
from .config import DEFAULT_CONFIG, default_config, load_config, save_config
|
|
6
6
|
from .frontmatter import FrontmatterError, dump_frontmatter, dump_yaml, load_yaml, parse_frontmatter
|
|
@@ -14,6 +14,14 @@ import unicodedata
|
|
|
14
14
|
from typing import Any, Iterable, Mapping
|
|
15
15
|
|
|
16
16
|
from .validation import ModelOutputError, parse_strict_json
|
|
17
|
+
from .evidence_syntax import (
|
|
18
|
+
_BULLET, _CLOSED_TASK, _EXAMPLE, _EXTERNAL_OWNER, _HEADING, _NEGATIVE_TASK,
|
|
19
|
+
_POLITE, _QUERY_START, _QUERY_WORD, _READ_ONLY_CONTROL, _clauses, _query,
|
|
20
|
+
)
|
|
21
|
+
from .evidence_structure import (
|
|
22
|
+
MAX_EXTERNAL_UNIT_BYTES, _EXTERNAL_MARKER, _external_blocks,
|
|
23
|
+
_has_external_structure, _structured_external_blocks,
|
|
24
|
+
)
|
|
17
25
|
|
|
18
26
|
|
|
19
27
|
# Tool capture already bounds ordinary records to 32 KiB. JSON and unstructured
|
|
@@ -21,38 +29,13 @@ from .validation import ModelOutputError, parse_strict_json
|
|
|
21
29
|
# explicit plain-text structure may be split into bounded semantic sections.
|
|
22
30
|
# Oversized legacy records are split only when necessary; every block retains
|
|
23
31
|
# the original record identity in its EvidenceUnit metadata.
|
|
24
|
-
MAX_EXTERNAL_UNIT_BYTES = 32 * 1024
|
|
25
32
|
MAX_GATE_BATCH_UNITS = 8
|
|
26
33
|
MAX_GATE_BATCH_BYTES = 64 * 1024
|
|
27
34
|
|
|
28
35
|
# Syntax recognizers, not a catalogue of business scenarios or tool names.
|
|
29
|
-
_POLITE = re.compile(r"^(?:(?:麻烦你|麻烦|请问|请|帮我|替我|劳驾)\s*)+")
|
|
30
|
-
_QUERY_START = re.compile(
|
|
31
|
-
r"^(?:查询|查一下|查下|看看|看下|查看|阅读|读取|检查|汇总|列出|罗列|告诉我|梳理|盘点|总结|给我|"
|
|
32
|
-
r"把.+(?:列出|发我|告诉我|整理|汇总|梳理|总结)|(?:please\s+)?(?:list|show|tell|summari[sz]e|"
|
|
33
|
-
r"recap|check|find|what|which|who|when|where|why|how)\b)", re.I)
|
|
34
|
-
_QUERY_WORD = re.compile(r"有没有|有什么|有哪些|是什么|是谁|多少|哪个|哪些|什么时候|何时|"
|
|
35
|
-
r"如何|怎么|为什么|是否|能否|可否|\b(?:what|which|who|when|where|why|how)\b", re.I)
|
|
36
36
|
# A complete, standalone control sentence that only tells memleaf not to
|
|
37
37
|
# mutate memory is still a query. Keep this deliberately narrow: project
|
|
38
38
|
# constraints such as "不要修改数据库配置" remain user assertions.
|
|
39
|
-
_READ_ONLY_CONTROL = re.compile(
|
|
40
|
-
r"^(?:(?:不要|不|请勿|勿)\s*(?:修改|更新|写入|保存|删除)\s*记忆|"
|
|
41
|
-
r"(?:please\s+)?(?:do\s+not|don['’]t)\s+(?:modify|update|write|save|delete)\s+memor(?:y|ies))$",
|
|
42
|
-
re.IGNORECASE,
|
|
43
|
-
)
|
|
44
|
-
_EXAMPLE = re.compile(r"(?:仅供.{0,8}(?:参考示例|示例|测试)|举(?:一个|个).{0,16}(?:例子|示例)|"
|
|
45
|
-
r"(?:只是|以下是|这是|作为).{0,12}(?:示例|样例|模板|测试数据)|"
|
|
46
|
-
r"假设|例如|测试数据|不要.{0,16}(?:记住|记录|当成真实))|"
|
|
47
|
-
r"\b(?:example|hypothetical|suppose|fictional|test fixture)\b", re.I)
|
|
48
|
-
_HEADING = re.compile(r"^\s*(?:#{1,6}\s+.+|\d+[.)、]\s*[^。;;\n]{1,100}[::]\s*.*)$")
|
|
49
|
-
_BULLET = re.compile(r"^\s*(?:[-*•]|\d+[.)、])\s+")
|
|
50
|
-
_NEGATIVE_TASK = re.compile(r"无需|不需要|不用|不必|无须|毋须|(?:没有|不存在).{0,12}(?:需要|待办|问题)|"
|
|
51
|
-
r"\b(?:no need|need not|not required|does not need|do not need)\b", re.I)
|
|
52
|
-
_CLOSED_TASK = re.compile(r"(?:已|已经).{0,4}(?:全部|均)?(?:完成|取消|解决|关闭)|"
|
|
53
|
-
r"\b(?:already (?:done|completed|cancelled)|all .{0,20}(?:resolved|completed))\b", re.I)
|
|
54
|
-
_EXTERNAL_OWNER = re.compile(r"(?:客户|供应商|第三方)(?:自行|自己)?(?:需要|需|负责|必须|应当|要(?!求))|"
|
|
55
|
-
r"\b(?:customer|vendor|supplier|third party)\s+(?:must|needs? to|is responsible)\b", re.I)
|
|
56
39
|
|
|
57
40
|
|
|
58
41
|
@dataclass(frozen=True)
|
|
@@ -128,206 +111,12 @@ def partition_evidence_units(units: Iterable[EvidenceUnit]) -> EvidencePartition
|
|
|
128
111
|
return EvidencePartition(tuple(physical), tuple(non_physical), tuple(unresolved))
|
|
129
112
|
|
|
130
113
|
|
|
131
|
-
def _external_blocks(text: str) -> Iterable[tuple[int, int, str, str, tuple[str, ...]]]:
|
|
132
|
-
"""Yield deterministic, exact source blocks for one external record.
|
|
133
114
|
|
|
134
|
-
JSON documents remain whole records. Plain text that contains explicit
|
|
135
|
-
structure is divided at paragraphs, headings, numbered items and bullets
|
|
136
|
-
so coverage can account for each actionable item. Ordinary prose and
|
|
137
|
-
line oriented logs remain whole records; punctuation never creates a
|
|
138
|
-
fragment. The oversized fallback is byte bounded and always returns
|
|
139
|
-
Python character offsets.
|
|
140
|
-
"""
|
|
141
115
|
|
|
142
|
-
stripped = text.lstrip()
|
|
143
|
-
is_json = False
|
|
144
|
-
if stripped.startswith(("{", "[")):
|
|
145
|
-
try:
|
|
146
|
-
json.loads(text)
|
|
147
|
-
except (TypeError, ValueError):
|
|
148
|
-
pass
|
|
149
|
-
else:
|
|
150
|
-
is_json = True
|
|
151
|
-
|
|
152
|
-
# Explicit record dividers delimit complete observations in a batched
|
|
153
|
-
# text result. Keep each record's header and paragraphs together so they
|
|
154
|
-
# cannot drift into unrelated model batches. This recognizes layout only;
|
|
155
|
-
# it assigns no business meaning, owner, scope or source authority.
|
|
156
|
-
dividers = list(re.finditer(r"(?m)^[ \t]*(?:={8,}|-{8,}|\*{8,})[ \t]*\r?$", text)) if not is_json else []
|
|
157
|
-
if dividers:
|
|
158
|
-
boundaries = sorted({0, *(match.start() for match in dividers), len(text)})
|
|
159
|
-
for left, right in zip(boundaries, boundaries[1:]):
|
|
160
|
-
block = text[left:right]
|
|
161
|
-
if not block.strip():
|
|
162
|
-
continue
|
|
163
|
-
if re.fullmatch(r"[ \t]*(?:={8,}|-{8,}|\*{8,})[ \t\r\n]*", block):
|
|
164
|
-
continue
|
|
165
|
-
# A divider is structural context, not an independent assertion.
|
|
166
|
-
if len(block.encode("utf-8")) <= MAX_EXTERNAL_UNIT_BYTES:
|
|
167
|
-
yield left, right, block, "external_record", ()
|
|
168
|
-
else:
|
|
169
|
-
# Avoid recursively recognizing the same leading divider.
|
|
170
|
-
cursor = left
|
|
171
|
-
while cursor < right:
|
|
172
|
-
end = cursor
|
|
173
|
-
size = 0
|
|
174
|
-
while end < right:
|
|
175
|
-
width = len(text[end].encode("utf-8"))
|
|
176
|
-
if end > cursor and size + width > MAX_EXTERNAL_UNIT_BYTES:
|
|
177
|
-
break
|
|
178
|
-
size += width
|
|
179
|
-
end += 1
|
|
180
|
-
yield cursor, end, text[cursor:end], "external_block", ()
|
|
181
|
-
cursor = end
|
|
182
|
-
return
|
|
183
|
-
|
|
184
|
-
if len(text.encode("utf-8")) <= MAX_EXTERNAL_UNIT_BYTES and (
|
|
185
|
-
is_json or not _has_external_structure(text)
|
|
186
|
-
):
|
|
187
|
-
yield 0, len(text), text, "external_record", ()
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
if len(text.encode("utf-8")) <= MAX_EXTERNAL_UNIT_BYTES:
|
|
191
|
-
yield from _structured_external_blocks(text)
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
start = 0
|
|
195
|
-
while start < len(text):
|
|
196
|
-
end = start
|
|
197
|
-
encoded = 0
|
|
198
|
-
while end < len(text):
|
|
199
|
-
width = len(text[end].encode("utf-8"))
|
|
200
|
-
if end > start and encoded + width > MAX_EXTERNAL_UNIT_BYTES:
|
|
201
|
-
break
|
|
202
|
-
encoded += width
|
|
203
|
-
end += 1
|
|
204
|
-
if end <= start:
|
|
205
|
-
# A single code point larger than the budget is impossible for a
|
|
206
|
-
# normal Unicode scalar, but make progress defensively.
|
|
207
|
-
end = min(start + 1, len(text))
|
|
208
|
-
yield start, end, text[start:end], "external_block", ()
|
|
209
|
-
start = end
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
_EXTERNAL_MARKER = re.compile(r"^\s*(?:#{1,6}\s+|[-*+•]\s+|\d+[.)、]\s+)")
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _has_external_structure(text: str) -> bool:
|
|
216
|
-
"""Recognize structural boundaries without treating every line as one."""
|
|
217
|
-
|
|
218
|
-
if "\n\n" in text or "\r\n\r\n" in text:
|
|
219
|
-
return True
|
|
220
|
-
for line in text.splitlines():
|
|
221
|
-
value = line.strip()
|
|
222
|
-
if not value:
|
|
223
|
-
continue
|
|
224
|
-
if _EXTERNAL_MARKER.match(line) or value.endswith((":", ":")):
|
|
225
|
-
return True
|
|
226
|
-
return False
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def _structured_external_blocks(
|
|
230
|
-
text: str,
|
|
231
|
-
) -> Iterable[tuple[int, int, str, str, tuple[str, ...]]]:
|
|
232
|
-
"""Split explicit text structure while retaining parent section context."""
|
|
233
|
-
|
|
234
|
-
# ``splitlines(True)`` keeps offsets exact while allowing us to discard
|
|
235
|
-
# only structural whitespace at each emitted boundary.
|
|
236
|
-
lines: list[tuple[int, int, str, str]] = []
|
|
237
|
-
cursor = 0
|
|
238
|
-
for raw in text.splitlines(True):
|
|
239
|
-
line_end = cursor + len(raw)
|
|
240
|
-
body = raw[:-1] if raw.endswith("\n") else raw
|
|
241
|
-
if body.endswith("\r"):
|
|
242
|
-
body = body[:-1]
|
|
243
|
-
lines.append((cursor, line_end, body, raw))
|
|
244
|
-
cursor = line_end
|
|
245
|
-
if cursor < len(text):
|
|
246
|
-
lines.append((cursor, len(text), text[cursor:], text[cursor:]))
|
|
247
|
-
if not lines:
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
# Stack entries are ``(indent, label, kind)``. Headings remain in scope for
|
|
251
|
-
# sibling numbered items; prior items only remain in scope for indented
|
|
252
|
-
# children such as the two Morgan bullets in the regression digest.
|
|
253
|
-
contexts: list[tuple[int, str, str]] = []
|
|
254
|
-
current_start: int | None = None
|
|
255
|
-
current_end: int | None = None
|
|
256
|
-
current_section: tuple[str, ...] = ()
|
|
257
|
-
current_syntax = "external_paragraph"
|
|
258
|
-
|
|
259
|
-
def emit_current() -> tuple[int, int, str, str, tuple[str, ...]] | None:
|
|
260
|
-
if current_start is None or current_end is None or current_start >= current_end:
|
|
261
|
-
return None
|
|
262
|
-
return (
|
|
263
|
-
current_start,
|
|
264
|
-
current_end,
|
|
265
|
-
text[current_start:current_end],
|
|
266
|
-
current_syntax,
|
|
267
|
-
current_section,
|
|
268
|
-
)
|
|
269
116
|
|
|
270
|
-
for line_start, line_end, body, raw in lines:
|
|
271
|
-
left = len(body) - len(body.lstrip())
|
|
272
|
-
right = len(body.rstrip())
|
|
273
|
-
value = body.strip()
|
|
274
|
-
if not value:
|
|
275
|
-
emitted = emit_current()
|
|
276
|
-
if emitted is not None:
|
|
277
|
-
yield emitted
|
|
278
|
-
current_start = current_end = None
|
|
279
|
-
current_section = ()
|
|
280
|
-
current_syntax = "external_paragraph"
|
|
281
|
-
continue
|
|
282
117
|
|
|
283
|
-
indent = left
|
|
284
|
-
marker = _EXTERNAL_MARKER.match(body)
|
|
285
|
-
heading = bool(re.match(r"^\s*#{1,6}\s+", body)) or (
|
|
286
|
-
not marker and value.endswith((":", ":"))
|
|
287
|
-
)
|
|
288
|
-
structural = bool(marker) or heading
|
|
289
|
-
if structural:
|
|
290
|
-
emitted = emit_current()
|
|
291
|
-
if emitted is not None:
|
|
292
|
-
yield emitted
|
|
293
|
-
current_start = line_start + left
|
|
294
|
-
current_end = line_start + right
|
|
295
|
-
current_syntax = "external_section"
|
|
296
|
-
|
|
297
|
-
if heading:
|
|
298
|
-
contexts = [
|
|
299
|
-
(level, label, kind)
|
|
300
|
-
for level, label, kind in contexts
|
|
301
|
-
if level < indent
|
|
302
|
-
]
|
|
303
|
-
current_section = tuple(label for _, label, _ in contexts)
|
|
304
|
-
contexts.append((indent, value, "heading"))
|
|
305
|
-
else:
|
|
306
|
-
# Same-level numbered/bullet siblings replace the previous
|
|
307
|
-
# item, while a heading at that level remains their context.
|
|
308
|
-
contexts = [
|
|
309
|
-
(level, label, kind)
|
|
310
|
-
for level, label, kind in contexts
|
|
311
|
-
if level < indent or (level == indent and kind == "heading")
|
|
312
|
-
]
|
|
313
|
-
current_section = tuple(label for _, label, _ in contexts)
|
|
314
|
-
contexts.append((indent, value, "item"))
|
|
315
|
-
continue
|
|
316
118
|
|
|
317
|
-
# Non-structural lines continue the current item/paragraph. This keeps
|
|
318
|
-
# wrapped prose together and avoids turning line-oriented logs into one
|
|
319
|
-
# evidence unit per line.
|
|
320
|
-
line_content_start = line_start + left
|
|
321
|
-
line_content_end = line_start + right
|
|
322
|
-
if current_start is None:
|
|
323
|
-
current_start = line_content_start
|
|
324
|
-
current_section = tuple(label for _, label, _ in contexts)
|
|
325
|
-
current_syntax = "external_paragraph"
|
|
326
|
-
current_end = line_content_end
|
|
327
119
|
|
|
328
|
-
emitted = emit_current()
|
|
329
|
-
if emitted is not None:
|
|
330
|
-
yield emitted
|
|
331
120
|
|
|
332
121
|
|
|
333
122
|
def gate_evidence_batches(
|
|
@@ -366,38 +155,8 @@ def gate_evidence_batches(
|
|
|
366
155
|
return tuple(batches) if batches else ((),)
|
|
367
156
|
|
|
368
157
|
|
|
369
|
-
def _query(text: str) -> bool:
|
|
370
|
-
text = _POLITE.sub("", text.strip())
|
|
371
|
-
control = text.rstrip("。!?!?;;.! ")
|
|
372
|
-
if _READ_ONLY_CONTROL.fullmatch(control):
|
|
373
|
-
return True
|
|
374
|
-
return bool(_QUERY_START.search(text) or _QUERY_WORD.search(text)
|
|
375
|
-
or re.search(r"[??]|(?:吗|么|呢)[。!!\s]*$", text))
|
|
376
158
|
|
|
377
159
|
|
|
378
|
-
def _clauses(text: str) -> Iterable[tuple[str, tuple[str, ...], bool]]:
|
|
379
|
-
"""Separate syntax while retaining headings as context, never as ownership."""
|
|
380
|
-
section: tuple[str, ...] = ()
|
|
381
|
-
in_code = False
|
|
382
|
-
for line in text.splitlines():
|
|
383
|
-
line = line.strip()
|
|
384
|
-
if line.startswith("```") or line.startswith("~~~"):
|
|
385
|
-
in_code = not in_code
|
|
386
|
-
continue
|
|
387
|
-
if not line:
|
|
388
|
-
continue
|
|
389
|
-
if _HEADING.match(line):
|
|
390
|
-
# Every heading resets context, including unregistered names.
|
|
391
|
-
section = (re.sub(r"^(?:#{1,6}|\d+[.)、])\s*", "", line).split(":", 1)[0].split(":", 1)[0],)
|
|
392
|
-
quoted = in_code or line.startswith(">")
|
|
393
|
-
line = _BULLET.sub("", line)
|
|
394
|
-
# Independent assertion/query clauses must not suppress one another.
|
|
395
|
-
# Do not split numeric thousands separators.
|
|
396
|
-
line = re.sub(r"(?<![0-9])[,,]\s*|[,,](?![0-9])\s*", "\n", line)
|
|
397
|
-
for clause in re.split(r"(?<=[。!?!?;;])\s*|\n+|(?<=[A-Za-z0-9]\.)\s+", line):
|
|
398
|
-
clause = clause.strip()
|
|
399
|
-
if clause:
|
|
400
|
-
yield clause, section, quoted
|
|
401
160
|
|
|
402
161
|
|
|
403
162
|
def analyze_turn_evidence(events: Iterable[Mapping[str, Any]]) -> tuple[EvidenceUnit, ...]:
|
|
@@ -790,6 +549,12 @@ def parse_coverage(
|
|
|
790
549
|
validation_detail="invalid_evidence",
|
|
791
550
|
evidence_check="coverage_terminal_witness",
|
|
792
551
|
)
|
|
552
|
+
if set(row) != {"unit_id", "decision", "candidate_ids"}:
|
|
553
|
+
raise ModelOutputError(
|
|
554
|
+
"invalid coverage candidate shape",
|
|
555
|
+
validation_detail="invalid_evidence",
|
|
556
|
+
evidence_check="coverage_shape",
|
|
557
|
+
)
|
|
793
558
|
if not units[uid].can_support or not isinstance(ids, list) or not ids or any(not isinstance(i, str) or i not in candidates for i in ids):
|
|
794
559
|
raise ModelOutputError("invalid coverage candidate", validation_detail="invalid_evidence",
|
|
795
560
|
evidence_check="coverage_candidate")
|
|
@@ -825,6 +590,15 @@ def parse_coverage(
|
|
|
825
590
|
validation_detail="invalid_evidence",
|
|
826
591
|
evidence_check="coverage_terminal_witness",
|
|
827
592
|
)
|
|
593
|
+
expected_fields = ({"unit_id", "decision", "reason", "memory_id"}
|
|
594
|
+
if reason == "already_completed"
|
|
595
|
+
else {"unit_id", "decision", "reason"})
|
|
596
|
+
if set(row) != expected_fields:
|
|
597
|
+
raise ModelOutputError(
|
|
598
|
+
"invalid coverage decision shape",
|
|
599
|
+
validation_detail="invalid_evidence",
|
|
600
|
+
evidence_check="coverage_shape",
|
|
601
|
+
)
|
|
828
602
|
# Normalize only the model's declared reason. This keeps the
|
|
829
603
|
# protocol source-neutral: no local topic or business heuristic
|
|
830
604
|
# decides whether a fragment is retryable.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Bounded batch transport for independent automatic semantic reviews.
|
|
2
|
+
|
|
3
|
+
The semantic contract and per-item summary parser remain the existing single-review
|
|
4
|
+
ones. Batching changes only transport granularity: up to four independent reviews
|
|
5
|
+
share one model call. A malformed associated row is retried as one legacy single
|
|
6
|
+
review; an un-associable batch envelope falls back to legacy singles for the whole
|
|
7
|
+
chunk. No Vault or audit mutation occurs here.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any, Iterable, Mapping
|
|
13
|
+
|
|
14
|
+
from .llm import ModelError
|
|
15
|
+
from .update_review import (
|
|
16
|
+
CREATE_SEMANTIC_REVIEW_SYSTEM,
|
|
17
|
+
UPDATE_SEMANTIC_REVIEW_SYSTEM,
|
|
18
|
+
_MAX_PROMPT_BYTES,
|
|
19
|
+
_complete_json_stage_compat,
|
|
20
|
+
_json_safe,
|
|
21
|
+
_source_projection,
|
|
22
|
+
_target_projection,
|
|
23
|
+
parse_update_review_output,
|
|
24
|
+
review_create,
|
|
25
|
+
review_update,
|
|
26
|
+
)
|
|
27
|
+
from .validation import ModelOutputError, parse_strict_json
|
|
28
|
+
|
|
29
|
+
MAX_REVIEW_BATCH_ITEMS = 4
|
|
30
|
+
|
|
31
|
+
_CREATE_BATCH_SYSTEM = CREATE_SEMANTIC_REVIEW_SYSTEM + """
|
|
32
|
+
|
|
33
|
+
BATCH MODE: the single-review semantic rules above apply independently to each
|
|
34
|
+
input row. This mode changes only the outer transport shape. Return exactly
|
|
35
|
+
{"reviews":[...]} with one row per supplied review_id. Each row must copy its
|
|
36
|
+
review_id exactly and then contain exactly the fields of one legal single-review
|
|
37
|
+
response. Do not transfer facts, decisions, Scope, targets, or evidence between
|
|
38
|
+
rows. Row order is irrelevant; review_id is the association key. Return JSON only.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_UPDATE_BATCH_SYSTEM = UPDATE_SEMANTIC_REVIEW_SYSTEM + """
|
|
42
|
+
|
|
43
|
+
BATCH MODE: the single-review semantic rules above apply independently to each
|
|
44
|
+
input row. This mode changes only the outer transport shape. Return exactly
|
|
45
|
+
{"reviews":[...]} with one row per supplied review_id. Each row must copy its
|
|
46
|
+
review_id exactly and then contain exactly the fields of one legal single-review
|
|
47
|
+
response. Do not transfer facts, decisions, Scope, targets, or evidence between
|
|
48
|
+
rows. Row order is irrelevant; review_id is the association key. Return JSON only.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _validated_specs(items: Iterable[Mapping[str, Any]], *, update: bool) -> list[dict[str, Any]]:
|
|
53
|
+
specs = [dict(item) for item in items]
|
|
54
|
+
if not specs or len(specs) > MAX_REVIEW_BATCH_ITEMS:
|
|
55
|
+
raise ValueError("review batch must contain between one and four items")
|
|
56
|
+
seen: set[str] = set()
|
|
57
|
+
for spec in specs:
|
|
58
|
+
review_id = spec.get("review_id")
|
|
59
|
+
if not isinstance(review_id, str) or not review_id or review_id in seen:
|
|
60
|
+
raise ValueError("review batch requires unique non-empty review_id values")
|
|
61
|
+
seen.add(review_id)
|
|
62
|
+
if not callable(spec.get("parse_summary")):
|
|
63
|
+
raise ValueError("review batch item requires parse_summary")
|
|
64
|
+
if not isinstance(spec.get("proposed_summary"), Mapping):
|
|
65
|
+
raise ValueError("review batch item requires proposed_summary")
|
|
66
|
+
source = spec.get("admitted_source")
|
|
67
|
+
if isinstance(source, (str, bytes)) or source is None:
|
|
68
|
+
raise ValueError("review batch item requires admitted_source")
|
|
69
|
+
if update and spec.get("target") is None:
|
|
70
|
+
raise ValueError("update review batch item requires target")
|
|
71
|
+
return specs
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _batch_payload(specs: list[dict[str, Any]], *, update: bool) -> list[dict[str, Any]]:
|
|
75
|
+
rows: list[dict[str, Any]] = []
|
|
76
|
+
for spec in specs:
|
|
77
|
+
row = {
|
|
78
|
+
"review_id": spec["review_id"],
|
|
79
|
+
"admitted_source": _source_projection(spec["admitted_source"]),
|
|
80
|
+
"proposed_summary": _json_safe(dict(spec["proposed_summary"])),
|
|
81
|
+
}
|
|
82
|
+
if update:
|
|
83
|
+
row["active_target"] = _target_projection(spec["target"])
|
|
84
|
+
rows.append(row)
|
|
85
|
+
return rows
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_batch_prompt(specs: list[dict[str, Any]], *, update: bool) -> str:
|
|
89
|
+
mode = "UPDATE" if update else "CREATE"
|
|
90
|
+
payload = {"reviews": _batch_payload(specs, update=update)}
|
|
91
|
+
prompt = (
|
|
92
|
+
f"{mode}_SEMANTIC_REVIEW_BATCH\n"
|
|
93
|
+
+ json.dumps(_json_safe(payload), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
94
|
+
+ "\n\nReview each row independently and return the strict batch object."
|
|
95
|
+
)
|
|
96
|
+
if len(prompt.encode("utf-8")) > _MAX_PROMPT_BYTES:
|
|
97
|
+
raise ModelOutputError(
|
|
98
|
+
"semantic review batch input exceeds prompt budget",
|
|
99
|
+
validation_detail="other_schema_violation",
|
|
100
|
+
)
|
|
101
|
+
return prompt
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _parse_batch_output(raw: Any, specs: list[dict[str, Any]]) -> dict[str, Any]:
|
|
105
|
+
value = parse_strict_json(raw) if isinstance(raw, str) else raw
|
|
106
|
+
if not isinstance(value, Mapping) or set(value) != {"reviews"} or not isinstance(value["reviews"], list):
|
|
107
|
+
raise ModelOutputError("invalid semantic review batch envelope", validation_detail="root_shape")
|
|
108
|
+
by_id = {spec["review_id"]: spec for spec in specs}
|
|
109
|
+
seen: set[str] = set()
|
|
110
|
+
outcomes: dict[str, dict[str, Any]] = {}
|
|
111
|
+
retry_ids: list[str] = []
|
|
112
|
+
for row in value["reviews"]:
|
|
113
|
+
if not isinstance(row, Mapping):
|
|
114
|
+
raise ModelOutputError("unassociated semantic review batch row", validation_detail="root_shape")
|
|
115
|
+
review_id = row.get("review_id")
|
|
116
|
+
if not isinstance(review_id, str) or review_id not in by_id or review_id in seen:
|
|
117
|
+
raise ModelOutputError("invalid semantic review batch membership", validation_detail="invalid_evidence")
|
|
118
|
+
seen.add(review_id)
|
|
119
|
+
single = {key: item for key, item in row.items() if key != "review_id"}
|
|
120
|
+
try:
|
|
121
|
+
outcome = parse_update_review_output(
|
|
122
|
+
single,
|
|
123
|
+
parse_summary=by_id[review_id]["parse_summary"],
|
|
124
|
+
)
|
|
125
|
+
except ModelOutputError:
|
|
126
|
+
retry_ids.append(review_id)
|
|
127
|
+
else:
|
|
128
|
+
outcomes[review_id] = outcome
|
|
129
|
+
for spec in specs:
|
|
130
|
+
if spec["review_id"] not in seen:
|
|
131
|
+
retry_ids.append(spec["review_id"])
|
|
132
|
+
return {"outcomes": outcomes, "retry_ids": retry_ids}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _single(model_executor: Any, backend: Any, spec: Mapping[str, Any], *, update: bool) -> dict[str, Any]:
|
|
136
|
+
kwargs = {
|
|
137
|
+
"admitted_source": spec["admitted_source"],
|
|
138
|
+
"proposed_summary": spec["proposed_summary"],
|
|
139
|
+
"parse_summary": spec["parse_summary"],
|
|
140
|
+
"diagnostic_context": spec.get("diagnostic_context"),
|
|
141
|
+
}
|
|
142
|
+
if update:
|
|
143
|
+
return review_update(model_executor, backend, target=spec["target"], **kwargs)
|
|
144
|
+
return review_create(model_executor, backend, **kwargs)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _review_batch(
|
|
148
|
+
model_executor: Any,
|
|
149
|
+
backend: Any,
|
|
150
|
+
items: Iterable[Mapping[str, Any]],
|
|
151
|
+
*,
|
|
152
|
+
update: bool,
|
|
153
|
+
) -> list[dict[str, Any]]:
|
|
154
|
+
specs = _validated_specs(items, update=update)
|
|
155
|
+
if getattr(backend, "structured_batch_safe", False) is not True:
|
|
156
|
+
return [_single(model_executor, backend, spec, update=update) for spec in specs]
|
|
157
|
+
if len(specs) == 1:
|
|
158
|
+
return [_single(model_executor, backend, specs[0], update=update)]
|
|
159
|
+
try:
|
|
160
|
+
prompt = _build_batch_prompt(specs, update=update)
|
|
161
|
+
parsed = _complete_json_stage_compat(
|
|
162
|
+
model_executor,
|
|
163
|
+
backend,
|
|
164
|
+
prompt,
|
|
165
|
+
system=_UPDATE_BATCH_SYSTEM if update else _CREATE_BATCH_SYSTEM,
|
|
166
|
+
purpose="summarize",
|
|
167
|
+
parser=lambda raw: _parse_batch_output(raw, specs),
|
|
168
|
+
diagnostic_context=specs[0].get("diagnostic_context"),
|
|
169
|
+
metric_stage="semantic_review",
|
|
170
|
+
)
|
|
171
|
+
if not isinstance(parsed, Mapping):
|
|
172
|
+
raise ModelOutputError("invalid semantic review batch result", validation_detail="root_shape")
|
|
173
|
+
outcomes = parsed.get("outcomes")
|
|
174
|
+
retry_ids = parsed.get("retry_ids")
|
|
175
|
+
if not isinstance(outcomes, Mapping) or not isinstance(retry_ids, list):
|
|
176
|
+
raise ModelOutputError("invalid semantic review batch result", validation_detail="root_shape")
|
|
177
|
+
except (ModelOutputError, TypeError, ValueError):
|
|
178
|
+
return [_single(model_executor, backend, spec, update=update) for spec in specs]
|
|
179
|
+
|
|
180
|
+
resolved = {key: dict(value) for key, value in outcomes.items() if isinstance(key, str) and isinstance(value, Mapping)}
|
|
181
|
+
retry_set = {value for value in retry_ids if isinstance(value, str)}
|
|
182
|
+
for spec in specs:
|
|
183
|
+
review_id = spec["review_id"]
|
|
184
|
+
if review_id in retry_set or review_id not in resolved:
|
|
185
|
+
resolved[review_id] = _single(model_executor, backend, spec, update=update)
|
|
186
|
+
return [resolved[spec["review_id"]] for spec in specs]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def review_create_batch(model_executor: Any, backend: Any, items: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]:
|
|
190
|
+
"""Review up to four independent CREATE requests with per-row fallback."""
|
|
191
|
+
|
|
192
|
+
return _review_batch(model_executor, backend, items, update=False)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def review_update_batch(model_executor: Any, backend: Any, items: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]:
|
|
196
|
+
"""Review up to four independent UPDATE requests with per-row fallback."""
|
|
197
|
+
|
|
198
|
+
return _review_batch(model_executor, backend, items, update=True)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
__all__ = [
|
|
202
|
+
"MAX_REVIEW_BATCH_ITEMS",
|
|
203
|
+
"review_create_batch",
|
|
204
|
+
"review_update_batch",
|
|
205
|
+
]
|