memleaf 0.2.41__tar.gz → 0.2.42__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.41 → memleaf-0.2.42}/CHANGELOG.md +10 -0
- {memleaf-0.2.41/src/memleaf.egg-info → memleaf-0.2.42}/PKG-INFO +2 -2
- {memleaf-0.2.41 → memleaf-0.2.42}/README.en.md +1 -1
- {memleaf-0.2.41 → memleaf-0.2.42}/README.md +1 -1
- {memleaf-0.2.41 → memleaf-0.2.42}/pyproject.toml +1 -1
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/config.py +5 -1
- memleaf-0.2.42/src/memleaf/extraction_budget.py +280 -0
- memleaf-0.2.42/src/memleaf/extraction_capability.py +114 -0
- memleaf-0.2.42/src/memleaf/extraction_work_state.py +337 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/base.py +24 -1
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/openai_compatible.py +11 -1
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/router.py +158 -4
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/memory_commit.py +71 -12
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/process_jobs.py +82 -14
- memleaf-0.2.42/src/memleaf/processing.py +485 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/single_pass_memory_planner.py +6 -5
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/single_pass_plan.py +37 -16
- {memleaf-0.2.41 → memleaf-0.2.42/src/memleaf.egg-info}/PKG-INFO +2 -2
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf.egg-info/SOURCES.txt +14 -1
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/semantic_fixtures.py +18 -6
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_single_pass_memory_planner.py +4 -2
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_single_pass_plan.py +30 -5
- memleaf-0.2.42/tests/test_backlog_bound_v2.py +81 -0
- memleaf-0.2.42/tests/test_explicit_remember_budget_v2.py +91 -0
- memleaf-0.2.42/tests/test_extraction_unified_v2.py +244 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_scope_latency_v038.py +5 -2
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_long_run_hygiene.py +6 -6
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_maintenance_v2.py +14 -12
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_process_jobs.py +51 -0
- memleaf-0.2.42/tests/test_process_jobs_backlog_v2.py +51 -0
- memleaf-0.2.42/tests/test_process_jobs_state_corruption_v2.py +107 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_read_only_deferred_isolation.py +8 -4
- memleaf-0.2.42/tests/test_read_only_zero_call_v2.py +116 -0
- memleaf-0.2.42/tests/test_single_pass_prompt_slim_v2.py +73 -0
- memleaf-0.2.42/tests/test_single_pass_protocol_capability_v2.py +186 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3a_commit.py +25 -8
- memleaf-0.2.42/tests/test_worker_owner_state_v2.py +87 -0
- memleaf-0.2.42/tests/test_worker_restart_budget_v2.py +259 -0
- memleaf-0.2.41/src/memleaf/processing.py +0 -263
- {memleaf-0.2.41 → memleaf-0.2.42}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/LICENSE +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/MANIFEST.in +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/config-migrations.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/core-refactor.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/general-processing.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/examples/README.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/examples/basic_usage.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/examples/live_core_lifecycle_acceptance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/examples/live_processing_acceptance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/install.ps1 +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/install.sh +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/setup.cfg +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/_provider.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/index.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/models.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/service.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/__init__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/hermes_provider_support.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/stage_b1_support.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/stage_b2a_support.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_audit_followup_v041.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_automatic_duplicate_noop_collision.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_activation.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_closeout.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_planning_context.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_scope_provenance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_batch_review.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_batch_review_integration.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_candidate_polarity.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_config_migrations_v028.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_conversation_only.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_cross_turn_dedupe_regressions.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_due_date_grounding.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_due_date_grounding_retry.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_email_actionable_coverage.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_evidence_budget.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_evidence_retention_policy.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_external_source_dates.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_extraction_quality_regressions.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_capacity.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_output_protocol_v041.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_output_references_v041.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_protocol_matrix_v041.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_schema_repair.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_general_evidence_admission.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_general_tool_provenance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_global_todo_acceptance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_global_todo_query_no_write.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_global_todo_retrieval.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_native_registration.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider_01.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider_02.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider_03.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider_04.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_runtime_install.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_transport_evidence.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_windows_subprocess.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_host_events.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_inspection_state_v028.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_install.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_model_invalid_response_metrics_v041.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_model_owned_fields.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_new_scope_source_grounding.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_p3_summary_batch.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_partial_retry_idempotency.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_phase2_model_decisions.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_process_owner_locking.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_processing_contract_v026.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_processing_observability_concurrency.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_prompt_role_slim_v040.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_provider_neutral_thinking_v039.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_pypi_install.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_review_source_context.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_revision_digest.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_shared_memory_refactor.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_source_neutral_todos_v028.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b1.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b1_01.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_01.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_02.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_03.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_04.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_05.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_c1_mcp.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_c3_packaging.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_state_layout_v028.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_summary_date_grounding_integration.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_target_reconciliation.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_target_reconciliation_integration.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_todo_state_recovery.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_update_review.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_update_target_recovery.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v023_scope_correction.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_search_gate_acceptance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_whole_unit_bindings.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_windows_public_mcp_launcher.py +0 -0
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.42 — 2026-09-12
|
|
6
|
+
|
|
7
|
+
- Rework ordinary automatic extraction around the unified `b3-single-pass-v1` planner. The current visible user/assistant turn is the only new-fact evidence; existing local/native memories remain comparison context. One structured model stage decides CREATE / UPDATE / NO_CHANGE / DEFERRED, with deterministic Core validation and at most one bounded format/structure repair.
|
|
8
|
+
- Make the latency-critical `single_pass` stage non-thinking by default and use bounded structured output for supported OpenAI-compatible routes. Fixed safe API routes enforce a six-second primary request cap, an eight-second preparation-plus-model window, a ten-second total turn deadline, and rejection of late results before commit; no unmeasured P50/P95 claim is made.
|
|
9
|
+
- Persist background extraction request/time budgets by process-job and turn identity so worker restarts cannot regain consumed attempts or wall time. Damaged budget/job state, invalid ownership/order, and wall-clock rollback fail closed instead of reopening a fresh extraction budget.
|
|
10
|
+
- Commit automatic processing one complete turn at a time and re-read durable Markdown/journal state before planning the next turn. Frozen-turn recovery preserves already committed work without replaying model calls/history, while one process invocation drains at most four claimed turns and exposes the remaining backlog explicitly.
|
|
11
|
+
- Move compaction/maintenance out of the `process()` / `remember()` extraction critical path, keep explicit no-write instructions as deterministic zero-model-call settlements, and retain structural-only model telemetry without prompt/response bodies or credentials.
|
|
12
|
+
- Separate B3 protocol compatibility from strict transport deadline safety. Host/Python callbacks can use the same B3 semantic protocol without falsely claiming hard cancellation; mixed auto routes remain fail-closed, `single_pass` forbids hidden host-to-API fallback, and explicit `remember()` keeps its existing bounded validator retry semantics.
|
|
13
|
+
- Preserve the product architecture: Markdown under `knowledge/` remains the active source of truth, `history/` remains historical state, permanent memory stays globally shared across agents using the same Vault, and no database, Redis, vector service, resident daemon, or local-model dependency is introduced. The release candidate passed Linux Python 3.11/3.12/3.13, Windows Python 3.11/3.12/3.13, macOS Python 3.11/3.13, native Codex Windows/macOS, wheel/sdist, installed-entry-point, and full unittest CI; real-model quality/latency acceptance remains a separate user-run step.
|
|
14
|
+
|
|
5
15
|
## 0.2.41 — 2026-09-11
|
|
6
16
|
|
|
7
17
|
- 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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.42
|
|
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.42。**
|
|
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.42.**
|
|
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.42。**
|
|
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.42"
|
|
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
|
|
@@ -19,9 +19,13 @@ MAX_REQUEST_TIMEOUT = 240
|
|
|
19
19
|
DEFAULT_MODEL_CONCURRENCY = 3
|
|
20
20
|
MIN_MODEL_CONCURRENCY = 1
|
|
21
21
|
MAX_MODEL_CONCURRENCY = 8
|
|
22
|
-
THINKING_PURPOSES = ("gate", "summarize", "compact")
|
|
22
|
+
THINKING_PURPOSES = ("gate", "summarize", "compact", "single_pass")
|
|
23
23
|
THINKING_MODES = frozenset({"default", "disabled", "low", "high", "max"})
|
|
24
24
|
DEFAULT_THINKING = {purpose: "low" for purpose in THINKING_PURPOSES}
|
|
25
|
+
# Unified extraction is latency-sensitive and keeps deterministic validation in
|
|
26
|
+
# Core. Make the single-pass path non-thinking by default without changing
|
|
27
|
+
# the established policy for maintenance/legacy stages.
|
|
28
|
+
DEFAULT_THINKING["single_pass"] = "disabled"
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
def _normalize_request_timeout(value: Any) -> int | float:
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Latency and outbound-request budget for unified memory extraction.
|
|
2
|
+
|
|
3
|
+
The budget is deliberately attached to one logical single-pass turn instead
|
|
4
|
+
of to an HTTP adapter. That keeps the product invariants (at most two actual
|
|
5
|
+
model requests and a bounded end-to-end turn) independent from provider-
|
|
6
|
+
specific prompt/transport details.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any, Mapping
|
|
14
|
+
|
|
15
|
+
from .llm import ModelError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
MAX_MODEL_REQUESTS = 2
|
|
19
|
+
TARGET_TOTAL_SECONDS = 10.0
|
|
20
|
+
MODEL_TIME_BUDGET_SECONDS = 8.0
|
|
21
|
+
PRIMARY_REQUEST_MAX_SECONDS = 6.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SinglePassBudgetBackend:
|
|
25
|
+
"""Wrap one safe backend with the extraction request/time budget.
|
|
26
|
+
|
|
27
|
+
``single_pass_safe`` routes are already constrained so one ``complete()``
|
|
28
|
+
maps to one provider request (no host->API fallback). The wrapper
|
|
29
|
+
therefore makes the two-call limit a true outbound-request limit for the
|
|
30
|
+
production B3 route, not merely a parser-attempt count.
|
|
31
|
+
|
|
32
|
+
``deadline`` is absolute in the supplied monotonic clock. Supplying it is
|
|
33
|
+
what lets preparation time consume the same turn budget instead of
|
|
34
|
+
starting a fresh eight-second clock only when the HTTP call begins.
|
|
35
|
+
|
|
36
|
+
``reserve_request`` optionally persists the request ordinal before the
|
|
37
|
+
outbound call. Background workers use it so a process restart cannot
|
|
38
|
+
reopen attempts already consumed by the same durable work item.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
single_pass_safe = True
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
backend: Any,
|
|
46
|
+
*,
|
|
47
|
+
clock: Any = time.monotonic,
|
|
48
|
+
deadline: float | None = None,
|
|
49
|
+
reserve_request: Callable[[], int | None] | None = None,
|
|
50
|
+
):
|
|
51
|
+
if not hasattr(backend, "complete"):
|
|
52
|
+
raise TypeError("single-pass backend must expose complete()")
|
|
53
|
+
if reserve_request is not None and not callable(reserve_request):
|
|
54
|
+
raise TypeError("reserve_request must be callable")
|
|
55
|
+
self._backend = backend
|
|
56
|
+
self._clock = clock
|
|
57
|
+
self._started = float(clock())
|
|
58
|
+
self._deadline = (
|
|
59
|
+
self._started + MODEL_TIME_BUDGET_SECONDS
|
|
60
|
+
if deadline is None
|
|
61
|
+
else float(deadline)
|
|
62
|
+
)
|
|
63
|
+
self._requests = 0
|
|
64
|
+
self._reserve_request = reserve_request
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def provider(self) -> str:
|
|
68
|
+
return str(getattr(self._backend, "provider", "unknown"))
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def model(self) -> str:
|
|
72
|
+
return str(getattr(self._backend, "model", "unknown"))
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def parallel_safe(self) -> bool:
|
|
76
|
+
return getattr(self._backend, "parallel_safe", False) is True
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def structured_batch_safe(self) -> bool:
|
|
80
|
+
return getattr(self._backend, "structured_batch_safe", False) is True
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def request_count(self) -> int:
|
|
84
|
+
return self._requests
|
|
85
|
+
|
|
86
|
+
def _remaining(self) -> float:
|
|
87
|
+
return self._deadline - float(self._clock())
|
|
88
|
+
|
|
89
|
+
def _request_ordinal(self, *, purpose: str) -> int:
|
|
90
|
+
if self._requests >= MAX_MODEL_REQUESTS:
|
|
91
|
+
raise ModelError(
|
|
92
|
+
"single-pass extraction budget exhausted",
|
|
93
|
+
code="model_timeout",
|
|
94
|
+
stage=purpose or "single_pass",
|
|
95
|
+
)
|
|
96
|
+
if self._reserve_request is None:
|
|
97
|
+
return self._requests + 1
|
|
98
|
+
try:
|
|
99
|
+
ordinal = self._reserve_request()
|
|
100
|
+
except Exception as error:
|
|
101
|
+
raise ModelError(
|
|
102
|
+
"single-pass extraction request budget cannot be reserved",
|
|
103
|
+
code="model_timeout",
|
|
104
|
+
stage=purpose or "single_pass",
|
|
105
|
+
) from error
|
|
106
|
+
if type(ordinal) is not int or not 1 <= ordinal <= MAX_MODEL_REQUESTS:
|
|
107
|
+
raise ModelError(
|
|
108
|
+
"single-pass extraction budget exhausted",
|
|
109
|
+
code="model_timeout",
|
|
110
|
+
stage=purpose or "single_pass",
|
|
111
|
+
)
|
|
112
|
+
return ordinal
|
|
113
|
+
|
|
114
|
+
def complete(
|
|
115
|
+
self,
|
|
116
|
+
prompt: str,
|
|
117
|
+
*,
|
|
118
|
+
system: str = "",
|
|
119
|
+
purpose: str = "",
|
|
120
|
+
temperature: float = 0.0,
|
|
121
|
+
) -> str:
|
|
122
|
+
remaining = self._remaining()
|
|
123
|
+
if remaining <= 0:
|
|
124
|
+
raise ModelError(
|
|
125
|
+
"single-pass extraction budget exhausted",
|
|
126
|
+
code="model_timeout",
|
|
127
|
+
stage=purpose or "single_pass",
|
|
128
|
+
)
|
|
129
|
+
ordinal = self._request_ordinal(purpose=purpose)
|
|
130
|
+
cap = min(
|
|
131
|
+
PRIMARY_REQUEST_MAX_SECONDS if ordinal == 1 else remaining,
|
|
132
|
+
remaining,
|
|
133
|
+
)
|
|
134
|
+
if cap <= 0:
|
|
135
|
+
raise ModelError(
|
|
136
|
+
"single-pass extraction budget exhausted",
|
|
137
|
+
code="model_timeout",
|
|
138
|
+
stage=purpose or "single_pass",
|
|
139
|
+
)
|
|
140
|
+
# Count locally only after the durable reservation succeeded. A kill
|
|
141
|
+
# after this point still leaves the persistent ordinal consumed.
|
|
142
|
+
self._requests += 1
|
|
143
|
+
|
|
144
|
+
set_timeout = getattr(self._backend, "set_call_timeout", None)
|
|
145
|
+
clear_timeout = getattr(self._backend, "clear_call_timeout", None)
|
|
146
|
+
if callable(set_timeout):
|
|
147
|
+
set_timeout(cap)
|
|
148
|
+
try:
|
|
149
|
+
value = self._backend.complete(
|
|
150
|
+
prompt,
|
|
151
|
+
system=system,
|
|
152
|
+
purpose=purpose,
|
|
153
|
+
temperature=temperature,
|
|
154
|
+
)
|
|
155
|
+
finally:
|
|
156
|
+
if callable(clear_timeout):
|
|
157
|
+
try:
|
|
158
|
+
clear_timeout()
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
# A callback/custom transport may ignore the timeout hook. Such a late
|
|
163
|
+
# result must never become writable merely because it eventually returned.
|
|
164
|
+
if self._remaining() < 0:
|
|
165
|
+
raise ModelError(
|
|
166
|
+
"single-pass extraction result arrived after deadline",
|
|
167
|
+
code="model_timeout",
|
|
168
|
+
stage=purpose or "single_pass",
|
|
169
|
+
)
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
def consume_call_metrics(self) -> dict[str, Any]:
|
|
173
|
+
consume = getattr(self._backend, "consume_call_metrics", None)
|
|
174
|
+
if not callable(consume):
|
|
175
|
+
return {}
|
|
176
|
+
try:
|
|
177
|
+
value = consume()
|
|
178
|
+
except Exception:
|
|
179
|
+
return {}
|
|
180
|
+
return dict(value) if isinstance(value, Mapping) else {}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ExtractionWorkBudget:
|
|
184
|
+
"""One monotonic budget beginning before preparation for a visible turn.
|
|
185
|
+
|
|
186
|
+
The first eight seconds are available to preparation plus model work. The
|
|
187
|
+
remaining two seconds are reserved for deterministic validation/commit.
|
|
188
|
+
If the ten-second total deadline has already elapsed, the turn is not
|
|
189
|
+
allowed to enter the mutation boundary.
|
|
190
|
+
|
|
191
|
+
``elapsed_seconds`` restores wall time already consumed by the same durable
|
|
192
|
+
background work item before a worker restart. The persisted ledger uses a
|
|
193
|
+
wall clock only to derive that cross-process elapsed interval; after this
|
|
194
|
+
object is constructed, every new deadline check uses the supplied monotonic
|
|
195
|
+
clock in the current process.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
*,
|
|
201
|
+
clock: Any = time.monotonic,
|
|
202
|
+
elapsed_seconds: float = 0.0,
|
|
203
|
+
):
|
|
204
|
+
if (
|
|
205
|
+
isinstance(elapsed_seconds, bool)
|
|
206
|
+
or not isinstance(elapsed_seconds, (int, float))
|
|
207
|
+
or not math.isfinite(float(elapsed_seconds))
|
|
208
|
+
or float(elapsed_seconds) < 0
|
|
209
|
+
):
|
|
210
|
+
raise ValueError("elapsed_seconds must be a finite non-negative number")
|
|
211
|
+
self._clock = clock
|
|
212
|
+
current = float(clock())
|
|
213
|
+
elapsed = float(elapsed_seconds)
|
|
214
|
+
self._started = current - elapsed
|
|
215
|
+
self._model_deadline = current + (MODEL_TIME_BUDGET_SECONDS - elapsed)
|
|
216
|
+
self._total_deadline = current + (TARGET_TOTAL_SECONDS - elapsed)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def started(self) -> float:
|
|
220
|
+
return self._started
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def model_deadline(self) -> float:
|
|
224
|
+
return self._model_deadline
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def total_deadline(self) -> float:
|
|
228
|
+
return self._total_deadline
|
|
229
|
+
|
|
230
|
+
def remaining_total(self) -> float:
|
|
231
|
+
return self._total_deadline - float(self._clock())
|
|
232
|
+
|
|
233
|
+
def wrap_backend(
|
|
234
|
+
self,
|
|
235
|
+
backend: Any,
|
|
236
|
+
*,
|
|
237
|
+
reserve_request: Callable[[], int | None] | None = None,
|
|
238
|
+
) -> SinglePassBudgetBackend:
|
|
239
|
+
return budget_single_pass_backend(
|
|
240
|
+
backend,
|
|
241
|
+
clock=self._clock,
|
|
242
|
+
deadline=self._model_deadline,
|
|
243
|
+
reserve_request=reserve_request,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def ensure_before_commit(self) -> None:
|
|
247
|
+
if self.remaining_total() <= 0:
|
|
248
|
+
raise ModelError(
|
|
249
|
+
"single-pass extraction exceeded total deadline before commit",
|
|
250
|
+
code="model_timeout",
|
|
251
|
+
stage="single_pass",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def budget_single_pass_backend(
|
|
256
|
+
backend: Any,
|
|
257
|
+
*,
|
|
258
|
+
clock: Any = time.monotonic,
|
|
259
|
+
deadline: float | None = None,
|
|
260
|
+
reserve_request: Callable[[], int | None] | None = None,
|
|
261
|
+
) -> SinglePassBudgetBackend:
|
|
262
|
+
if isinstance(backend, SinglePassBudgetBackend):
|
|
263
|
+
return backend
|
|
264
|
+
return SinglePassBudgetBackend(
|
|
265
|
+
backend,
|
|
266
|
+
clock=clock,
|
|
267
|
+
deadline=deadline,
|
|
268
|
+
reserve_request=reserve_request,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
__all__ = [
|
|
273
|
+
"MAX_MODEL_REQUESTS",
|
|
274
|
+
"MODEL_TIME_BUDGET_SECONDS",
|
|
275
|
+
"PRIMARY_REQUEST_MAX_SECONDS",
|
|
276
|
+
"TARGET_TOTAL_SECONDS",
|
|
277
|
+
"ExtractionWorkBudget",
|
|
278
|
+
"SinglePassBudgetBackend",
|
|
279
|
+
"budget_single_pass_backend",
|
|
280
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Capability checks for unified automatic extraction.
|
|
2
|
+
|
|
3
|
+
Protocol compatibility and hard latency safety are intentionally separate.
|
|
4
|
+
A backend may understand the B3 single-pass contract without being able to
|
|
5
|
+
honor transport-level cancellation/deadlines. Automatic extraction can use
|
|
6
|
+
one semantic protocol in both cases, while Processor applies the strict
|
|
7
|
+
8/10-second budget only to ``single_pass_safe`` transports.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .llm import CallableBackend, ModelRouter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _callable_protocol_override(backend: CallableBackend) -> bool | None:
|
|
17
|
+
"""Return a caller-declared protocol override for callback adapters.
|
|
18
|
+
|
|
19
|
+
Raw callbacks default to the unified B3 contract. A callback may set
|
|
20
|
+
``single_pass_protocol = False`` when it intentionally implements the
|
|
21
|
+
legacy staged test/compatibility protocol. Product code never needs this
|
|
22
|
+
for ordinary host callbacks; the escape hatch prevents capability
|
|
23
|
+
inference from rewriting explicitly versioned compatibility fixtures.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
callback = getattr(backend, "callback", None)
|
|
27
|
+
value = getattr(callback, "single_pass_protocol", None)
|
|
28
|
+
return value if isinstance(value, bool) else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _direct_protocol_capable(backend: Any) -> bool:
|
|
32
|
+
"""Return whether one concrete backend can speak the B3 output contract."""
|
|
33
|
+
|
|
34
|
+
if backend is None:
|
|
35
|
+
return False
|
|
36
|
+
if getattr(backend, "single_pass_safe", False) is True:
|
|
37
|
+
return True
|
|
38
|
+
declared = getattr(backend, "single_pass_protocol", None)
|
|
39
|
+
if isinstance(declared, bool):
|
|
40
|
+
return declared
|
|
41
|
+
if isinstance(backend, CallableBackend):
|
|
42
|
+
override = _callable_protocol_override(backend)
|
|
43
|
+
if override is not None:
|
|
44
|
+
return override
|
|
45
|
+
# Raw Python callbacks are adapted by ModelExecutor into
|
|
46
|
+
# CallableBackend. They can consume memleaf's B3 prompt, but are not
|
|
47
|
+
# strict-deadline safe because caller-owned code may ignore
|
|
48
|
+
# timeout/cancellation entirely.
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def supports_single_pass_protocol(backend: Any) -> bool:
|
|
54
|
+
"""Return B3 protocol capability without claiming strict transport SLA."""
|
|
55
|
+
|
|
56
|
+
if _direct_protocol_capable(backend):
|
|
57
|
+
return True
|
|
58
|
+
if not isinstance(backend, ModelRouter):
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
if backend.mode == "api":
|
|
62
|
+
return _direct_protocol_capable(backend.api)
|
|
63
|
+
if backend.mode == "host":
|
|
64
|
+
return _direct_protocol_capable(backend.host)
|
|
65
|
+
|
|
66
|
+
# Auto mode can expose both routes over its lifetime. Even though the
|
|
67
|
+
# single-pass call itself is pinned against hidden host->API fallback, B3
|
|
68
|
+
# capability remains fail-closed unless every configured reachable route
|
|
69
|
+
# speaks the same protocol. This prevents a later routing-policy change or
|
|
70
|
+
# route selection from silently changing the output contract.
|
|
71
|
+
if backend.host is not None:
|
|
72
|
+
if not _direct_protocol_capable(backend.host):
|
|
73
|
+
return False
|
|
74
|
+
return backend.api is None or _direct_protocol_capable(backend.api)
|
|
75
|
+
return _direct_protocol_capable(backend.api)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _direct_requires_inline_system(backend: Any) -> bool:
|
|
79
|
+
"""Return whether a concrete callback may receive only the prompt value.
|
|
80
|
+
|
|
81
|
+
CallableBackend intentionally supports legacy ``callback(prompt)``
|
|
82
|
+
signatures. In that mode its ``system`` argument cannot reach the caller,
|
|
83
|
+
so B3 must inline the system contract into the prompt rather than silently
|
|
84
|
+
dropping the planner rules. This says nothing about latency safety.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
return isinstance(backend, CallableBackend)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def requires_inline_single_pass_system(backend: Any) -> bool:
|
|
91
|
+
"""Keep B3 instructions visible across prompt-only callback routes."""
|
|
92
|
+
|
|
93
|
+
if _direct_requires_inline_system(backend):
|
|
94
|
+
return True
|
|
95
|
+
if not isinstance(backend, ModelRouter):
|
|
96
|
+
return False
|
|
97
|
+
if backend.mode == "api":
|
|
98
|
+
return _direct_requires_inline_system(backend.api)
|
|
99
|
+
if backend.mode == "host":
|
|
100
|
+
return _direct_requires_inline_system(backend.host)
|
|
101
|
+
|
|
102
|
+
# Keep the prompt self-contained whenever either configured auto route is
|
|
103
|
+
# a prompt-only callback. Capability gating above ensures both routes speak
|
|
104
|
+
# B3 before automatic extraction chooses this protocol.
|
|
105
|
+
return (
|
|
106
|
+
_direct_requires_inline_system(backend.host)
|
|
107
|
+
or _direct_requires_inline_system(backend.api)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
__all__ = [
|
|
112
|
+
"requires_inline_single_pass_system",
|
|
113
|
+
"supports_single_pass_protocol",
|
|
114
|
+
]
|