memleaf 0.2.45__tar.gz → 0.2.47__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.45 → memleaf-0.2.47}/CHANGELOG.md +17 -0
- {memleaf-0.2.45/src/memleaf.egg-info → memleaf-0.2.47}/PKG-INFO +2 -2
- {memleaf-0.2.45 → memleaf-0.2.47}/README.en.md +1 -1
- {memleaf-0.2.45 → memleaf-0.2.47}/README.md +1 -1
- {memleaf-0.2.45 → memleaf-0.2.47}/pyproject.toml +1 -1
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/cli.py +21 -2
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/installer.py +77 -12
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/mcp_server.py +16 -4
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/model_capabilities.py +10 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/model_execution.py +3 -1
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/process_journal.py +58 -2
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/prompts.py +24 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/retrieval.py +22 -2
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/validation.py +19 -2
- {memleaf-0.2.45 → memleaf-0.2.47/src/memleaf.egg-info}/PKG-INFO +2 -2
- {memleaf-0.2.45 → memleaf-0.2.47}/LICENSE +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/MANIFEST.in +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/config-migrations.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/core-refactor.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/general-processing.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/examples/README.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/examples/basic_usage.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/install.ps1 +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/install.sh +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/setup.cfg +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/config.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/hermes_provider/_provider.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/index.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/models.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/service.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/single_pass_memory_planner.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/single_pass_plan.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.45 → memleaf-0.2.47}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.47 — 2026-09-12
|
|
6
|
+
|
|
7
|
+
- Fix two satisfiability deadlocks in the public candidate filter that made every compound query carrying a punctuation-rich identifier return no_match. Word splitting turns `vX.Y.Z` into `vX` / `Y` / `Z`; those one-character fragments were collected as *required* ASCII components while the matching rule refused to treat anything shorter than three characters as a strong match, so the requirement could never be met. A fixed three-character bar had the same effect on a two-character CJK component whenever an ASCII component was present. A component that cannot reach its own bar no longer creates a requirement, and a CJK component's bar never exceeds its own length. Identifier strictness is unchanged: a short fragment still cannot stand in for a longer topic.
|
|
8
|
+
- State the `sources` entry shape in the summarize contract instead of leaving the model to infer it, and generate that sentence from the validator's own field list so the instruction and the enforced shape cannot drift apart. Production `remember` calls writing a todo failed four times with `schema_violation` / `source_shape`: the prompt required `sources` and mentioned event keys but never described the object the validator accepts. A bare admitted event key is now additionally normalized to `{"event_key": ...}` losslessly, and `source_shape` gained a targeted repair instruction so the bounded retry corrects the shape instead of re-deriving the whole summary.
|
|
9
|
+
- Make failed automatic extraction visible. `_state/processed.json` already recorded which sessions failed, at which stage and with which code, but nothing surfaced it: a Vault whose every turn failed extraction still looked idle from outside. `process_status` now accepts an omitted `job_id` and reports Vault-level extraction health — failed and running sessions, failure code and stage, and retryable turn counts. The report is strictly read-only and opens the Vault without creating it.
|
|
10
|
+
- Report an incompatible model route instead of a misleading `ready`. The installer answered `processing_status: ready` for any configured route, including one whose adapter cannot speak the single-pass protocol. `install --host hermes` now distinguishes `ready`, `model_route_required` and `model_route_incompatible`. Capability is resolved through the same `ModelRouter` the extraction path uses, so the adapter decides and no rule is restated; no request is sent and no service is probed.
|
|
11
|
+
- Document that the Claude and Gemini adapters implement no verified structured-output boundary, so the single-pass protocol stays fail-closed for them by design: a Vault routed through `llm.protocol: claude` cannot run automatic extraction, while capture, retrieval and explicit `remember`/`forget` are unaffected. Automatic extraction therefore requires an OpenAI-compatible route. No tool was added, removed or renamed; the public MCP surface remains the same 13 tools.
|
|
12
|
+
|
|
13
|
+
Verification for this release used synthetic memories, temporary Vaults, synthetic routes and no model call: 51 focused tests cover the retrieval deadlocks and the strictness they preserve, the `sources` contract and its consistency with the validator, the targeted repair selection, the route capability decision across Claude, OpenAI/DeepSeek and unknown OpenAI-compatible routes, and the read-only processing-health report. Real-model first-pass success rate, repair success rate, reasoning reduction and latency improvement remain unmeasured, and the two reported production sessions were not replayed.
|
|
14
|
+
|
|
15
|
+
## 0.2.46 — 2026-09-12
|
|
16
|
+
|
|
17
|
+
- Restore the host protocol literals that v0.2.45 dropped from the API-route protocol table. `anthropic_messages`, `chat_completions`, `chat`, `google` and `generate_content` resolve again. v0.2.45 replaced the previous tolerant mapping with an exact table that omitted every canonical Hermes `api_mode` value, so a Hermes custom provider that used to resolve to a protocol began resolving to none and was reported as `missing provider, base URL, protocol, or model`. Substring and model-name matching stay removed: only exact host-contract literals are accepted, and an unknown protocol still fails closed.
|
|
18
|
+
- Stop `memleaf install --host hermes` from aborting when model-route discovery finds nothing. Installation now continues and reports `processing_status: model_route_required` with an explicit user action, matching the Codex installer policy. Previously the install returned `stage: model_route` before touching the provider, which left the core package upgraded while the Hermes provider stayed on its previous version. The provider and MCP surface do not need a model route for capture or retrieval, so a missing route is a deferred configuration step rather than an installation failure.
|
|
19
|
+
- Add `install --no-model-discovery` so an upgrade can skip host model discovery while still preserving an existing memleaf route, matching the `init` flag that already existed. Discovery remains enabled by default, so an installation that already succeeds is unaffected.
|
|
20
|
+
- Verification for this release used synthetic Hermes-shaped routes, a temporary Vault and mocked host calls only: 18 focused tests cover the restored protocol literals, rejection of still-unknown protocols, discovery of a Hermes-shaped custom provider, the degraded-install decision, the new CLI flag, and an end-to-end assertion that `install_hermes` walks past a failed model route. No real provider call was made and no production Vault, Hermes configuration or session was read or written. A real Windows Hermes install and a real macOS Hermes install remain unverified by this release, and the manual model-route configuration step is still required on a host whose credentials the host CLI only reports masked.
|
|
21
|
+
|
|
5
22
|
## 0.2.45 — 2026-09-12
|
|
6
23
|
|
|
7
24
|
- Repair the `b3-single-pass-v1` automatic extraction contract so model output is constrained by the same CREATE / UPDATE / NO_CHANGE / DEFERRED fields, evidence shapes, no-memory/defer reasons, target rules and lookup-completeness checks that Core validates. Pure ASCII decision-case differences are normalized locally; unknown decisions and missing semantic fields still fail closed.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.47
|
|
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.47。**
|
|
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.47.**
|
|
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.47。**
|
|
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.47"
|
|
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
|
|
@@ -77,6 +77,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
77
77
|
"this installation (current), or retain a version-matched configured runtime (existing)"
|
|
78
78
|
),
|
|
79
79
|
)
|
|
80
|
+
install.add_argument(
|
|
81
|
+
"--no-model-discovery",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help=(
|
|
84
|
+
"Hermes only: skip host model discovery; an existing memleaf route is "
|
|
85
|
+
"still preserved and the install continues either way"
|
|
86
|
+
),
|
|
87
|
+
)
|
|
80
88
|
install.add_argument("--json", action="store_true", help="emit one JSON result")
|
|
81
89
|
audit = commands.add_parser("audit", help="inspect an existing Vault without changing it")
|
|
82
90
|
audit.add_argument("--vault", type=Path, default=None)
|
|
@@ -232,6 +240,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
232
240
|
install_kwargs = {"vault_path": args.vault}
|
|
233
241
|
if args.mcp_runtime != "auto":
|
|
234
242
|
install_kwargs["mcp_runtime"] = args.mcp_runtime
|
|
243
|
+
if args.no_model_discovery:
|
|
244
|
+
install_kwargs["skip_model_discovery"] = True
|
|
235
245
|
output = install_hermes(**install_kwargs)
|
|
236
246
|
elif args.command in {"audit", "process"}:
|
|
237
247
|
from .inspection import audit_vault, existing_root, preview_process
|
|
@@ -302,8 +312,17 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
302
312
|
print("Restart Hermes to use memleaf.")
|
|
303
313
|
elif output.get("user_action_required"):
|
|
304
314
|
print(f"Codex action required: {output.get('user_action')}")
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
processing = output.get("processing_status")
|
|
316
|
+
if processing == "model_route_required":
|
|
317
|
+
print(
|
|
318
|
+
"Automatic memory extraction needs a memleaf model route before it can run. "
|
|
319
|
+
"Set llm.provider/family/protocol/base_url/model/key in the Vault config.yaml, "
|
|
320
|
+
"or rerun `memleaf init` from an interactive terminal."
|
|
321
|
+
)
|
|
322
|
+
elif processing == "model_route_incompatible":
|
|
323
|
+
print("Automatic memory extraction cannot run on this route.")
|
|
324
|
+
if output.get("user_action"):
|
|
325
|
+
print(f" {output['user_action']}")
|
|
307
326
|
else:
|
|
308
327
|
_print_install_failure(output, host=args.host)
|
|
309
328
|
return 0 if output.get("status") in {"configured", "already_configured"} else 2
|
|
@@ -15,7 +15,7 @@ import subprocess
|
|
|
15
15
|
import sys
|
|
16
16
|
import sysconfig
|
|
17
17
|
import tempfile
|
|
18
|
-
from typing import Any
|
|
18
|
+
from typing import Any, Mapping
|
|
19
19
|
|
|
20
20
|
from . import __version__
|
|
21
21
|
from .adapters.base import (
|
|
@@ -754,10 +754,72 @@ def _failure_result(
|
|
|
754
754
|
return result
|
|
755
755
|
|
|
756
756
|
|
|
757
|
+
_MODEL_ROUTE_REQUIRED_ACTION = (
|
|
758
|
+
"Configure an independent memleaf Model Route for this Vault before relying on "
|
|
759
|
+
"automatic memory extraction. Host model/provider settings are intentionally "
|
|
760
|
+
"not used or modified."
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
_MODEL_ROUTE_INCOMPATIBLE_ACTION = (
|
|
764
|
+
"This Vault's model route cannot run automatic memory extraction. The Claude and "
|
|
765
|
+
"Gemini adapters implement no verified structured-output boundary, so the "
|
|
766
|
+
"single-pass protocol stays fail-closed for them; v0.2.x automatic extraction "
|
|
767
|
+
"requires an OpenAI-compatible route (set llm.protocol to openai). Capture, "
|
|
768
|
+
"retrieval, and explicit remember/forget keep working unchanged."
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _route_can_extract(vault: Any) -> bool:
|
|
773
|
+
"""Ask the production router whether this Vault's route can speak B3.
|
|
774
|
+
|
|
775
|
+
This deliberately reuses the same decision the extraction path makes instead
|
|
776
|
+
of restating the capability rules: the installer must never advertise
|
|
777
|
+
``ready`` for a route whose adapter leaves ``single_pass_protocol`` False.
|
|
778
|
+
No request is sent and no service is probed.
|
|
779
|
+
"""
|
|
780
|
+
|
|
781
|
+
try:
|
|
782
|
+
from .extraction_capability import supports_single_pass_protocol
|
|
783
|
+
from .llm import ModelRouter
|
|
784
|
+
|
|
785
|
+
router = ModelRouter.from_config(vault.config())
|
|
786
|
+
except Exception:
|
|
787
|
+
return False
|
|
788
|
+
try:
|
|
789
|
+
return bool(supports_single_pass_protocol(router))
|
|
790
|
+
except Exception:
|
|
791
|
+
return False
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _model_route_outcome(
|
|
795
|
+
model: Any, *, can_extract: bool | None = None
|
|
796
|
+
) -> tuple[bool, str, str | None]:
|
|
797
|
+
"""Decide how one host installation reports its model route.
|
|
798
|
+
|
|
799
|
+
A missing model route must never block installing the host integration.
|
|
800
|
+
The provider/MCP surface still serves capture and retrieval without one,
|
|
801
|
+
so installation continues and reports ``model_route_required`` until a
|
|
802
|
+
route is configured. This is the documented Codex policy, and applying it
|
|
803
|
+
to Hermes keeps the two hosts consistent instead of aborting the install
|
|
804
|
+
and leaving core and provider versions out of step.
|
|
805
|
+
|
|
806
|
+
``can_extract`` carries the resolved capability of a *configured* route. A
|
|
807
|
+
route can be present and still be unable to run automatic extraction, so
|
|
808
|
+
reporting plain ``ready`` for it would be a false promise.
|
|
809
|
+
"""
|
|
810
|
+
|
|
811
|
+
if not (isinstance(model, Mapping) and model.get("status") in {"configured", "already_configured"}):
|
|
812
|
+
return False, "model_route_required", _MODEL_ROUTE_REQUIRED_ACTION
|
|
813
|
+
if can_extract is False:
|
|
814
|
+
return False, "model_route_incompatible", _MODEL_ROUTE_INCOMPATIBLE_ACTION
|
|
815
|
+
return True, "ready", None
|
|
816
|
+
|
|
817
|
+
|
|
757
818
|
def install_hermes(
|
|
758
819
|
*,
|
|
759
820
|
vault_path: Path | None = None,
|
|
760
821
|
mcp_runtime: str = "auto",
|
|
822
|
+
skip_model_discovery: bool = False,
|
|
761
823
|
) -> dict[str, Any]:
|
|
762
824
|
"""Install memleaf for Hermes with runtime preflight and host rollback.
|
|
763
825
|
|
|
@@ -870,18 +932,18 @@ def install_hermes(
|
|
|
870
932
|
home=home,
|
|
871
933
|
dry_run=False,
|
|
872
934
|
non_interactive=not sys.stdin.isatty(),
|
|
873
|
-
skip_discovery=
|
|
935
|
+
skip_discovery=skip_model_discovery,
|
|
874
936
|
)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
937
|
+
# A missing route degrades the installation instead of aborting it: the
|
|
938
|
+
# Hermes provider and MCP surface do not need a model route, and stopping
|
|
939
|
+
# here used to leave core and provider versions out of step. A *present*
|
|
940
|
+
# route that cannot speak B3 is reported separately, because advertising
|
|
941
|
+
# ``ready`` for it would promise extraction that can never run.
|
|
942
|
+
model_ready, processing_status, model_action = _model_route_outcome(
|
|
943
|
+
model, can_extract=_route_can_extract(vault)
|
|
944
|
+
)
|
|
945
|
+
# ``model_ready`` is reported through ``processing_status`` below; the host
|
|
946
|
+
# integration itself is configured either way.
|
|
885
947
|
|
|
886
948
|
provider_target = hermes_home / "plugins" / "memleaf"
|
|
887
949
|
provider_config = hermes_home / "memleaf.json"
|
|
@@ -1079,6 +1141,9 @@ def install_hermes(
|
|
|
1079
1141
|
"model": model,
|
|
1080
1142
|
"native_sources": native_registration,
|
|
1081
1143
|
"capture": capture_policy_status(vault.config()),
|
|
1144
|
+
"processing_status": processing_status,
|
|
1145
|
+
"user_action_required": bool(model_action),
|
|
1146
|
+
"user_action": model_action,
|
|
1082
1147
|
}
|
|
1083
1148
|
|
|
1084
1149
|
|
|
@@ -286,8 +286,14 @@ _TOOLS: tuple[dict[str, Any], ...] = (
|
|
|
286
286
|
},
|
|
287
287
|
{
|
|
288
288
|
"name": "process_status",
|
|
289
|
-
"description":
|
|
290
|
-
|
|
289
|
+
"description": (
|
|
290
|
+
"Read the status of an accepted background process job, or, when job_id is "
|
|
291
|
+
"omitted, the Vault's automatic-extraction health: failed and running "
|
|
292
|
+
"sessions, the failure code and stage, and whether the work is still "
|
|
293
|
+
"retryable. Failed extraction is never silent; check this when expected "
|
|
294
|
+
"memories do not appear."
|
|
295
|
+
),
|
|
296
|
+
"inputSchema": _object_schema({"job_id": {"type": "string"}}),
|
|
291
297
|
},
|
|
292
298
|
{
|
|
293
299
|
"name": "remember",
|
|
@@ -930,9 +936,15 @@ def _invoke_tool(
|
|
|
930
936
|
else:
|
|
931
937
|
value = service.process(**args)
|
|
932
938
|
elif name == "process_status":
|
|
933
|
-
|
|
939
|
+
job_id = args.get("job_id")
|
|
940
|
+
if isinstance(job_id, str) and job_id:
|
|
941
|
+
from .process_jobs import status
|
|
942
|
+
|
|
943
|
+
value = status(service.vault.root, job_id=job_id)
|
|
944
|
+
else:
|
|
945
|
+
from .process_journal import processing_health
|
|
934
946
|
|
|
935
|
-
|
|
947
|
+
value = processing_health(service.vault.root)
|
|
936
948
|
elif name == "remember":
|
|
937
949
|
value = service.remember(**args)
|
|
938
950
|
elif name == "forget_memory":
|
|
@@ -36,13 +36,23 @@ _LEGACY_PROVIDER_PROTOCOL = {
|
|
|
36
36
|
"google": "gemini",
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
# Exact protocol identities, including the canonical ``api_mode`` literals a
|
|
40
|
+
# Hermes configuration actually stores (``chat_completions`` and
|
|
41
|
+
# ``anthropic_messages``). These are host-contract literals, not fuzzy
|
|
42
|
+
# guesses: substring and model-name matching stay removed so a custom alias can
|
|
43
|
+
# never authorize a protocol it does not name.
|
|
39
44
|
_PROTOCOL_ALIASES = {
|
|
40
45
|
"openai": "openai",
|
|
41
46
|
"openai-compatible": "openai",
|
|
42
47
|
"openai_compatible": "openai",
|
|
48
|
+
"chat_completions": "openai",
|
|
49
|
+
"chat": "openai",
|
|
43
50
|
"claude": "claude",
|
|
44
51
|
"anthropic": "claude",
|
|
52
|
+
"anthropic_messages": "claude",
|
|
45
53
|
"gemini": "gemini",
|
|
54
|
+
"google": "gemini",
|
|
55
|
+
"generate_content": "gemini",
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
|
|
@@ -9,7 +9,7 @@ from typing import Any, Callable, Mapping, Optional
|
|
|
9
9
|
from .config import DEFAULT_MODEL_CONCURRENCY, MAX_MODEL_CONCURRENCY, MIN_MODEL_CONCURRENCY
|
|
10
10
|
from .llm import MODEL_VALIDATION_REASONS, CallableBackend, ModelError, ModelUnavailable, ModelRouter
|
|
11
11
|
from .models import utc_now
|
|
12
|
-
from .prompts import COVERAGE_ALREADY_COMPLETED_CORRECTION, COVERAGE_CANDIDATE_CORRECTION, COVERAGE_CORRECTION, COVERAGE_SHAPE_CORRECTION, DUPLICATE_TARGET_CORRECTION, EVIDENCE_EVENT_MAPPING_CORRECTION, EVIDENCE_SPAN_CORRECTION, GATE_STRUCTURE_REPAIR_SYSTEM, GATE_TYPE_CORRECTION, JSON_CORRECTION, MIXED_FUTURE_USE_CORRECTION, MIXED_PROJECT_SCOPES_CORRECTION, RELATIVE_TIME_CORRECTION, SCOPE_GROUNDING_CORRECTION, SUMMARY_SCOPE_CORRECTION, SUMMARY_TARGET_CORRECTION, SUMMARY_TYPE_CORRECTION, TARGET_RELEVANCE_CORRECTION, UPDATE_TARGET_TYPE_CORRECTION, gate_structure_repair_prompt
|
|
12
|
+
from .prompts import COVERAGE_ALREADY_COMPLETED_CORRECTION, COVERAGE_CANDIDATE_CORRECTION, COVERAGE_CORRECTION, COVERAGE_SHAPE_CORRECTION, DUPLICATE_TARGET_CORRECTION, EVIDENCE_EVENT_MAPPING_CORRECTION, EVIDENCE_SPAN_CORRECTION, GATE_STRUCTURE_REPAIR_SYSTEM, GATE_TYPE_CORRECTION, JSON_CORRECTION, MIXED_FUTURE_USE_CORRECTION, MIXED_PROJECT_SCOPES_CORRECTION, RELATIVE_TIME_CORRECTION, SCOPE_GROUNDING_CORRECTION, SOURCES_SHAPE_CORRECTION, SUMMARY_SCOPE_CORRECTION, SUMMARY_TARGET_CORRECTION, SUMMARY_TYPE_CORRECTION, TARGET_RELEVANCE_CORRECTION, UPDATE_TARGET_TYPE_CORRECTION, gate_structure_repair_prompt
|
|
13
13
|
from .validation import MODEL_VALIDATION_DETAILS, ModelOutputError, parse_strict_json
|
|
14
14
|
from .process_common import _DIAGNOSTIC_FILENAME, _DIAGNOSTIC_MAX_BYTES, _failure_metadata, _model_output_statistics, _safe_evidence_check, _safe_evidence_diagnostics
|
|
15
15
|
|
|
@@ -686,6 +686,8 @@ class ModelExecutor:
|
|
|
686
686
|
return COVERAGE_ALREADY_COMPLETED_CORRECTION
|
|
687
687
|
context = ModelExecutor._evidence_correction_context(error)
|
|
688
688
|
return COVERAGE_CORRECTION if context is None else COVERAGE_CORRECTION + "\n" + context
|
|
689
|
+
if stage == "summarize" and hint == "source_shape":
|
|
690
|
+
return SOURCES_SHAPE_CORRECTION
|
|
689
691
|
if stage == "summarize" and hint == "scope_drift":
|
|
690
692
|
return SUMMARY_SCOPE_CORRECTION
|
|
691
693
|
if hint in {"relative_time", "due_date_not_grounded"}:
|
|
@@ -16,14 +16,70 @@ from .inbox import InboxEvent, InboxTurn, parse_inbox
|
|
|
16
16
|
from .locking import atomic_write_json, atomic_write_text
|
|
17
17
|
from .turn_plan import turn_identity_key
|
|
18
18
|
from .redaction import redact_text
|
|
19
|
-
from .vault import safe_component
|
|
19
|
+
from .vault import Vault, safe_component
|
|
20
20
|
from .process_common import ProcessingError, _FAILED_STATUS, _LEGACY_PROCESSING_GRACE_SECONDS, _MAX_SESSION_LINEAGE_DEPTH, _PROCESSING_LEASE_SECONDS, _PROCESSING_STATUS, _Snapshot, _as_int, _event_payload, _failure_metadata, _now_value, _parse_time, _read_processed, _safe_evidence_check, _safe_evidence_diagnostics, _safe_scope_background, _session_key
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def processing_health(vault_path: Path | str) -> dict[str, Any]:
|
|
24
|
+
"""Report whether automatic extraction is keeping up, and why not.
|
|
25
|
+
|
|
26
|
+
Strictly read-only: the Vault is opened without ``create`` and nothing is
|
|
27
|
+
written. Failures were previously visible only inside
|
|
28
|
+
``_state/processed.json``, so a Vault whose every turn failed extraction
|
|
29
|
+
still looked idle from the outside and the user had no way to notice.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
vault = Vault(vault_path, create=False)
|
|
33
|
+
processed = _read_processed(vault.processed_state_path)
|
|
34
|
+
sessions = processed.get("sessions")
|
|
35
|
+
failures: list[dict[str, Any]] = []
|
|
36
|
+
running = 0
|
|
37
|
+
idle = 0
|
|
38
|
+
failed_turns = 0
|
|
39
|
+
last_processed: str | None = None
|
|
40
|
+
if isinstance(sessions, Mapping):
|
|
41
|
+
for key, state in sessions.items():
|
|
42
|
+
if not isinstance(state, Mapping):
|
|
43
|
+
continue
|
|
44
|
+
marker = state.get("processing")
|
|
45
|
+
marker = marker if isinstance(marker, Mapping) else {}
|
|
46
|
+
observed = marker.get("last_processed_at")
|
|
47
|
+
if isinstance(observed, str) and (last_processed is None or observed > last_processed):
|
|
48
|
+
last_processed = observed
|
|
49
|
+
status = marker.get("status")
|
|
50
|
+
if status == _FAILED_STATUS:
|
|
51
|
+
turn_indices = marker.get("turn_indices")
|
|
52
|
+
count = len(turn_indices) if isinstance(turn_indices, list) else 0
|
|
53
|
+
failed_turns += count
|
|
54
|
+
failures.append(
|
|
55
|
+
{
|
|
56
|
+
"session_id": key,
|
|
57
|
+
"failure_code": marker.get("failure_code"),
|
|
58
|
+
"failure_stage": marker.get("failure_stage"),
|
|
59
|
+
"attempt_count": marker.get("attempt_count"),
|
|
60
|
+
"turn_count": count,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
elif status == _PROCESSING_STATUS:
|
|
64
|
+
running += 1
|
|
65
|
+
else:
|
|
66
|
+
idle += 1
|
|
67
|
+
failures.sort(key=lambda item: str(item.get("session_id")))
|
|
68
|
+
return {
|
|
69
|
+
"status": "failed" if failures else ("running" if running else "idle"),
|
|
70
|
+
"failed_sessions": len(failures),
|
|
71
|
+
"failed_turns": failed_turns,
|
|
72
|
+
"running_sessions": running,
|
|
73
|
+
"idle_sessions": idle,
|
|
74
|
+
"last_processed_at": last_processed,
|
|
75
|
+
"retryable": bool(failures or running),
|
|
76
|
+
"failures": failures,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
23
80
|
class ProcessJournal:
|
|
24
81
|
def __init__(self, service: Any):
|
|
25
82
|
self.service = service
|
|
26
|
-
|
|
27
83
|
def _write_processed_unlocked(self, processed: Mapping[str, Any]) -> None:
|
|
28
84
|
atomic_write_json(self.service.vault.processed_state_path, dict(processed))
|
|
29
85
|
|
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from .validation import SOURCE_FIELDS
|
|
6
|
+
|
|
7
|
+
_SOURCE_FIELDS_TEXT = ", ".join(SOURCE_FIELDS)
|
|
8
|
+
|
|
9
|
+
# The model is never asked to guess the ``sources`` entry shape: this sentence
|
|
10
|
+
# is generated from the validator's own field list, so the instruction and the
|
|
11
|
+
# enforced contract stay in step.
|
|
12
|
+
_SOURCES_CONTRACT = (
|
|
13
|
+
"sources is a non-empty array of objects, never an array of bare strings. Each "
|
|
14
|
+
f"object may contain only these keys: {_SOURCE_FIELDS_TEXT}. Every value is a "
|
|
15
|
+
"string, except evidence_event_ids, which is a non-empty array of strings. Write "
|
|
16
|
+
'[{"event_key":"<admitted event key>"}], not ["<admitted event key>"]. Use only '
|
|
17
|
+
"event keys admitted for the current turn."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
SOURCES_SHAPE_CORRECTION = (
|
|
21
|
+
"Previous output violated: source_shape. "
|
|
22
|
+
+ _SOURCES_CONTRACT
|
|
23
|
+
+ " Keep every other field of the summary unchanged."
|
|
24
|
+
)
|
|
25
|
+
|
|
5
26
|
|
|
6
27
|
GATE_SYSTEM = """You are memleaf's strict, source-neutral memory admission Gate. Return exactly one strict JSON object with top-level fields candidates, coverage, and evidence_bindings.
|
|
7
28
|
|
|
@@ -73,6 +94,9 @@ Semantic completeness is required: keep the smallest complete confirmed content
|
|
|
73
94
|
OUTPUT CONTRACT
|
|
74
95
|
A normal summary requires title, body, tags, type, scopes, and sources. Optional existing-schema fields are memory_id, update_memory_id, aliases, keywords, scope_source, evidence_event_ids, shadow_native_ids, scope_operations, status, completed_at, and due_date. Use only admitted current event keys in sources/evidence references. Copy the Gate candidate's type and scopes exactly; if scope_source is present, it must match the Gate value.
|
|
75
96
|
|
|
97
|
+
SOURCES SHAPE
|
|
98
|
+
""" + _SOURCES_CONTRACT + """
|
|
99
|
+
|
|
76
100
|
TODO AND DATES
|
|
77
101
|
Evidence events may include an ISO-8601 UTC timestamp. For a new todo, include status and due_date; use due_date=null when no deadline is established. For an updated todo, include current status. completed requires completed_at grounded in the admitted event timestamp. Do not invent dates. Preserve only date meaning supported by admitted Evidence; an admitted visible-message timestamp may anchor a supported relative date. Core normalizes supported relative calendar dates to YYYY-MM-DD and validates grounding; recurring schedules may remain recurring.
|
|
78
102
|
|
|
@@ -26,6 +26,19 @@ def _is_cjk_char(value: str) -> bool:
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _cjk_span_bar(part: str) -> int:
|
|
30
|
+
"""Return the contiguous-span bar for one CJK component.
|
|
31
|
+
|
|
32
|
+
The bar is a fraction of the component but never longer than the component
|
|
33
|
+
itself. A two-character term could otherwise never reach it, which turned
|
|
34
|
+
every compound query carrying a short CJK word plus an ASCII token into an
|
|
35
|
+
unsatisfiable one: the compound branch requires a strong CJK match as soon
|
|
36
|
+
as an ASCII component is present.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
return min(len(part), max(3, (len(part) * 35 + 99) // 100))
|
|
40
|
+
|
|
41
|
+
|
|
29
42
|
def _query_parts(query: str | Iterable[str]) -> tuple[str, list[str], set[str]]:
|
|
30
43
|
"""Return the normalized query and its lexical components.
|
|
31
44
|
|
|
@@ -45,6 +58,13 @@ def _query_parts(query: str | Iterable[str]) -> tuple[str, list[str], set[str]]:
|
|
|
45
58
|
for raw_part, part in zip(raw_parts, normalized_raw_parts):
|
|
46
59
|
if any(_is_cjk_char(char) for char in part):
|
|
47
60
|
continue
|
|
61
|
+
# Only a component that can satisfy the strong-match bar used below may
|
|
62
|
+
# become a *requirement*. Splitting a punctuated identifier such as
|
|
63
|
+
# ``vX.Y.Z`` yields the one-character fragments ``Y`` and ``Z``; making
|
|
64
|
+
# those mandatory would demand a match that the same bar refuses to
|
|
65
|
+
# grant, so the compound query could never match anything at all.
|
|
66
|
+
if len(part) < 3 and not any(char.isdigit() for char in raw_part):
|
|
67
|
+
continue
|
|
48
68
|
if any(char.isdigit() for char in raw_part) or raw_part.isalpha() and raw_part.isupper():
|
|
49
69
|
explicit.add(part)
|
|
50
70
|
# A trailing hyphen component is a common identifier form even when a
|
|
@@ -123,7 +143,7 @@ def candidate_matches_query(memory: Memory, query: str | Iterable[str]) -> bool:
|
|
|
123
143
|
return True
|
|
124
144
|
if not any(_is_cjk_char(char) for char in part):
|
|
125
145
|
return False
|
|
126
|
-
minimum =
|
|
146
|
+
minimum = _cjk_span_bar(part)
|
|
127
147
|
return max((_longest_common_substring(part, value) for value in values), default=0) >= minimum
|
|
128
148
|
|
|
129
149
|
has_cjk_part = any(any(_is_cjk_char(char) for char in part) for part in parts)
|
|
@@ -142,7 +162,7 @@ def candidate_matches_query(memory: Memory, query: str | Iterable[str]) -> bool:
|
|
|
142
162
|
for part in parts:
|
|
143
163
|
if any(_is_cjk_char(char) for char in part):
|
|
144
164
|
longest = max((_longest_common_substring(part, value) for value in values), default=0)
|
|
145
|
-
minimum =
|
|
165
|
+
minimum = _cjk_span_bar(part)
|
|
146
166
|
if longest >= minimum:
|
|
147
167
|
strong_match = True
|
|
148
168
|
strong_cjk_match = True
|
|
@@ -410,7 +410,11 @@ _RELATIVE_DAY_OFFSETS = {
|
|
|
410
410
|
"昨天": -1,
|
|
411
411
|
"昨日": -1,
|
|
412
412
|
}
|
|
413
|
-
|
|
413
|
+
# The single authority for the ``sources`` entry shape. ``prompts.py`` builds
|
|
414
|
+
# its output contract from this tuple, so the instruction the model receives and
|
|
415
|
+
# the shape this validator enforces cannot drift apart the way the B3 protocol
|
|
416
|
+
# string once did.
|
|
417
|
+
SOURCE_FIELDS = ("event_key", "session_id", "turn_id", "conversation_title", "evidence_event_ids")
|
|
414
418
|
_COMPACT_FIELDS = frozenset(
|
|
415
419
|
(
|
|
416
420
|
"title",
|
|
@@ -1274,9 +1278,22 @@ def _source_items(value: Any) -> list[dict[str, Any]]:
|
|
|
1274
1278
|
raise ModelOutputError("sources must be a non-empty list", validation_detail="source_shape")
|
|
1275
1279
|
result: list[dict[str, Any]] = []
|
|
1276
1280
|
for source in value:
|
|
1281
|
+
if isinstance(source, str):
|
|
1282
|
+
# A bare admitted event key is unambiguous: under this contract a
|
|
1283
|
+
# bare string cannot denote any other field. Normalize it to the
|
|
1284
|
+
# object form instead of failing the whole summary; the event-key
|
|
1285
|
+
# authority check below still applies unchanged.
|
|
1286
|
+
result.append(
|
|
1287
|
+
{
|
|
1288
|
+
"event_key": _string(
|
|
1289
|
+
source, "source event_key", validation_detail="source_shape"
|
|
1290
|
+
)
|
|
1291
|
+
}
|
|
1292
|
+
)
|
|
1293
|
+
continue
|
|
1277
1294
|
if not isinstance(source, Mapping):
|
|
1278
1295
|
raise ModelOutputError("each source must be an object", validation_detail="source_shape")
|
|
1279
|
-
if set(source) - set(
|
|
1296
|
+
if set(source) - set(SOURCE_FIELDS):
|
|
1280
1297
|
raise ModelOutputError("source contains unknown fields", validation_detail="unknown_fields")
|
|
1281
1298
|
item = {}
|
|
1282
1299
|
for key, child in source.items():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.47
|
|
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.47。**
|
|
27
27
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|