Skip to content

Commit 936b304

Browse files
committed
feat(memory): finalize production memory workflows and onboarding guidance
1 parent b4e774b commit 936b304

46 files changed

Lines changed: 5616 additions & 733 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ __marimo__/
218218
.mcp.json
219219
.scratch/
220220
.workflows/
221+
.worktrees/
221222
ARCHITECTURE.md
222223
CLAUDE.md
223224

README.md

Lines changed: 173 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,17 @@ execute_workflow(workflow="python-ci-pipeline", inputs={...}, mode="async")
104104

105105
| Tool | When to call | Typical call pattern |
106106
| --- | --- | --- |
107-
| `list_workflows` | First step in most sessions | `list_workflows(tags=[], format="json")` |
108-
| `get_workflow_info` | Before execution to confirm inputs/outputs | `get_workflow_info(workflow="name", format="json")` |
107+
| `list_workflows` | List registered workflows | `list_workflows(tags=[], format="json")` |
108+
| `get_workflow_info` | Exploratory to confirm inputs/outputs | `get_workflow_info(workflow="name", format="json")` |
109109
| `execute_workflow` | Run a registered workflow | `execute_workflow(workflow="name", inputs={...}, mode="sync")` |
110110
| `execute_inline_workflow` | Test one-off YAML without registering | `execute_inline_workflow(workflow_yaml="...", inputs={...})` |
111-
| `reload_workflows` | After editing YAML files on disk | `reload_workflows()` |
111+
| `reload_workflows` | After editing workflow YAML files on disk | `reload_workflows()` |
112112

113113
### Authoring and validation
114114

115115
| Tool | When to call | Typical call pattern |
116116
| --- | --- | --- |
117-
| `get_workflow_schema` | Need full JSON schema for authoring | `get_workflow_schema()` |
117+
| `get_workflow_schema` | Debugging only. Retrieve full JSON schema for authoring | `get_workflow_schema()` |
118118
| `validate_workflow_yaml` | Validate YAML before execution | `validate_workflow_yaml(yaml_content="...")` |
119119

120120
### Async, queue, and interactive control
@@ -132,34 +132,165 @@ execute_workflow(workflow="python-ci-pipeline", inputs={...}, mode="async")
132132
| Tool | When to call | Typical call pattern |
133133
| --- | --- | --- |
134134
| `memory` | Unified memory query/ingest/maintenance/graph operations | `memory(operation="query", scope={...}, query={...})` |
135+
| `project_onboard` | Current memory onboarding flow with checkpoints | `project_onboard(scope={...}, ingest={...}, max_operations=1)` |
136+
| `project_sync` | Current memory checkpoint resume/continuation | `project_sync(checkpoint={...}, max_operations=3)` |
135137

136-
The `memory` tool is registered only when memory DB setup is available and valid at startup.
138+
IMPORTANT: The `memory` tool is registered only when memory DB setup is available and valid at startup (see [below](#memory))
137139

138140
Memory contract highlights:
139141

140142
- Unified envelope: `operation` + optional `scope/query/record/graph/maintenance/response`.
143+
- Current memory taxonomy: `scope` accepts only `palace`, `wing`, `room`, `compartment`.
144+
- Context activation and scope defaulting:
145+
- Resolution precedence is `scope``scope_token``context_id` (per-field merge).
146+
- `scope_token` resolves from execution context `memory_scope_tokens`; `context_id` resolves from `memory_context_scopes`.
147+
- Responses include `resolved_scope` and `scope_source` when available.
148+
- Required scope by operation:
149+
- `query`: all four fields must resolve.
150+
- `ingest`: all four fields must resolve (including `compartment`).
151+
- `graph_upsert` with `graph.kind="place"`: all four fields must resolve.
152+
- `validate|supersede|archive|maintain|graph_delete|graph_upsert(kind="link")`: scope is optional.
141153
- Temporal query semantics:
142154
- `operation="query"` supports either `query.as_of` OR interval `query.from/query.to` (mutually exclusive).
143155
- `query.mode="graph"` supports `query.as_of` only; `query.from/query.to` are rejected.
144156
- `operation="ingest"` with `record.format="raw"` supports `record.valid_from` and `record.valid_to` with ordering validation (`valid_from <= valid_to`).
145157
- Strict validation: unknown/extra fields are rejected.
158+
- Direct vs derived boundaries:
159+
- `operation="ingest"` is direct-only (`record.memory_tier` must be `direct`).
160+
- Category governance for ingest is explicit:
161+
- Unknown `record.categories` fail deterministically with `MEM_UNKNOWN_CATEGORY` when `record.allow_create_categories=false` (default behavior).
162+
- Setting `record.allow_create_categories=true` opts into category creation and allows ingest to proceed when categories are otherwise valid.
163+
- Derived/community artifacts are produced by maintenance flows (for example `maintenance.mode="community_refresh"`).
164+
- `query.mode="communities"` uses the dedicated communities strategy.
146165
- Lifecycle semantics:
147166
- Archived records are excluded by default query behavior.
148167
- `operation="supersede"` requires `record.superseded_by`.
149168
- `operation="archive"` maps to forget semantics; repeating archive on already archived records is safe/idempotent.
150169
- Graph semantics:
151170
- `operation="graph_upsert"` with `graph.kind="link"` is idempotent.
152-
- `operation="graph_delete"` with `graph.kind="place"` includes cascaded `deleted_relation_count` in the response.
171+
- `operation="graph_delete"` returns compact delete counters (`deleted_places` for `kind="place"`, `deleted_links` for `kind="link"`); optional debug output adds diagnostics metadata.
172+
173+
Validation note: this ingest category behavior (`MEM_UNKNOWN_CATEGORY` with create disabled, successful ingest with `allow_create_categories=true`) was live-validated on 2026-04-21 via production-like direct MCP `memory` calls.
153174

154175
Direct-call JSON examples:
155176

156-
Valid (point-in-time query):
177+
`query`:
157178

158179
```json
159180
{
160181
"operation": "query",
161-
"scope": {"wing": "workflows", "room": "memory-engine"},
162-
"query": {"mode": "search", "text": "schema epoch", "as_of": "2026-04-20T00:00:00Z"}
182+
"scope": {"palace": "acme", "wing": "workflows", "room": "memory-engine", "compartment": "contract-r2"},
183+
"query": {"mode": "search", "text": "schema epoch", "as_of": "2026-04-20T00:00:00Z", "radius": 1}
184+
}
185+
```
186+
187+
`ingest`:
188+
189+
```json
190+
{
191+
"operation": "ingest",
192+
"scope": {"palace": "acme", "wing": "workflows", "room": "memory-engine", "compartment": "contract-r2"},
193+
"record": {"format": "raw", "content": "Memory active", "memory_tier": "direct"}
194+
}
195+
```
196+
197+
`validate`:
198+
199+
```json
200+
{
201+
"operation": "validate",
202+
"record": {"ids": ["11111111-1111-1111-1111-111111111111"]}
203+
}
204+
```
205+
206+
`supersede`:
207+
208+
```json
209+
{
210+
"operation": "supersede",
211+
"record": {
212+
"ids": ["11111111-1111-1111-1111-111111111111"],
213+
"superseded_by": "22222222-2222-2222-2222-222222222222"
214+
}
215+
}
216+
```
217+
218+
`archive`:
219+
220+
```json
221+
{
222+
"operation": "archive",
223+
"record": {"ids": ["11111111-1111-1111-1111-111111111111"]}
224+
}
225+
```
226+
227+
`maintain`:
228+
229+
```json
230+
{
231+
"operation": "maintain",
232+
"maintenance": {"mode": "community_refresh"}
233+
}
234+
```
235+
236+
`graph_upsert` (`kind=place`):
237+
238+
```json
239+
{
240+
"operation": "graph_upsert",
241+
"scope": {"palace": "acme", "wing": "workflows", "room": "memory-engine", "compartment": "contract-r2"},
242+
"graph": {"kind": "place", "place_name": "Memory API (current)", "place_type": "feature"}
243+
}
244+
```
245+
246+
`graph_upsert` (`kind=link`):
247+
248+
```json
249+
{
250+
"operation": "graph_upsert",
251+
"graph": {
252+
"kind": "link",
253+
"from": "11111111-1111-1111-1111-111111111111",
254+
"to": "22222222-2222-2222-2222-222222222222",
255+
"link_type": "depends_on"
256+
}
257+
}
258+
```
259+
260+
`graph_delete`:
261+
262+
```json
263+
{
264+
"operation": "graph_delete",
265+
"graph": {"kind": "place", "ids": ["33333333-3333-3333-3333-333333333333"]}
266+
}
267+
```
268+
269+
`project_onboard`:
270+
271+
```json
272+
{
273+
"scope": {"palace": "acme", "wing": "workflows", "room": "memory-engine", "compartment": "contract-r2"},
274+
"ingest": {"format": "raw", "content": "Initial baseline", "memory_tier": "direct"},
275+
"supersede": {"ids": ["11111111-1111-1111-1111-111111111111"], "superseded_by": "22222222-2222-2222-2222-222222222222"},
276+
"archive": {"ids": ["33333333-3333-3333-3333-333333333333"]},
277+
"maintain": {"mode": "community_refresh"},
278+
"max_operations": 1
279+
}
280+
```
281+
282+
`project_sync`:
283+
284+
```json
285+
{
286+
"checkpoint": {
287+
"version": "oss-r2",
288+
"scope": {"palace": "acme", "wing": "workflows", "room": "memory-engine", "compartment": "contract-r2"},
289+
"plan": [{"operation": "ingest", "payload": {"format": "raw", "content": "Initial baseline", "memory_tier": "direct"}}],
290+
"next_index": 0,
291+
"completed": []
292+
},
293+
"max_operations": 3
163294
}
164295
```
165296

@@ -168,6 +299,7 @@ Invalid (mutually exclusive temporal filters):
168299
```json
169300
{
170301
"operation": "query",
302+
"scope": {"palace": "acme", "wing": "workflows", "room": "memory-engine", "compartment": "contract-r2"},
171303
"query": {
172304
"mode": "search",
173305
"text": "maintenance",
@@ -183,6 +315,7 @@ Invalid (graph query rejects interval filters):
183315
```json
184316
{
185317
"operation": "query",
318+
"scope": {"palace": "acme", "wing": "workflows", "room": "memory-engine", "compartment": "contract-r2"},
186319
"query": {
187320
"mode": "graph",
188321
"text": "service graph",
@@ -192,6 +325,16 @@ Invalid (graph query rejects interval filters):
192325
}
193326
```
194327

328+
Invalid (legacy taxonomy key):
329+
330+
```json
331+
{
332+
"operation": "query",
333+
"scope": {"palace": "acme", "wing": "svc", "room": "component", "hall": "legacy"},
334+
"query": {"text": "find this", "mode": "search"}
335+
}
336+
```
337+
195338
Invalid (supersede missing required `superseded_by`):
196339

197340
```json
@@ -233,36 +376,43 @@ Memory is an optional persistent storage feature that lets agents and workflows
233376
### What memory provides
234377

235378
- **`memory` MCP tool** — direct call interface for LLM agents to store and query information without writing workflow YAML.
379+
- **`project_onboard` / `project_sync` MCP tools** — Current memory contract helpers for checkpointed onboarding/sync sequences.
236380
- **`Memory` workflow block** — use inside YAML workflows to automate memory operations as part of larger pipelines.
237-
- **Three-level memory palace scoping** (`wing``room``hall`) — a strict containment hierarchy: `wing` is a service/project, `room` is a component/module inside that wing, and `hall` is a topic lane inside that room. All three levels are optional and independently filterable. Providing only `wing` returns everything in that wing; adding `room` or `hall` narrows the scope further. `hall` is a sub-partition of a room, not a connection between rooms — cross-room recall is preserved separately by the global companion lane in `auto` queries.
381+
- **Memory topology scoping** (`palace``wing``room``compartment`) — current memory scope keys are strict and legacy keys (for example `hall`) are rejected. Scope can be supplied directly and/or resolved from context (`scope_token`, `context_id`) using deterministic precedence.
238382
- **Temporal tracking** — records carry `valid_from` / `valid_to` timestamps supporting point-in-time and interval queries.
239383
- **Knowledge graph** — link memories to places, entities, or concepts and query the resulting graph.
240384
- **Lifecycle management** — archive or supersede records without deletion; archived records are excluded from default queries.
241385

242386
### Retrieval strategies
243387

244-
Two strategies are available via the `memory` tool. The correct one is selected automatically based on the call:
388+
Current memory behavior exposes query modes that map to retrieval strategies internally:
245389

246-
| Strategy | When it triggers | Scope behavior |
390+
| Query mode / trigger | Effective strategy | Behavior |
247391
| --- | --- | --- |
248-
| `auto` | Default for most queries (any `radius ≥ 1` or unscoped) | Runs a scoped lane (filtered by `wing`/`room`/`hall`) **plus** a global companion lane of up to 20 items; results are fused via RRF. Cross-scope recall is preserved. |
249-
| `palace` | When `radius == 0` **and** at least one scope field is set | Strict scoped retrieval only — no global companion lane. Requires at least `wing` or `room` to be set; returns an error if both are absent. Use when you want hard isolation. |
392+
| `query.mode="search"` + `radius=0` | `palace` | Strict scoped retrieval lane (no companion lane). |
393+
| `query.mode="search"` + `radius>=1` | `auto` | Scoped lane + optional S2 companion lane (`s2_enabled=true` by default). |
394+
| `query.mode="hybrid"` | `auto` | Same retrieval family as `auto` with fused ranking. |
395+
| `query.mode="context"` | `context` | Context assembly retrieval path. |
396+
| `query.mode="graph"` | `graph` | Graph traversal/path/stats retrieval. |
397+
| `query.mode="communities"` | `communities` | Community-focused retrieval strategy. |
250398

251-
`graph` mode (`query.mode="graph"`) bypasses scope filtering and traverses the entity graph by ID.
399+
Every `query` call still requires a fully resolved current memory scope (`palace/wing/room/compartment`) resolved from request and/or context sources.
252400

253-
#### Scope call shape
401+
#### Scope call shape (with context activation)
254402

255-
All three scope levels are passed under the `scope` key:
403+
Scope fields can be passed directly and/or resolved from `scope_token` / `context_id`:
256404

257405
```json
258406
{
259407
"operation": "query",
260-
"scope": {"wing": "my-service", "room": "auth", "hall": "tokens"},
261-
"query": {"text": "refresh token lifetime"}
408+
"scope": {"palace": "acme", "wing": "my-service"},
409+
"scope_token": "st_auth",
410+
"context_id": "ctx_default",
411+
"query": {"text": "refresh token lifetime", "mode": "context"}
262412
}
263413
```
264414

265-
Any of the three fields can be omitted. Filters that are present are applied with `AND` semantics.
415+
Resolution precedence is `scope``scope_token``context_id` for each field.
266416

267417
### Prerequisites
268418

@@ -316,8 +466,8 @@ On first boot with `MEMORY_DB_AUTO_CREATE=true` (the default), the server create
316466
```json
317467
{
318468
"operation": "ingest",
319-
"scope": {"wing": "test", "room": "setup"},
320-
"record": {"format": "raw", "content": "Memory is working."}
469+
"scope": {"palace": "acme", "wing": "test", "room": "setup", "compartment": "smoke"},
470+
"record": {"format": "raw", "content": "Memory is working.", "memory_tier": "direct"}
321471
}
322472
```
323473

@@ -349,6 +499,7 @@ Example block families include `Shell`, `ReadFiles`, `HttpCall`, `LLMCall`, `Sql
349499
## Documentation map
350500

351501
- `README.md`: install, usage, and tool catalog.
502+
- `docs/guides/memory-tools-cheatsheet.md`: Current memory contract quick reference, detailed guide, and examples.
352503
- `docs/llm/block-reference.md`: exact block inputs/outputs for workflow authoring.
353504
- `docs/TESTING.md`: test strategy and test commands.
354505
- `ARCHITECTURE.md`: architecture overview.

benchmarks/workflows_bench_common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ async def ingest_corpus(
207207
item_rows.append((item_id, source_id, item_path, corpus_id))
208208
memory_rows.append(
209209
(
210-
memory_id,
210+
memory_id,
211211
item_id,
212212
text,
213213
str(vector),

benchmarks/workflows_locomo_bench.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def build_corpus_from_sessions(
7878
for dialog in session["dialogs"]:
7979
speaker = dialog.get("speaker", "?")
8080
text = dialog.get("text", "")
81-
texts.append(f"{speaker} said, \"{text}\"")
81+
texts.append(f'{speaker} said, "{text}"')
8282
corpus.append("\n".join(texts))
8383
corpus_ids.append(f"session_{session['session_num']}")
8484
corpus_timestamps.append(str(session["date"]))
@@ -88,7 +88,7 @@ def build_corpus_from_sessions(
8888
dialog_id = str(dialog.get("dia_id", f"D{session['session_num']}:?"))
8989
speaker = dialog.get("speaker", "?")
9090
text = dialog.get("text", "")
91-
corpus.append(f"{speaker} said, \"{text}\"")
91+
corpus.append(f'{speaker} said, "{text}"')
9292
corpus_ids.append(dialog_id)
9393
corpus_timestamps.append(str(session["date"]))
9494

@@ -169,10 +169,7 @@ async def run_benchmark(args: argparse.Namespace) -> None:
169169
)
170170

171171
if not corpus:
172-
print(
173-
f"[{conversation_index:2}/{len(data)}] {sample_id:<24} "
174-
"SKIP (empty corpus)"
175-
)
172+
print(f"[{conversation_index:2}/{len(data)}] {sample_id:<24} SKIP (empty corpus)")
176173
continue
177174

178175
source_name = f"{args.source_prefix}:{sample_id}"

benchmarks/workflows_locomo_pooled_bench.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
purge_benchmark_sources,
4848
run_hybrid_search,
4949
)
50+
5051
from workflows_mcp.engine.knowledge.constants import Authority, LifecycleState
5152
from workflows_mcp.engine.knowledge.search import room_scoped_search
5253

@@ -186,7 +187,7 @@ async def _ingest_conversation_with_room(
186187
item_rows.append((item_id, source_id, item_path, pid))
187188
memory_rows.append(
188189
(
189-
memory_id,
190+
memory_id,
190191
item_id,
191192
text,
192193
str(vector),

benchmarks/workflows_memory_multiservice_bench.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,7 @@ async def run_benchmark(args: argparse.Namespace) -> dict[str, Any]:
373373
"Observed latency spike mitigation and rollback checklist."
374374
)
375375
path = (
376-
f"{scope.wing}/{scope.room}/{scope.hall}/"
377-
f"rolling-{dynamic_ingest_index:06d}.md"
376+
f"{scope.wing}/{scope.room}/{scope.hall}/rolling-{dynamic_ingest_index:06d}.md"
378377
)
379378
record = [(scope, content, path)]
380379
emb = encoder.encode([content])

0 commit comments

Comments
 (0)