memleaf 0.2.4__tar.gz → 0.2.6__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.4 → memleaf-0.2.6}/CHANGELOG.md +24 -0
- {memleaf-0.2.4/src/memleaf.egg-info → memleaf-0.2.6}/PKG-INFO +3 -3
- {memleaf-0.2.4 → memleaf-0.2.6}/README.md +2 -2
- {memleaf-0.2.4 → memleaf-0.2.6}/pyproject.toml +1 -1
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/hermes_provider/__init__.py +2 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/memory_writer.py +41 -13
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/processing.py +59 -1
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/prompts.py +73 -4
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/validation.py +85 -2
- {memleaf-0.2.4 → memleaf-0.2.6/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_hermes_provider.py +34 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_pypi_install.py +1 -1
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b1.py +158 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b2a.py +607 -1
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_c3_packaging.py +1 -1
- {memleaf-0.2.4 → memleaf-0.2.6}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/LICENSE +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/MANIFEST.in +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/README.en.md +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/examples/README.md +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/examples/basic_usage.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/install.ps1 +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/install.sh +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/setup.cfg +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/config.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/index.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/models.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/service.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/__init__.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_host_events.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_install.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.4 → memleaf-0.2.6}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.6 — 2026-09-01
|
|
6
|
+
|
|
7
|
+
- Reject one worthy memory that spans multiple independent project scopes and
|
|
8
|
+
retry with an explicit instruction to split actionable project topics while
|
|
9
|
+
discarding temporary mailbox-sweep status items.
|
|
10
|
+
- Tighten automatic admission so aggregate mailbox/daily-report snapshots,
|
|
11
|
+
one-off meetings without durable outcomes, pending PM acceptance, and
|
|
12
|
+
awaiting-feedback states do not become a cross-project permanent memory;
|
|
13
|
+
keep independent risks and actionable todos eligible.
|
|
14
|
+
- Prefer `UPDATE` or `NO_CHANGE` for an existing future-use topic, preserve
|
|
15
|
+
unimplemented customer requests as proposed or pending, and recognize the
|
|
16
|
+
Chinese relative-date forms `今日`, `明日`, and `昨日` in strict retries.
|
|
17
|
+
|
|
18
|
+
## 0.2.5 — 2026-09-01
|
|
19
|
+
|
|
20
|
+
- Detect multiple gate candidates that target the same active memory before
|
|
21
|
+
batch writing, then retry with an explicit instruction to merge the
|
|
22
|
+
candidates while preserving all supporting evidence.
|
|
23
|
+
- Keep gate-selected update targets and active-memory types stable through
|
|
24
|
+
summarization, with bounded corrective retries for target or type drift.
|
|
25
|
+
- Preserve `duplicate_update_target` and aggregate preflight diagnostics across
|
|
26
|
+
Core, MCP, and Hermes while retaining the inbox and zero-write transaction
|
|
27
|
+
boundary after repeated failure.
|
|
28
|
+
|
|
5
29
|
## 0.2.4 — 2026-09-01
|
|
6
30
|
|
|
7
31
|
- Give `relative_time` retries explicit, actionable correction instructions so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,8 +23,8 @@ 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.
|
|
27
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
26
|
+
> **当前版本:0.2.6。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.6 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -4,8 +4,8 @@
|
|
|
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.
|
|
8
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
7
|
+
> **当前版本:0.2.6。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.6 通过 PyPI 分发。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
11
11
|
## 项目定位
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.6"
|
|
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
|
|
@@ -133,6 +133,25 @@ class MemoryWriter:
|
|
|
133
133
|
}
|
|
134
134
|
return request_keys.issubset(applied_keys)
|
|
135
135
|
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _preflight_error(
|
|
138
|
+
message: str,
|
|
139
|
+
*,
|
|
140
|
+
validation_detail: str = "other_schema_violation",
|
|
141
|
+
) -> ModelOutputError:
|
|
142
|
+
"""Return a safe, stage-labelled batch-consistency failure."""
|
|
143
|
+
|
|
144
|
+
error = ModelOutputError(
|
|
145
|
+
message,
|
|
146
|
+
validation_reason="schema_violation",
|
|
147
|
+
validation_detail=validation_detail,
|
|
148
|
+
)
|
|
149
|
+
# The model outputs that form this batch have already passed their
|
|
150
|
+
# individual summaries. Keep the public stage vocabulary bounded
|
|
151
|
+
# while making a defensive aggregate failure diagnosable.
|
|
152
|
+
error.stage = "summarize"
|
|
153
|
+
return error
|
|
154
|
+
|
|
136
155
|
def _preflight(self, requests: list[Mapping[str, Any]]) -> None:
|
|
137
156
|
active = self._active_records()
|
|
138
157
|
target_ids: set[str] = set()
|
|
@@ -144,11 +163,14 @@ class MemoryWriter:
|
|
|
144
163
|
duplicate_id = request.get("duplicate_memory_id")
|
|
145
164
|
if duplicate_id is not None:
|
|
146
165
|
if not isinstance(duplicate_id, str) or duplicate_id not in active:
|
|
147
|
-
raise
|
|
166
|
+
raise self._preflight_error("duplicate target is not an active memory")
|
|
148
167
|
if duplicate_id in duplicate_ids:
|
|
149
|
-
raise
|
|
168
|
+
raise self._preflight_error(
|
|
169
|
+
"batch contains duplicate metadata merge target",
|
|
170
|
+
validation_detail="duplicate_update_target",
|
|
171
|
+
)
|
|
150
172
|
if summary.get("update_memory_id") is not None:
|
|
151
|
-
raise
|
|
173
|
+
raise self._preflight_error("duplicate target conflicts with update target")
|
|
152
174
|
duplicate_ids.add(duplicate_id)
|
|
153
175
|
continue
|
|
154
176
|
target_id = summary.get("update_memory_id")
|
|
@@ -161,12 +183,15 @@ class MemoryWriter:
|
|
|
161
183
|
)
|
|
162
184
|
seen_turns = target_turns.setdefault(target_id, set())
|
|
163
185
|
if turn_identity in seen_turns:
|
|
164
|
-
raise
|
|
186
|
+
raise self._preflight_error(
|
|
187
|
+
"batch contains duplicate update target",
|
|
188
|
+
validation_detail="duplicate_update_target",
|
|
189
|
+
)
|
|
165
190
|
seen_turns.add(turn_identity)
|
|
166
191
|
target_ids.add(target_id)
|
|
167
192
|
target = active.get(target_id)
|
|
168
193
|
if target is None:
|
|
169
|
-
raise
|
|
194
|
+
raise self._preflight_error("update target is not an active memory")
|
|
170
195
|
target_memory = getattr(target, "memory", target)
|
|
171
196
|
target_type = (
|
|
172
197
|
target_memory.type
|
|
@@ -176,19 +201,22 @@ class MemoryWriter:
|
|
|
176
201
|
else None
|
|
177
202
|
)
|
|
178
203
|
if target_type != summary.get("type"):
|
|
179
|
-
raise
|
|
204
|
+
raise self._preflight_error(
|
|
205
|
+
"update target type does not match summary",
|
|
206
|
+
validation_detail="invalid_type",
|
|
207
|
+
)
|
|
180
208
|
deterministic_id = request["memory_id"]
|
|
181
209
|
if deterministic_id in memory_ids:
|
|
182
|
-
raise
|
|
210
|
+
raise self._preflight_error("batch contains duplicate deterministic memory id")
|
|
183
211
|
if deterministic_id in duplicate_ids:
|
|
184
|
-
raise
|
|
212
|
+
raise self._preflight_error("batch memory id collides with metadata merge target")
|
|
185
213
|
if deterministic_id in target_ids:
|
|
186
|
-
raise
|
|
214
|
+
raise self._preflight_error("batch memory id collides with update target")
|
|
187
215
|
if target_id is not None and target_id.casefold() == deterministic_id.casefold():
|
|
188
|
-
raise
|
|
216
|
+
raise self._preflight_error("batch memory id collides with update target")
|
|
189
217
|
memory_ids.add(deterministic_id)
|
|
190
218
|
if "/" in deterministic_id or "\\" in deterministic_id or deterministic_id in (".", ".."):
|
|
191
|
-
raise
|
|
219
|
+
raise self._preflight_error("invalid deterministic memory id")
|
|
192
220
|
# Make the request visible to the rest of this same commit batch.
|
|
193
221
|
# This is needed when a later pending turn updates a memory that
|
|
194
222
|
# an earlier pending turn has just created; the real filesystem
|
|
@@ -198,9 +226,9 @@ class MemoryWriter:
|
|
|
198
226
|
else:
|
|
199
227
|
active[deterministic_id] = summary
|
|
200
228
|
if target_ids.intersection(duplicate_ids):
|
|
201
|
-
raise
|
|
229
|
+
raise self._preflight_error("batch update target collides with metadata merge target")
|
|
202
230
|
if memory_ids.intersection(duplicate_ids):
|
|
203
|
-
raise
|
|
231
|
+
raise self._preflight_error("batch memory id collides with metadata merge target")
|
|
204
232
|
|
|
205
233
|
def _core_sources(self, request: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
206
234
|
turn = request["turn"]
|
|
@@ -31,9 +31,13 @@ from .memory_writer import MemoryWriter
|
|
|
31
31
|
from .models import Memory, utc_now
|
|
32
32
|
from .native_index import NativeIndexer
|
|
33
33
|
from .prompts import (
|
|
34
|
+
DUPLICATE_TARGET_CORRECTION,
|
|
34
35
|
JSON_CORRECTION,
|
|
36
|
+
MIXED_PROJECT_SCOPES_CORRECTION,
|
|
35
37
|
RELATIVE_TIME_CORRECTION,
|
|
36
38
|
GATE_SYSTEM,
|
|
39
|
+
SUMMARY_TARGET_CORRECTION,
|
|
40
|
+
SUMMARY_TYPE_CORRECTION,
|
|
37
41
|
SUMMARIZE_SYSTEM,
|
|
38
42
|
gate_prompt,
|
|
39
43
|
summarize_prompt,
|
|
@@ -473,8 +477,17 @@ class Processor:
|
|
|
473
477
|
@staticmethod
|
|
474
478
|
def _correction_instruction(error: BaseException) -> Optional[str]:
|
|
475
479
|
hint = Processor._safe_correction_hint(error)
|
|
480
|
+
stage = getattr(error, "stage", None)
|
|
481
|
+
if hint == "duplicate_update_target":
|
|
482
|
+
return DUPLICATE_TARGET_CORRECTION
|
|
483
|
+
if hint == "mixed_project_scopes":
|
|
484
|
+
return MIXED_PROJECT_SCOPES_CORRECTION
|
|
476
485
|
if hint == "relative_time":
|
|
477
486
|
return RELATIVE_TIME_CORRECTION
|
|
487
|
+
if stage == "summarize" and hint == "invalid_update_target":
|
|
488
|
+
return SUMMARY_TARGET_CORRECTION
|
|
489
|
+
if stage == "summarize" and hint == "invalid_type":
|
|
490
|
+
return SUMMARY_TYPE_CORRECTION
|
|
478
491
|
if hint is not None:
|
|
479
492
|
return f"Previous output violated: {hint}."
|
|
480
493
|
return None
|
|
@@ -1548,6 +1561,13 @@ class Processor:
|
|
|
1548
1561
|
if item.get("native") is not True and isinstance(item.get("memory_id"), str)
|
|
1549
1562
|
]
|
|
1550
1563
|
gate_related_memory_ids = list(related_memory_ids)
|
|
1564
|
+
gate_related_memory_types = {
|
|
1565
|
+
item["memory_id"].casefold(): item["type"]
|
|
1566
|
+
for item in gate_related
|
|
1567
|
+
if isinstance(item, Mapping)
|
|
1568
|
+
and isinstance(item.get("memory_id"), str)
|
|
1569
|
+
and isinstance(item.get("type"), str)
|
|
1570
|
+
}
|
|
1551
1571
|
if scope_fallback is not None:
|
|
1552
1572
|
scoped_records, scope_ambiguous = scope_fallback
|
|
1553
1573
|
if scope_ambiguous and self._single_specific_scope(scope_background):
|
|
@@ -1559,6 +1579,11 @@ class Processor:
|
|
|
1559
1579
|
value.casefold() for value in gate_related_memory_ids
|
|
1560
1580
|
}:
|
|
1561
1581
|
gate_related_memory_ids.append(memory_id)
|
|
1582
|
+
if (
|
|
1583
|
+
isinstance(memory_id, str)
|
|
1584
|
+
and isinstance(entry.get("type"), str)
|
|
1585
|
+
):
|
|
1586
|
+
gate_related_memory_types[memory_id.casefold()] = entry["type"]
|
|
1562
1587
|
scope_registry = self._scope_registry_projection()
|
|
1563
1588
|
with self.service.vault.lock():
|
|
1564
1589
|
validation_scope_registry = self.service.vault.config().get("scopes", {})
|
|
@@ -1620,6 +1645,7 @@ class Processor:
|
|
|
1620
1645
|
raw,
|
|
1621
1646
|
current_event_keys=turn.event_keys,
|
|
1622
1647
|
related_memory_ids=gate_related_memory_ids,
|
|
1648
|
+
related_memory_types=gate_related_memory_types,
|
|
1623
1649
|
),
|
|
1624
1650
|
diagnostic_context={
|
|
1625
1651
|
"source": turn.source,
|
|
@@ -1728,6 +1754,22 @@ class Processor:
|
|
|
1728
1754
|
)
|
|
1729
1755
|
continue
|
|
1730
1756
|
|
|
1757
|
+
gate_update_target = candidate.get("update_memory_id")
|
|
1758
|
+
gate_target_type = None
|
|
1759
|
+
if isinstance(gate_update_target, str):
|
|
1760
|
+
gate_target_key = gate_update_target.casefold()
|
|
1761
|
+
gate_target_type = next(
|
|
1762
|
+
(
|
|
1763
|
+
item.get("type")
|
|
1764
|
+
for item in candidate_related
|
|
1765
|
+
if isinstance(item, Mapping)
|
|
1766
|
+
and isinstance(item.get("memory_id"), str)
|
|
1767
|
+
and item["memory_id"].casefold() == gate_target_key
|
|
1768
|
+
and isinstance(item.get("type"), str)
|
|
1769
|
+
),
|
|
1770
|
+
None,
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1731
1773
|
summary = self._complete_json_stage(
|
|
1732
1774
|
backend,
|
|
1733
1775
|
summarize_prompt(
|
|
@@ -1745,6 +1787,20 @@ class Processor:
|
|
|
1745
1787
|
related_native_ids=candidate_native_ids,
|
|
1746
1788
|
related_memory_ids=candidate_memory_ids,
|
|
1747
1789
|
scope_registry=validation_scope_registry,
|
|
1790
|
+
expected_type=(
|
|
1791
|
+
candidate.get("type")
|
|
1792
|
+
if (
|
|
1793
|
+
gate_update_target is not None
|
|
1794
|
+
and gate_target_type == candidate.get("type")
|
|
1795
|
+
)
|
|
1796
|
+
else None
|
|
1797
|
+
),
|
|
1798
|
+
expected_update_memory_id=gate_update_target,
|
|
1799
|
+
expected_target_type=(
|
|
1800
|
+
gate_target_type
|
|
1801
|
+
if gate_target_type == candidate.get("type")
|
|
1802
|
+
else None
|
|
1803
|
+
),
|
|
1748
1804
|
),
|
|
1749
1805
|
diagnostic_context={
|
|
1750
1806
|
"source": turn.source,
|
|
@@ -1752,7 +1808,6 @@ class Processor:
|
|
|
1752
1808
|
"turn_index": turn.turn_index,
|
|
1753
1809
|
},
|
|
1754
1810
|
)
|
|
1755
|
-
gate_update_target = candidate.get("update_memory_id")
|
|
1756
1811
|
if gate_update_target is not None:
|
|
1757
1812
|
summary_update_target = summary.get("update_memory_id")
|
|
1758
1813
|
if summary_update_target is None:
|
|
@@ -1766,6 +1821,9 @@ class Processor:
|
|
|
1766
1821
|
"summary update target differs from gate target",
|
|
1767
1822
|
validation_detail="invalid_update_target",
|
|
1768
1823
|
)
|
|
1824
|
+
else:
|
|
1825
|
+
summary = dict(summary)
|
|
1826
|
+
summary["update_memory_id"] = gate_update_target
|
|
1769
1827
|
if summary["scopes"] == ["unscoped"] or summary.get("scope_source") == "insufficient_context":
|
|
1770
1828
|
self._defer_candidate(
|
|
1771
1829
|
turn_ref,
|
|
@@ -34,6 +34,11 @@ useful. Related active memories only identify a complete duplicate or valid
|
|
|
34
34
|
state-update target; they are not current evidence and cannot make a non-worthy
|
|
35
35
|
operational/test result worthy. Their item, body, and serialized-character
|
|
36
36
|
context is bounded; ellipses and omissions do not prove a fact is absent.
|
|
37
|
+
Within one gate response, each active memory target may appear at most once
|
|
38
|
+
across all candidates, whether it is named by duplicate_memory_id or
|
|
39
|
+
update_memory_id. If multiple facts share one future-use/update target, merge
|
|
40
|
+
them into one candidate and include all supporting evidence IDs; never emit
|
|
41
|
+
separate candidates for the same target.
|
|
37
42
|
Resolve an indirect reference only to one supplied active memory; otherwise
|
|
38
43
|
return no candidate or leave it unscoped for retry. Never copy a related
|
|
39
44
|
diagnostic/tool failure without a separate user-confirmed business fact, project
|
|
@@ -42,7 +47,7 @@ Calendar dates are strict. Each event may include an ISO-8601 UTC timestamp.
|
|
|
42
47
|
For a one-off relative date in a candidate or its later summary, use the
|
|
43
48
|
timestamp of the evidence event that supports it as the calendar anchor and
|
|
44
49
|
emit an absolute YYYY-MM-DD date. Resolve today/tomorrow/yesterday,
|
|
45
|
-
|
|
50
|
+
今天/明天/昨天/今日/明日/昨日, 本周X/这周X/下周X/上周X, and this/next/last weekday;
|
|
46
51
|
use Monday-Sunday calendar weeks for week expressions. Do not leave these
|
|
47
52
|
strong relative forms in a worthy memory, and never guess without a timestamp:
|
|
48
53
|
defer or omit a date-dependent candidate if its supporting event has no
|
|
@@ -87,6 +92,13 @@ be independently retrieved and updated. Prefer one coherent current-state memory
|
|
|
87
92
|
over adjacent overlapping snapshots; do not use a score or fixed category rule.
|
|
88
93
|
Most ordinary turns should produce zero or one candidate; multiple require
|
|
89
94
|
genuinely independent future questions or actions.
|
|
95
|
+
Never persist an aggregate mailbox sweep, daily report, or "items to watch"
|
|
96
|
+
digest as one cross-project memory. Inspect each item and split genuinely
|
|
97
|
+
independent project risks or actionable todo items; if an item has no
|
|
98
|
+
independent future use, set worth=false. A one-off meeting with no durable
|
|
99
|
+
decision or commitment, a request awaiting PM acceptance, and pending feedback
|
|
100
|
+
or another intermediate status normally have worth=false. Keep an independent
|
|
101
|
+
project risk or concrete future action even when it came from a digest.
|
|
90
102
|
An intermediate state such as a draft awaiting confirmation, preparation/
|
|
91
103
|
processing status, temporary error, repeated confirmation, or unconfirmed
|
|
92
104
|
suggestion normally has no independent future use and is worth=false. Keep one
|
|
@@ -108,6 +120,11 @@ operational incident spans stats/search/remember calls; this default remains.
|
|
|
108
120
|
Retain only a separate, user-confirmed future-use fact, preference, identity, constraint, commitment,
|
|
109
121
|
project risk, or durable lesson that prevents repeating the investigation, not
|
|
110
122
|
the execution transcript.
|
|
123
|
+
If a related active memory already covers the same future question/action and
|
|
124
|
+
the current turn adds no confirmed change, use worth=false and do not create a
|
|
125
|
+
sibling. If a customer or user proposes a new change without confirmation of
|
|
126
|
+
implementation, preserve it as requested/proposed/pending and never state that
|
|
127
|
+
it is already implemented.
|
|
111
128
|
Return no prose, markdown fences, comments, or trailing text."""
|
|
112
129
|
|
|
113
130
|
|
|
@@ -130,14 +147,22 @@ evidence; they cannot make a rejected operational or test result worthy. Their
|
|
|
130
147
|
item/body/serialized context is bounded; ellipses and omissions do not prove
|
|
131
148
|
absence. Resolve indirect references only from supplied scope and one related
|
|
132
149
|
active memory; otherwise return no candidate or leave it unscoped for retry.
|
|
150
|
+
When the gate candidate supplies update_memory_id, that target is immutable:
|
|
151
|
+
omit update_memory_id or copy that exact active ID, never replace it with a
|
|
152
|
+
different, guessed, native, or historical ID. Keep the summary type identical
|
|
153
|
+
to the gate candidate type and to the active update target type.
|
|
133
154
|
For the same use, preserve still-valid facts, add confirmed progress, replace
|
|
134
155
|
contradictions, and never reduce the memory to the latest operation or create a
|
|
135
156
|
sibling with a new label.
|
|
157
|
+
Never turn an email sweep, daily report, or watchlist into one cross-project
|
|
158
|
+
summary. Keep only one future-use topic in the summary; split unrelated
|
|
159
|
+
projects at the gate, and omit temporary meeting/awaiting-acceptance/pending-
|
|
160
|
+
feedback states unless they have an independent future action.
|
|
136
161
|
Calendar dates are strict. Each event may include an ISO-8601 UTC timestamp.
|
|
137
162
|
For a one-off relative date in the summary title or body, use the timestamp of
|
|
138
163
|
the supporting evidence event as the calendar anchor and emit an absolute
|
|
139
164
|
YYYY-MM-DD date. Resolve today/tomorrow/yesterday,
|
|
140
|
-
|
|
165
|
+
今天/明天/昨天/今日/明日/昨日, 本周X/这周X/下周X/上周X, and this/next/last weekday;
|
|
141
166
|
use Monday-Sunday calendar weeks for week expressions. Never leave these
|
|
142
167
|
strong relative forms in the summary, and never guess without a timestamp.
|
|
143
168
|
Recurring schedules such as 每天、每周三, or every Wednesday are not one-off
|
|
@@ -146,7 +171,12 @@ In automatic capture/process mode, a pure read-only query whose answer only
|
|
|
146
171
|
restates a related active memory is not a new candidate. It must have been
|
|
147
172
|
rejected by the gate; do not use duplicate_memory_id or update_memory_id to
|
|
148
173
|
append the query as a source. Only a newly confirmed fact or state change in
|
|
149
|
-
the current evidence may update an existing memory.
|
|
174
|
+
the current evidence may update an existing memory. If a related active memory
|
|
175
|
+
already covers the same future question/action and the current evidence adds no
|
|
176
|
+
confirmed change, retain the existing memory unchanged (the gate should emit
|
|
177
|
+
worth=false) rather than creating a sibling. When the current evidence is a
|
|
178
|
+
customer/user request that is not confirmed implemented, state it as
|
|
179
|
+
requested/proposed/pending and never as completed or deployed.
|
|
150
180
|
fields and JSON types are title (string), body (string), tags (string list),
|
|
151
181
|
type (one of preference, fact, project, todo, event, identity, other), scopes
|
|
152
182
|
(non-empty string list), and sources (non-empty object list). scope_source, if
|
|
@@ -201,7 +231,7 @@ RELATIVE_TIME_CORRECTION = (
|
|
|
201
231
|
"each one-off relative date from that timestamp and the stated weekday "
|
|
202
232
|
"semantics. In the summary title and body, every one-off calendar date must "
|
|
203
233
|
"be written only as YYYY-MM-DD: remove forms such as today/tomorrow/yesterday, "
|
|
204
|
-
"
|
|
234
|
+
"今天/明天/昨天/今日/明日/昨日, 本周三/这周三/下周三/上周三, and this/next/last Wednesday. "
|
|
205
235
|
"If a relative weekday is followed by a parenthesized numeric date and they "
|
|
206
236
|
"conflict, trust the event timestamp plus the weekday meaning, replace the "
|
|
207
237
|
"numeric date with the computed YYYY-MM-DD, and remove the relative wording "
|
|
@@ -211,6 +241,45 @@ RELATIVE_TIME_CORRECTION = (
|
|
|
211
241
|
)
|
|
212
242
|
|
|
213
243
|
|
|
244
|
+
MIXED_PROJECT_SCOPES_CORRECTION = (
|
|
245
|
+
"Previous output violated: mixed_project_scopes. Keep each worthy "
|
|
246
|
+
"candidate and summary atomic: it may contain at most one distinct "
|
|
247
|
+
"project:<name> scope. At the gate, split genuinely independent projects "
|
|
248
|
+
"into separate candidates, preserving their own evidence, or mark purely "
|
|
249
|
+
"temporary meeting/awaiting-acceptance/pending-feedback items worth=false. "
|
|
250
|
+
"In a summary, keep only one future-use project topic and its matching "
|
|
251
|
+
"details. global plus one project and valid domain/portfolio parent scopes "
|
|
252
|
+
"are allowed; do not remove those parent scopes merely to pass validation. "
|
|
253
|
+
"Return only the strict JSON object."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
DUPLICATE_TARGET_CORRECTION = (
|
|
258
|
+
"Previous output violated: duplicate_update_target. Merge every candidate "
|
|
259
|
+
"that refers to the same active memory target into one candidate for one "
|
|
260
|
+
"future-use topic. A target may appear only once across duplicate_memory_id "
|
|
261
|
+
"and update_memory_id; preserve all supporting current evidence_event_ids "
|
|
262
|
+
"in that single candidate. Keep update_memory_id only when it is the exact "
|
|
263
|
+
"related active ID, and return no separate sibling candidate for that target. "
|
|
264
|
+
"Return only the strict gate JSON object."
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
SUMMARY_TARGET_CORRECTION = (
|
|
269
|
+
"Previous output violated: invalid_update_target. The gate selected the "
|
|
270
|
+
"update target and it is immutable. In the summary, omit update_memory_id "
|
|
271
|
+
"or copy the gate-selected active memory_id exactly; never substitute a "
|
|
272
|
+
"different, guessed, native, or historical ID. Return only the strict JSON object."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
SUMMARY_TYPE_CORRECTION = (
|
|
277
|
+
"Previous output violated: invalid_type. Keep the summary type exactly equal "
|
|
278
|
+
"to the gate candidate type and to the active update target type. Do not "
|
|
279
|
+
"change the target or invent a new type. Return only the strict JSON object."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
214
283
|
COMPACT_SYSTEM = """You are memleaf's memory compactor. Return JSON only.
|
|
215
284
|
Merge only the supplied low-priority memories when they express compatible
|
|
216
285
|
information. Return an object with a memories array; [] is a safe no-op.
|
|
@@ -51,6 +51,8 @@ MODEL_VALIDATION_DETAILS = frozenset(
|
|
|
51
51
|
"unknown_fields",
|
|
52
52
|
"candidate_shape",
|
|
53
53
|
"duplicate_candidate_id",
|
|
54
|
+
"duplicate_update_target",
|
|
55
|
+
"mixed_project_scopes",
|
|
54
56
|
"invalid_evidence",
|
|
55
57
|
"invalid_flags",
|
|
56
58
|
"invalid_type",
|
|
@@ -71,7 +73,7 @@ _RELATIVE_CALENDAR_EXPRESSION = re.compile(
|
|
|
71
73
|
r"(?<![A-Za-z])(?:today|tomorrow|yesterday)(?![A-Za-z])"
|
|
72
74
|
r"|(?<![A-Za-z])(?:this|next|last)\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)(?![A-Za-z])"
|
|
73
75
|
r"|(?:本|这|下|上)(?:个)?(?:周|星期|礼拜)\s*(?:一|二|三|四|五|六|日|天|末|[1-7])"
|
|
74
|
-
r"|(
|
|
76
|
+
r"|(?:今天|明天|昨天|今日|明日|昨日)"
|
|
75
77
|
r")",
|
|
76
78
|
re.IGNORECASE,
|
|
77
79
|
)
|
|
@@ -227,6 +229,25 @@ def _scopes(value: Any, scope_source: Any) -> list[str]:
|
|
|
227
229
|
return result
|
|
228
230
|
|
|
229
231
|
|
|
232
|
+
def _reject_mixed_project_scopes(scopes: Iterable[str]) -> None:
|
|
233
|
+
"""Keep one worthy memory atomic across independent project scopes.
|
|
234
|
+
|
|
235
|
+
Parent scopes (global/domain/portfolio) may accompany one project scope;
|
|
236
|
+
only two or more distinct project scopes represent an ambiguous aggregate.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
project_scopes = {
|
|
240
|
+
scope.casefold()
|
|
241
|
+
for scope in scopes
|
|
242
|
+
if isinstance(scope, str) and scope.partition(":")[0] == "project"
|
|
243
|
+
}
|
|
244
|
+
if len(project_scopes) > 1:
|
|
245
|
+
raise ModelOutputError(
|
|
246
|
+
"one memory cannot cover multiple project scopes",
|
|
247
|
+
validation_detail="mixed_project_scopes",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
230
251
|
def _memory_id(value: Any, field: str) -> str:
|
|
231
252
|
result = _string(value, field)
|
|
232
253
|
if "/" in result or "\\" in result or result in (".", ".."):
|
|
@@ -397,6 +418,7 @@ def validate_gate_output(
|
|
|
397
418
|
*,
|
|
398
419
|
current_event_keys: Iterable[Any] | None = None,
|
|
399
420
|
related_memory_ids: Iterable[Any] | None = None,
|
|
421
|
+
related_memory_types: Mapping[str, Any] | None = None,
|
|
400
422
|
) -> dict[str, Any]:
|
|
401
423
|
"""Validate and return a normalized gate object without writing anything."""
|
|
402
424
|
|
|
@@ -426,6 +448,7 @@ def validate_gate_output(
|
|
|
426
448
|
}
|
|
427
449
|
normalized: list[dict[str, Any]] = []
|
|
428
450
|
candidate_ids: set[str] = set()
|
|
451
|
+
target_ids: set[str] = set()
|
|
429
452
|
for candidate in candidates:
|
|
430
453
|
if not isinstance(candidate, Mapping):
|
|
431
454
|
raise ModelOutputError("each gate candidate must be an object", validation_detail="candidate_shape")
|
|
@@ -504,10 +527,37 @@ def validate_gate_output(
|
|
|
504
527
|
"update_memory_id is not a related active memory",
|
|
505
528
|
validation_detail="invalid_update_target",
|
|
506
529
|
)
|
|
530
|
+
target_type = None
|
|
531
|
+
if isinstance(related_memory_types, Mapping):
|
|
532
|
+
target_type = next(
|
|
533
|
+
(
|
|
534
|
+
value
|
|
535
|
+
for key, value in related_memory_types.items()
|
|
536
|
+
if isinstance(key, str) and key.casefold() == update_id.casefold()
|
|
537
|
+
),
|
|
538
|
+
None,
|
|
539
|
+
)
|
|
540
|
+
if target_type is not None and candidate_type != target_type:
|
|
541
|
+
raise ModelOutputError(
|
|
542
|
+
"candidate type does not match update target",
|
|
543
|
+
validation_detail="invalid_type",
|
|
544
|
+
)
|
|
507
545
|
item["update_memory_id"] = update_id
|
|
546
|
+
for target_field in ("duplicate_memory_id", "update_memory_id"):
|
|
547
|
+
target = item.get(target_field)
|
|
548
|
+
if isinstance(target, str):
|
|
549
|
+
target_key = target.casefold()
|
|
550
|
+
if target_key in target_ids:
|
|
551
|
+
raise ModelOutputError(
|
|
552
|
+
"multiple gate candidates reference the same memory target; merge them into one candidate",
|
|
553
|
+
validation_detail="duplicate_update_target",
|
|
554
|
+
)
|
|
555
|
+
target_ids.add(target_key)
|
|
508
556
|
if not isinstance(item["scope_source"], str) or item["scope_source"] not in SCOPE_SOURCES:
|
|
509
557
|
raise ModelOutputError("invalid scope_source", validation_detail="invalid_scope_source")
|
|
510
|
-
_scopes(item["scopes"], item["scope_source"])
|
|
558
|
+
item["scopes"] = _scopes(item["scopes"], item["scope_source"])
|
|
559
|
+
if item["worth"]:
|
|
560
|
+
_reject_mixed_project_scopes(item["scopes"])
|
|
511
561
|
if "reason" in item:
|
|
512
562
|
_string(item["reason"], "reason", nonempty=False)
|
|
513
563
|
if len(item["reason"]) > 30:
|
|
@@ -523,6 +573,7 @@ def parse_gate_output(
|
|
|
523
573
|
*,
|
|
524
574
|
current_event_keys: Iterable[Any] | None = None,
|
|
525
575
|
related_memory_ids: Iterable[Any] | None = None,
|
|
576
|
+
related_memory_types: Mapping[str, Any] | None = None,
|
|
526
577
|
) -> dict[str, Any]:
|
|
527
578
|
try:
|
|
528
579
|
parsed = parse_strict_json(raw)
|
|
@@ -536,6 +587,7 @@ def parse_gate_output(
|
|
|
536
587
|
event_keys,
|
|
537
588
|
current_event_keys=current_event_keys,
|
|
538
589
|
related_memory_ids=related_memory_ids,
|
|
590
|
+
related_memory_types=related_memory_types,
|
|
539
591
|
)
|
|
540
592
|
except ModelOutputError as error:
|
|
541
593
|
if error.validation_reason is None:
|
|
@@ -581,6 +633,9 @@ def validate_summarize_output(
|
|
|
581
633
|
related_native_ids: Iterable[Any] | None = None,
|
|
582
634
|
related_memory_ids: Iterable[Any] | None = None,
|
|
583
635
|
scope_registry: Mapping[str, Any] | None = None,
|
|
636
|
+
expected_type: str | None = None,
|
|
637
|
+
expected_update_memory_id: str | None = None,
|
|
638
|
+
expected_target_type: str | None = None,
|
|
584
639
|
) -> dict[str, Any]:
|
|
585
640
|
"""Validate one atomic memory summary; this function has no filesystem effects."""
|
|
586
641
|
|
|
@@ -615,7 +670,18 @@ def validate_summarize_output(
|
|
|
615
670
|
candidate_type = item["type"]
|
|
616
671
|
if not isinstance(candidate_type, str) or candidate_type not in MEMORY_TYPES:
|
|
617
672
|
raise ModelOutputError("invalid memory type", validation_detail="invalid_type")
|
|
673
|
+
if expected_type is not None and candidate_type != expected_type:
|
|
674
|
+
raise ModelOutputError(
|
|
675
|
+
"summary type differs from gate candidate",
|
|
676
|
+
validation_detail="invalid_type",
|
|
677
|
+
)
|
|
678
|
+
if expected_target_type is not None and candidate_type != expected_target_type:
|
|
679
|
+
raise ModelOutputError(
|
|
680
|
+
"summary type does not match update target",
|
|
681
|
+
validation_detail="invalid_type",
|
|
682
|
+
)
|
|
618
683
|
item["scopes"] = _scopes(item["scopes"], item.get("scope_source"))
|
|
684
|
+
_reject_mixed_project_scopes(item["scopes"])
|
|
619
685
|
item["scope_operations"] = _scope_operations(
|
|
620
686
|
item.get("scope_operations"),
|
|
621
687
|
summary_scopes=item["scopes"],
|
|
@@ -635,6 +701,17 @@ def validate_summarize_output(
|
|
|
635
701
|
item["memory_id"] = _memory_id(item["memory_id"], "memory_id")
|
|
636
702
|
if "update_memory_id" in item:
|
|
637
703
|
item["update_memory_id"] = _memory_id(item["update_memory_id"], "update_memory_id")
|
|
704
|
+
if (
|
|
705
|
+
expected_update_memory_id is not None
|
|
706
|
+
and (
|
|
707
|
+
not isinstance(expected_update_memory_id, str)
|
|
708
|
+
or item["update_memory_id"].casefold() != expected_update_memory_id.casefold()
|
|
709
|
+
)
|
|
710
|
+
):
|
|
711
|
+
raise ModelOutputError(
|
|
712
|
+
"summary update target differs from gate target",
|
|
713
|
+
validation_detail="invalid_update_target",
|
|
714
|
+
)
|
|
638
715
|
if related_memory_ids is not None:
|
|
639
716
|
related_ids = {
|
|
640
717
|
memory_id.casefold()
|
|
@@ -689,6 +766,9 @@ def parse_summarize_output(
|
|
|
689
766
|
related_native_ids: Iterable[Any] | None = None,
|
|
690
767
|
related_memory_ids: Iterable[Any] | None = None,
|
|
691
768
|
scope_registry: Mapping[str, Any] | None = None,
|
|
769
|
+
expected_type: str | None = None,
|
|
770
|
+
expected_update_memory_id: str | None = None,
|
|
771
|
+
expected_target_type: str | None = None,
|
|
692
772
|
) -> dict[str, Any]:
|
|
693
773
|
try:
|
|
694
774
|
parsed = parse_strict_json(raw)
|
|
@@ -703,6 +783,9 @@ def parse_summarize_output(
|
|
|
703
783
|
related_native_ids=related_native_ids,
|
|
704
784
|
related_memory_ids=related_memory_ids,
|
|
705
785
|
scope_registry=scope_registry,
|
|
786
|
+
expected_type=expected_type,
|
|
787
|
+
expected_update_memory_id=expected_update_memory_id,
|
|
788
|
+
expected_target_type=expected_target_type,
|
|
706
789
|
)
|
|
707
790
|
except ModelOutputError as error:
|
|
708
791
|
if error.validation_reason is None:
|