lisa-pack-memory 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. lisa_pack_memory-0.1.0/.gitignore +33 -0
  2. lisa_pack_memory-0.1.0/PKG-INFO +9 -0
  3. lisa_pack_memory-0.1.0/pyproject.toml +30 -0
  4. lisa_pack_memory-0.1.0/src/sdk/packs/memory/__init__.py +14 -0
  5. lisa_pack_memory-0.1.0/src/sdk/packs/memory/mcp_servers.yaml +52 -0
  6. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/__init__.py +1 -0
  7. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/consolidate_semantic.py +375 -0
  8. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/consolidate_semantic.yml +9 -0
  9. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/dispute.py +263 -0
  10. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/dispute.yml +10 -0
  11. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/mine_thread_segment.py +381 -0
  12. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/mine_thread_segment.yml +9 -0
  13. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/prompts/consolidate_semantic_system.md +10 -0
  14. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/prompts/mine_thread_segment_system.md +26 -0
  15. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/prompts/synthesize_learning_system.md +18 -0
  16. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/recall.py +210 -0
  17. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/recall.yml +10 -0
  18. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/synthesize_learning.py +430 -0
  19. lisa_pack_memory-0.1.0/src/sdk/packs/memory/operations/memory/synthesize_learning.yml +13 -0
  20. lisa_pack_memory-0.1.0/src/sdk/packs/memory/slice_provider.py +715 -0
  21. lisa_pack_memory-0.1.0/src/sdk/packs/memory/utils/__init__.py +1 -0
  22. lisa_pack_memory-0.1.0/src/sdk/packs/memory/utils/memory_recall.py +140 -0
  23. lisa_pack_memory-0.1.0/tests/test_consolidate_semantic.py +223 -0
  24. lisa_pack_memory-0.1.0/tests/test_consolidate_semantic_branches.py +495 -0
  25. lisa_pack_memory-0.1.0/tests/test_dispute_memory.py +433 -0
  26. lisa_pack_memory-0.1.0/tests/test_distill_parse.py +73 -0
  27. lisa_pack_memory-0.1.0/tests/test_distill_retry.py +180 -0
  28. lisa_pack_memory-0.1.0/tests/test_memory_recall.py +370 -0
  29. lisa_pack_memory-0.1.0/tests/test_mine_thread_segment.py +120 -0
  30. lisa_pack_memory-0.1.0/tests/test_recall_branches.py +310 -0
  31. lisa_pack_memory-0.1.0/tests/test_slice_provider.py +403 -0
  32. lisa_pack_memory-0.1.0/tests/test_synthesize_learning_branches.py +437 -0
  33. lisa_pack_memory-0.1.0/tests/test_synthesize_learning_tier.py +101 -0
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+
9
+
10
+ # Virtual environments
11
+ .venv/
12
+
13
+ # IDE
14
+ .vscode/
15
+
16
+ # OS
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Testing
21
+ .import_linter_cache/
22
+ .pytest_cache/
23
+ .ruff_cache/
24
+ .mypy_cache/
25
+ .lisa-tmp/
26
+ coverage/
27
+ .coverage
28
+ .coverage.*
29
+ htmlcov/
30
+
31
+ # Outputs
32
+ artifacts/
33
+ .e2e/
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.5
2
+ Name: lisa-pack-memory
3
+ Version: 0.1.0
4
+ Summary: Lisa memory pack - memory-backed default and remember workflows
5
+ Requires-Python: >=3.11
6
+ Provides-Extra: dev
7
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
8
+ Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
9
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.targets.wheel]
6
+ packages = ["src/sdk"]
7
+ only-include = ["src/sdk/packs/memory"]
8
+
9
+ [project]
10
+ name = "lisa-pack-memory"
11
+ version = "0.1.0"
12
+ description = "Lisa memory pack - memory-backed default and remember workflows"
13
+ requires-python = ">=3.11"
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "pytest>=8.0.0",
18
+ "pytest-asyncio>=0.23.0",
19
+ "pytest-mock>=3.14.0",
20
+ ]
21
+
22
+ [tool.pytest.ini_options]
23
+ pythonpath = ["src"]
24
+ testpaths = ["tests"]
25
+
26
+ [project.entry-points."lisa.extensions.content_roots"]
27
+ memory = "sdk.packs.memory:get_content_root"
28
+
29
+ [project.entry-points."lisa.extensions.slice_providers"]
30
+ memory_consolidation = "sdk.packs.memory.slice_provider:get_slice_provider_factory"
@@ -0,0 +1,14 @@
1
+ """Memory pack for Lisa.
2
+
3
+ Adds memory-backed conversational behavior and explicit remember-mode workflows
4
+ without requiring changes to core-pack content.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+
12
+ def get_content_root() -> Path:
13
+ """Return the content root directory for this pack."""
14
+ return Path(__file__).parent
@@ -0,0 +1,52 @@
1
+ ---
2
+ version: 1
3
+
4
+ # Memory provides the bundled memory fallback (lisa_memory).
5
+ # External providers (such as Mem0) are contributed by separate packs and can
6
+ # replace this default through host provider-selection policy.
7
+ memory:
8
+ default_backend: lisa_memory
9
+ backends:
10
+ - id: lisa_memory
11
+ kind: stdio
12
+ command: "${LISA_MCP_COMMAND:lisa-memory}"
13
+ args:
14
+ - --data-dir
15
+ - "${LISA_DATA_DIR}"
16
+ tool_names:
17
+ add: add_memory
18
+ search: search_memory
19
+ list: list_memories
20
+ get: get_memory
21
+ delete: delete_memory
22
+ retire: retire_memory
23
+ count: count_memories
24
+ servers:
25
+ - server_id: lisa-memory
26
+ runtime_kind: venv_bin
27
+ entry_point: lisa-memory
28
+ venv_dir: "${FRAMEWORK_ROOT}"
29
+ warm: true
30
+ required: true
31
+ # tool_policy hides raw native tools from the model-facing catalog.
32
+ # The model calls the generic aliases (memory.add / memory.search /
33
+ # memory.list) instead. Internal dispatch (alias forwarding,
34
+ # operations) still reaches these tools — the per-server index is
35
+ # NOT filtered, only the model catalog is.
36
+ tool_policy:
37
+ denied_tools:
38
+ - get_memory
39
+ - delete_memory
40
+ - list_memories
41
+ - retire_memory
42
+ - count_memories
43
+ # proxy_policy strips the ambient identity field (user_slug) from the
44
+ # native tools' model-facing schema and injects it from the host
45
+ # runtime at dispatch time. The model never sees or sets user_slug —
46
+ # the ambient resolver registered by register_memory_aliases supplies
47
+ # the active user. Without this, the native add_memory / search_memory
48
+ # tools would surface user_slug as a settable parameter and the model
49
+ # would be forced to invent one (it has no source of truth).
50
+ proxy_policy:
51
+ ambient:
52
+ user_slug: user_slug
@@ -0,0 +1 @@
1
+ """Memory operations for the memory pack."""
@@ -0,0 +1,375 @@
1
+ """Consolidate a cluster of episodic memories into one durable semantic record.
2
+
3
+ The memory-consolidation slice (``sdk/packs/memory/slice_provider.py``)
4
+ assembles a batch of related episodic records and invokes this operation
5
+ via ``turn_hooks.execute_operation``. This operation distills the batch
6
+ into a single self-contained semantic fact using the same structured-output
7
+ loop as ``synthesize_learning``, writes the semantic record via
8
+ ``memory.add`` with ``memory_kind="semantic"``, a boosted salience, and
9
+ ``supersedes_id``/``item_ids`` linking it to its sources, then retires
10
+ each episodic source via ``memory.retire`` so the default
11
+ ``status="active"`` recall pool stops surfacing the originals.
12
+
13
+ Reuses the ``synthesize_learning`` distillation seam (``ctx.host.complete``
14
+ + ``json_only_retry_messages`` + ``parse_structured_payload``) and the
15
+ authored-prompt loader (no inline fallbacks per the pack-content rule).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from dataclasses import dataclass
22
+ from typing import Any, cast
23
+
24
+ from kernel.models import OperationExecutionContext, OperationResult, ToolExecutionContext
25
+ from kernel.operations.structured_output import (
26
+ completion_content,
27
+ json_only_retry_messages,
28
+ parse_structured_payload,
29
+ )
30
+ from pydantic import BaseModel, Field, ValidationError
31
+ from sdk.content import require_authored_text
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Authored-text path — loaded via ctx.host.read_authored_text (SDK loader).
36
+ _SYSTEM_PROMPT_PATH = "operations/memory/prompts/consolidate_semantic_system.md"
37
+
38
+ # Fallback default for the consolidation LLM token budget. The authoritative
39
+ # source is the session manifest's
40
+ # ``resources.packs.configuration.lisa-pack-memory`` block, surfaced to
41
+ # the op at runtime as ``ctx.pack_config``.
42
+ _CONSOLIDATE_MAX_TOKENS = 768
43
+
44
+ #: Max chars of the summary shown in the user-facing output preview.
45
+ _PREVIEW_CHARS = 80
46
+ #: Max retired ids shown inline in the output before switching to "+N more".
47
+ _PREVIEW_IDS = 8
48
+ #: Salience floor and boost for the consolidated semantic record.
49
+ _SEMANTIC_SALIENCE_FLOOR = 0.8
50
+ _SEMANTIC_SALIENCE_BOOST = 0.1
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class _ResolvedSettings:
55
+ """Resolved operation budgets: ``ctx.pack_config`` overrides module fallbacks."""
56
+
57
+ consolidate_max_tokens: int
58
+
59
+
60
+ def _cfg_int(pack_config: Any, key: str, default: int) -> int:
61
+ """Read an int from ``ctx.pack_config`` with a module-constant fallback."""
62
+ if pack_config is None:
63
+ return default
64
+ raw = pack_config.get(key)
65
+ if raw is None or raw == "":
66
+ return default
67
+ try:
68
+ return int(raw)
69
+ except (TypeError, ValueError):
70
+ logger.warning(
71
+ "consolidate_semantic: pack_config[%r]=%r is not an int; using default %d",
72
+ key,
73
+ raw,
74
+ default,
75
+ )
76
+ return default
77
+
78
+
79
+ def _resolve_settings(ctx: OperationExecutionContext) -> _ResolvedSettings:
80
+ """Resolve operation budgets from ``ctx.pack_config`` with fallbacks."""
81
+ return _ResolvedSettings(
82
+ consolidate_max_tokens=_cfg_int(
83
+ ctx.pack_config, "memory_consolidate_max_tokens", _CONSOLIDATE_MAX_TOKENS
84
+ ),
85
+ )
86
+
87
+
88
+ def _load_system_prompt(ctx: OperationExecutionContext) -> str:
89
+ """Load the consolidation system prompt via the SDK content-registry loader.
90
+
91
+ Fails loudly when the loader is not wired, returns empty/None, or raises.
92
+ Per the pack-content rule: no fallbacks to missing content.
93
+ """
94
+ return require_authored_text(
95
+ ctx.host.read_authored_text if ctx.host is not None else None,
96
+ _SYSTEM_PROMPT_PATH,
97
+ context="consolidate_semantic operation",
98
+ )
99
+
100
+
101
+ class Parameters(BaseModel):
102
+ """User-/slice-supplied inputs for the consolidation operation."""
103
+
104
+ episodic_records: list[dict[str, Any]] = Field(
105
+ ...,
106
+ description="The episodic memory records to consolidate, each with "
107
+ "at least id, content, and created_at.",
108
+ min_length=1,
109
+ )
110
+ memory_scope: str = Field("personal", description="Privacy scope for the semantic record.")
111
+ surface_id: str = Field("", description="Surface isolation key.")
112
+ user_slug: str = Field("default", description="The user identity to scope to.")
113
+ workspace_id: str = Field("default", description="Workspace isolation key.")
114
+
115
+
116
+ class ConsolidatedSemantic(BaseModel):
117
+ """Strict bounded output from the consolidation LLM call."""
118
+
119
+ summary: str = Field(
120
+ ...,
121
+ description="One concise, self-contained semantic sentence preserving "
122
+ "the durable knowledge across the cluster.",
123
+ min_length=1,
124
+ max_length=500,
125
+ )
126
+ confidence: float = Field(
127
+ 0.8,
128
+ description="Confidence in the consolidated summary (0.0-1.0).",
129
+ ge=0.0,
130
+ le=1.0,
131
+ )
132
+ item_ids: list[int] = Field(
133
+ ...,
134
+ description="The input record ids the summary was derived from.",
135
+ min_length=1,
136
+ )
137
+
138
+
139
+ def _render_batch(records: list[dict[str, Any]]) -> str:
140
+ """Render the episodic batch as a numbered list for the LLM user message."""
141
+ lines: list[str] = []
142
+ for idx, rec in enumerate(records, start=1):
143
+ rid = rec.get("id", "?")
144
+ content = str(rec.get("content", "")).strip()
145
+ created = str(rec.get("created_at", "")).strip()
146
+ lines.append(f"{idx}. [id={rid}, created={created}] {content}")
147
+ return "\n".join(lines)
148
+
149
+
150
+ def _parse_consolidated(content: str) -> ConsolidatedSemantic:
151
+ """Parse structured output with a guard against JSON-fragment fallbacks."""
152
+ normalized = (content or "").strip()
153
+ if not normalized:
154
+ raise ValueError("empty consolidation output")
155
+ try:
156
+ return cast(
157
+ ConsolidatedSemantic, parse_structured_payload(ConsolidatedSemantic, normalized)
158
+ )
159
+ except ValidationError as exc:
160
+ # Mirror synthesize_learning's guard: if the model emitted JSON that
161
+ # failed validation, refuse to fall back to prose parsing — surface
162
+ # the failure instead of persisting garbage.
163
+ if _looks_like_json(normalized):
164
+ raise RuntimeError(
165
+ "consolidate_semantic: model returned JSON-shaped output that "
166
+ f"failed validation; refusing to fall back. Raw: {normalized[:200]!r}"
167
+ ) from exc
168
+ raise
169
+
170
+
171
+ def _looks_like_json(text: str) -> bool:
172
+ """Return True when the response looks like JSON (fenced or bare object)."""
173
+ stripped = text.strip()
174
+ if not stripped:
175
+ return False
176
+ if stripped.startswith("```") or stripped.startswith("{") or stripped.startswith("["):
177
+ return True
178
+ return bool("{" in stripped and "}" in stripped)
179
+
180
+
181
+ # pylint: disable=protected-access — reusing the cast helper pattern from synthesize_learning
182
+
183
+
184
+ async def _distill_batch(
185
+ ctx: OperationExecutionContext, system_prompt: str, batch_text: str, max_tokens: int
186
+ ) -> ConsolidatedSemantic:
187
+ """Run the 2-attempt LLM distillation loop over the episodic batch."""
188
+ model_id = ctx.preferred_model_id
189
+ model_def = ctx.host.resolve_model(model_id) if ctx.host is not None else None
190
+ if model_def is None and ctx.host is not None:
191
+ model_def = ctx.host.resolve_model("default")
192
+ if model_def is None:
193
+ raise RuntimeError("consolidate_semantic could not resolve a model")
194
+ first_messages: list[dict[str, Any]] = [
195
+ {"role": "system", "content": system_prompt},
196
+ {"role": "user", "content": batch_text},
197
+ ]
198
+ last_exc: Exception | None = None
199
+ for attempt_idx, attempt_messages in enumerate(
200
+ (first_messages, json_only_retry_messages(first_messages, ConsolidatedSemantic))
201
+ ):
202
+ try:
203
+ raw = await ctx.host.complete( # type: ignore[union-attr]
204
+ messages=attempt_messages,
205
+ model_def=model_def,
206
+ max_tokens=max_tokens,
207
+ temperature=0.1,
208
+ response_format=ConsolidatedSemantic if attempt_idx == 0 else None,
209
+ )
210
+ return _parse_consolidated(completion_content(raw))
211
+ except Exception as exc: # pylint: disable=broad-exception-caught
212
+ last_exc = exc
213
+ if attempt_idx == 0:
214
+ continue
215
+ break
216
+ raise RuntimeError(f"consolidate_semantic distillation failed: {last_exc}") from last_exc
217
+
218
+
219
+ async def _persist_semantic(
220
+ ctx: OperationExecutionContext,
221
+ parsed: Parameters,
222
+ consolidated: ConsolidatedSemantic,
223
+ item_ids: list[int],
224
+ source_count: int,
225
+ ) -> int | str | None:
226
+ """Write the consolidated semantic record via ``memory.add`` and return its id."""
227
+ assert ctx.host is not None
228
+ max_source_salience = max(
229
+ (float(rec.get("salience", 0.0)) for rec in parsed.episodic_records), default=0.0
230
+ )
231
+ boosted_salience = max(
232
+ _SEMANTIC_SALIENCE_FLOOR, min(1.0, max_source_salience + _SEMANTIC_SALIENCE_BOOST)
233
+ )
234
+ supersedes_id = item_ids[0] if item_ids else None
235
+ add_result = await ctx.host.execute_tool(
236
+ "memory.add",
237
+ ToolExecutionContext(
238
+ tool_id="memory.add",
239
+ workspace_id=parsed.workspace_id,
240
+ user_id=ctx.user_id,
241
+ user_slug=parsed.user_slug,
242
+ surface_id=parsed.surface_id,
243
+ memory_scope=parsed.memory_scope, # type: ignore[arg-type]
244
+ mode_id=ctx.mode_id,
245
+ parameters={
246
+ "content": consolidated.summary,
247
+ "user_slug": parsed.user_slug,
248
+ "workspace_id": parsed.workspace_id,
249
+ "surface_id": parsed.surface_id,
250
+ "memory_scope": parsed.memory_scope,
251
+ "memory_tier": "long_term",
252
+ "memory_kind": "semantic",
253
+ "operation_id": "consolidate_semantic",
254
+ "salience": boosted_salience,
255
+ "supersedes_id": supersedes_id,
256
+ "item_ids": item_ids,
257
+ "confidence": consolidated.confidence,
258
+ "content_json": {
259
+ "consolidated_from": item_ids,
260
+ "source_count": source_count,
261
+ },
262
+ },
263
+ ),
264
+ )
265
+ if getattr(add_result, "error", None):
266
+ raise RuntimeError(f"semantic record storage failed: {add_result.error}")
267
+ add_text = getattr(add_result, "output_text", "") or ""
268
+ if "ID " in add_text:
269
+ try:
270
+ return int(add_text.split("ID ", 1)[1].rstrip("."))
271
+ except (ValueError, IndexError):
272
+ return None
273
+ return None
274
+
275
+
276
+ async def _retire_sources(
277
+ ctx: OperationExecutionContext, parsed: Parameters, item_ids: list[int]
278
+ ) -> list[int]:
279
+ """Retire each episodic source via ``memory.retire``; return the ids retired."""
280
+ assert ctx.host is not None
281
+ retired_ids: list[int] = []
282
+ for eid in item_ids:
283
+ retire_result = await ctx.host.execute_tool(
284
+ "memory.retire",
285
+ ToolExecutionContext(
286
+ tool_id="memory.retire",
287
+ workspace_id=parsed.workspace_id,
288
+ user_id=ctx.user_id,
289
+ user_slug=parsed.user_slug,
290
+ surface_id=parsed.surface_id,
291
+ memory_scope=parsed.memory_scope,
292
+ mode_id=ctx.mode_id,
293
+ parameters={
294
+ "memory_id": eid,
295
+ "user_slug": parsed.user_slug,
296
+ "workspace_id": parsed.workspace_id,
297
+ "surface_id": parsed.surface_id,
298
+ "memory_scope": parsed.memory_scope,
299
+ },
300
+ ),
301
+ )
302
+ if not getattr(retire_result, "error", None):
303
+ retired_ids.append(eid)
304
+ return retired_ids
305
+
306
+
307
+ async def execute(ctx: OperationExecutionContext) -> OperationResult:
308
+ """Distill an episodic batch into one semantic record and retire the sources."""
309
+ params = ctx.params or {}
310
+ try:
311
+ parsed = Parameters.model_validate(params)
312
+ except ValidationError as exc:
313
+ return OperationResult(output_text=f"⛔ Invalid consolidation parameters: {exc}")
314
+
315
+ records = parsed.episodic_records
316
+ if not records:
317
+ return OperationResult(output_text="⛔ No episodic records provided to consolidate.")
318
+
319
+ if ctx.host is None:
320
+ raise RuntimeError("consolidate_semantic requires the OperationHost seam")
321
+
322
+ # Step 1: Distill the batch with the LLM (delegates to _distill_batch).
323
+ system_prompt = _load_system_prompt(ctx)
324
+ batch_text = _render_batch(records)
325
+ consolidate_max_tokens = _resolve_settings(ctx).consolidate_max_tokens
326
+ try:
327
+ consolidated = await _distill_batch(ctx, system_prompt, batch_text, consolidate_max_tokens)
328
+ except Exception as exc: # pylint: disable=broad-except
329
+ raise RuntimeError(f"consolidate_semantic distillation failed: {exc}") from exc
330
+
331
+ # Validate item_ids are a subset of the input ids (guard against
332
+ # the model hallucinating ids that were not in the batch).
333
+ input_ids = {int(rec.get("id")) for rec in records if rec.get("id") is not None}
334
+ item_ids = [eid for eid in consolidated.item_ids if eid in input_ids]
335
+ if not item_ids:
336
+ # Fall back to the full input set if the model returned no valid ids.
337
+ item_ids = sorted(input_ids)
338
+
339
+ # Step 2: Persist the semantic record via memory.add with boosted salience.
340
+ try:
341
+ semantic_id = await _persist_semantic(ctx, parsed, consolidated, item_ids, len(records))
342
+ except RuntimeError as exc:
343
+ return OperationResult(output_text=f"⛔ {exc}")
344
+
345
+ # Step 3: Retire the episodic sources via memory.retire so the default
346
+ # status='active' recall pool stops surfacing them. The originals stay
347
+ # queryable with status=None (audit trail preserved).
348
+ retired_ids = await _retire_sources(ctx, parsed, item_ids)
349
+
350
+ preview = consolidated.summary[:_PREVIEW_CHARS]
351
+ return OperationResult(
352
+ output_text=(
353
+ f"✅ Consolidated {len(records)} episodic records into one semantic "
354
+ f"record (id={semantic_id}); retired {len(retired_ids)} sources: "
355
+ f"{_retired_ids_preview(retired_ids)}"
356
+ f"'{preview}{'...' if len(consolidated.summary) > _PREVIEW_CHARS else ''}'"
357
+ ),
358
+ extra={
359
+ "consolidated": True,
360
+ "semantic_id": semantic_id,
361
+ "retired_ids": retired_ids,
362
+ "item_ids": item_ids,
363
+ "summary": consolidated.summary,
364
+ "confidence": consolidated.confidence,
365
+ },
366
+ )
367
+
368
+
369
+ def _retired_ids_preview(ids: list[int]) -> str:
370
+ """Format the retired-ids list for the user-facing output line."""
371
+ if not ids:
372
+ return ""
373
+ shown = ", ".join(str(i) for i in ids[:_PREVIEW_IDS])
374
+ more = f", +{len(ids) - _PREVIEW_IDS} more" if len(ids) > _PREVIEW_IDS else ""
375
+ return f"[{shown}{more}]. "
@@ -0,0 +1,9 @@
1
+ type: Lisa Operation
2
+ title: Consolidate Episodic Memories into Semantic
3
+ description: Cluster related episodic memory records and distill them into a single durable semantic fact, then retire the originals. Invoked by the memory-consolidation slice; not user-facing by default.
4
+ tags: [memory, consolidation, semantic, reflection]
5
+ status: stable
6
+ generated: { by: human:adam, at: 2026-08-06T00:00:00Z }
7
+ operation_id: consolidate_semantic
8
+ exportable: true
9
+ effect: write