@ai2070/memex 0.9.0 → 0.10.0
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.
- package/API.md +20 -2
- package/README.md +47 -7
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/intent.ts +17 -0
- package/src/query.ts +10 -0
- package/src/task.ts +17 -0
- package/src/transplant.ts +16 -8
- package/src/types.ts +8 -0
- package/tests/bugfix-holes.test.ts +8 -8
- package/tests/cross-graph-fields.test.ts +378 -0
package/API.md
CHANGED
|
@@ -21,6 +21,9 @@ interface MemoryItem {
|
|
|
21
21
|
conviction?: number; // 0..1 -- how sure was the author?
|
|
22
22
|
importance?: number; // 0..1 -- how much attention does this need right now? (salience)
|
|
23
23
|
|
|
24
|
+
intent_id?: string; // intent that produced this item
|
|
25
|
+
task_id?: string; // task that produced this item
|
|
26
|
+
|
|
24
27
|
meta?: {
|
|
25
28
|
agent_id?: string;
|
|
26
29
|
session_id?: string;
|
|
@@ -221,6 +224,11 @@ interface MemoryFilter {
|
|
|
221
224
|
kind?: MemoryKind;
|
|
222
225
|
source_kind?: SourceKind;
|
|
223
226
|
|
|
227
|
+
intent_id?: string; // exact match on intent_id
|
|
228
|
+
intent_ids?: string[]; // match any of these intent_ids
|
|
229
|
+
task_id?: string; // exact match on task_id
|
|
230
|
+
task_ids?: string[]; // match any of these task_ids
|
|
231
|
+
|
|
224
232
|
range?: {
|
|
225
233
|
authority?: { min?: number; max?: number };
|
|
226
234
|
conviction?: { min?: number; max?: number };
|
|
@@ -757,6 +765,7 @@ type IntentStatus = "active" | "paused" | "completed" | "cancelled";
|
|
|
757
765
|
|
|
758
766
|
interface Intent {
|
|
759
767
|
id: string;
|
|
768
|
+
parent_id?: string; // parent intent for sub-intent hierarchies
|
|
760
769
|
label: string;
|
|
761
770
|
description?: string;
|
|
762
771
|
priority: number; // 0..1
|
|
@@ -808,10 +817,13 @@ interface IntentFilter {
|
|
|
808
817
|
statuses?: IntentStatus[];
|
|
809
818
|
min_priority?: number;
|
|
810
819
|
has_memory_id?: string; // intent references this memory item
|
|
820
|
+
parent_id?: string; // filter by parent intent
|
|
821
|
+
is_root?: boolean; // true = no parent, false = has parent
|
|
811
822
|
}
|
|
812
823
|
|
|
813
824
|
getIntents(state, { owner: "user:laz", statuses: ["active", "paused"] });
|
|
814
825
|
getIntentById(state, "i1");
|
|
826
|
+
getChildIntents(state, "i1"); // all intents with parent_id = "i1"
|
|
815
827
|
```
|
|
816
828
|
|
|
817
829
|
---
|
|
@@ -828,6 +840,7 @@ type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
|
|
|
828
840
|
interface Task {
|
|
829
841
|
id: string;
|
|
830
842
|
intent_id: string; // parent intent
|
|
843
|
+
parent_id?: string; // parent task for subtask hierarchies
|
|
831
844
|
action: string; // "search_linkedin", "summarize_case"
|
|
832
845
|
label?: string;
|
|
833
846
|
status: TaskStatus;
|
|
@@ -886,10 +899,13 @@ interface TaskFilter {
|
|
|
886
899
|
min_priority?: number;
|
|
887
900
|
has_input_memory_id?: string;
|
|
888
901
|
has_output_memory_id?: string;
|
|
902
|
+
parent_id?: string; // filter by parent task
|
|
903
|
+
is_root?: boolean; // true = no parent, false = has parent
|
|
889
904
|
}
|
|
890
905
|
|
|
891
906
|
getTasks(state, { intent_id: "i1", statuses: ["pending", "running"] });
|
|
892
907
|
getTaskById(state, "t1");
|
|
908
|
+
getChildTasks(state, "t1"); // all tasks with parent_id = "t1"
|
|
893
909
|
getTasksByIntent(state, "i1");
|
|
894
910
|
```
|
|
895
911
|
|
|
@@ -902,11 +918,13 @@ The three graphs (memory, intent, task) reference each other by ID:
|
|
|
902
918
|
| From | To | Field |
|
|
903
919
|
|------|----|-------|
|
|
904
920
|
| Intent | Memory | `Intent.root_memory_ids` |
|
|
921
|
+
| Intent | Intent (parent) | `Intent.parent_id` |
|
|
905
922
|
| Task | Intent | `Task.intent_id` |
|
|
923
|
+
| Task | Task (parent) | `Task.parent_id` |
|
|
906
924
|
| Task | Memory (input) | `Task.input_memory_ids` |
|
|
907
925
|
| Task | Memory (output) | `Task.output_memory_ids` |
|
|
908
|
-
| Memory | Intent | `MemoryItem.
|
|
909
|
-
| Memory | Task | `MemoryItem.
|
|
926
|
+
| Memory | Intent | `MemoryItem.intent_id` |
|
|
927
|
+
| Memory | Task | `MemoryItem.task_id` |
|
|
910
928
|
|
|
911
929
|
No unified query across graphs — each graph has its own getters. The app layer composes them.
|
|
912
930
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Every chat session starts from scratch. Memory systems try to fix this by append
|
|
|
14
14
|
- **Whether** it's still relevant (decay)
|
|
15
15
|
- **Where** it came from (source attribution)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Most systems conflate "I can retrieve it" with "I know it." Retrieval is not memory. MemEX separates recall (a tool problem) from belief state (a knowledge problem).
|
|
18
18
|
|
|
19
19
|
## What MemEX Does
|
|
20
20
|
|
|
@@ -77,7 +77,7 @@ MemEX is the structured memory layer in a larger stack. It doesn't replace your
|
|
|
77
77
|
|
|
78
78
|
**Event store** -- the append-only command log. MemEX emits lifecycle events that get persisted. On restart, `replayFromEnvelopes` rebuilds the graph from the log.
|
|
79
79
|
|
|
80
|
-
MemEX is the system of record. The library itself is pure TypeScript with a single runtime dependency (`uuidv7`). Storage, search, and bus integration belong in the service layer above.
|
|
80
|
+
MemEX is the system of record. It does not replace retrieval systems -- it governs them. Vector search and keyword search are recall tools; MemEX is the epistemic coordination layer that decides what matters, what conflicts, and what to include in context. The library itself is pure TypeScript with a single runtime dependency (`uuidv7`). Storage, search, and bus integration belong in the service layer above.
|
|
81
81
|
|
|
82
82
|
### What changes in agent behavior
|
|
83
83
|
|
|
@@ -257,6 +257,24 @@ Commands go in, lifecycle events come out of the reducer, state events are full
|
|
|
257
257
|
|
|
258
258
|
`applyCommand` never mutates input state. It returns a new `GraphState` and an array of lifecycle events. History is in the append-only event log; `GraphState` is always the latest snapshot.
|
|
259
259
|
|
|
260
|
+
## Design Philosophy
|
|
261
|
+
|
|
262
|
+
Every system encodes assumptions about truth, knowledge, and time -- whether it acknowledges them or not. MemEX makes those assumptions explicit.
|
|
263
|
+
|
|
264
|
+
| Question | Typical system | MemEX |
|
|
265
|
+
|----------|---------------|-------|
|
|
266
|
+
| What is knowledge? | Similar text (vectors) or structured facts (SQL) | Beliefs with provenance, confidence, and conflict |
|
|
267
|
+
| What exists? | Documents, rows | Observations, hypotheses, derivations, policies, traits |
|
|
268
|
+
| Is truth binary? | Yes (stored or not) | No -- graded by authority, conviction, and importance |
|
|
269
|
+
| Does knowledge decay? | No (or manually pruned) | Yes -- query-time decay, configurable per retrieval |
|
|
270
|
+
| What about contradictions? | Overwrite or ignore | Represent, carry, and optionally resolve |
|
|
271
|
+
|
|
272
|
+
Most memory systems compress and resolve -- they produce a single clean narrative. MemEX preserves and represents -- it maintains a field of competing claims that a reasoning layer can interpret. The graph is the pre-answer belief state, not the final answer.
|
|
273
|
+
|
|
274
|
+
This is a deliberate architectural choice. MemEX is not a thinking system. It is a substrate that makes thinking systems possible. Storage, search, and cognition belong above the library. MemEX provides the structured epistemic state they operate on.
|
|
275
|
+
|
|
276
|
+
Vector search tells you what is similar. MemEX tells you what you believe.
|
|
277
|
+
|
|
260
278
|
## Three Graphs
|
|
261
279
|
|
|
262
280
|
MemEX contains three logical graphs in one package. Use what you need:
|
|
@@ -273,11 +291,17 @@ All three follow the same pattern: commands → reducer → lifecycle events. Th
|
|
|
273
291
|
// intent links to memory items that motivated it
|
|
274
292
|
const intent = createIntent({ label: "find_kati", root_memory_ids: [obs.id], ... });
|
|
275
293
|
|
|
294
|
+
// sub-intent decomposes a parent goal
|
|
295
|
+
const sub = createIntent({ label: "check_financials", parent_id: intent.id, ... });
|
|
296
|
+
|
|
276
297
|
// task links to its parent intent and memory items it consumes/produces
|
|
277
298
|
const task = createTask({ intent_id: intent.id, input_memory_ids: [obs.id], ... });
|
|
278
299
|
|
|
300
|
+
// subtask breaks a task into steps
|
|
301
|
+
const step = createTask({ intent_id: intent.id, parent_id: task.id, action: "parse_profile", ... });
|
|
302
|
+
|
|
279
303
|
// after task completes, memory items link back
|
|
280
|
-
createMemoryItem({ ...,
|
|
304
|
+
createMemoryItem({ ..., intent_id: intent.id, task_id: task.id });
|
|
281
305
|
```
|
|
282
306
|
|
|
283
307
|
## The Loop
|
|
@@ -352,10 +376,21 @@ Transferring cognition between agents is transferring this vector. The receiving
|
|
|
352
376
|
|
|
353
377
|
This is what `exportSlice` / `importSlice` enables at the library level. The transport layer (network, bus, file) is outside the library; MemEX provides the serializable structure.
|
|
354
378
|
|
|
379
|
+
### What transplant enables
|
|
380
|
+
|
|
381
|
+
| Pattern | How it works |
|
|
382
|
+
|---------|-------------|
|
|
383
|
+
| **Safe delegation** | Export a slice to a sub-agent. It operates on its own copy. Merge results back append-only -- no risk of corrupting the main graph. |
|
|
384
|
+
| **Parallel reasoning** | Fork belief state into multiple slices. Run different reasoning paths independently. Compare outcomes before merging. |
|
|
385
|
+
| **Reproducibility** | Event logs + deterministic slices mean any state can be replayed, audited, or debugged after the fact. |
|
|
386
|
+
| **State mobility** | Memory is not tied to one runtime. Export, serialize, move between agents or machines, rehydrate anywhere. |
|
|
387
|
+
|
|
388
|
+
Memory is no longer a local resource. It is portable belief.
|
|
389
|
+
|
|
355
390
|
## Features
|
|
356
391
|
|
|
357
392
|
**Memory graph:**
|
|
358
|
-
- Full query algebra: `and`, `or`, `not`, `range`, `ids`, `scope_prefix`, `parents` (includes/count), `meta` (dot-path), `meta_has`, `created` (time range), `decay` (freshness filter)
|
|
393
|
+
- Full query algebra: `and`, `or`, `not`, `range`, `ids`, `scope_prefix`, `parents` (includes/count), `intent_id`, `task_id`, `meta` (dot-path), `meta_has`, `created` (time range), `decay` (freshness filter)
|
|
359
394
|
- Multi-sort with tiebreakers (authority, conviction, importance, recency)
|
|
360
395
|
- Configurable time decay: exponential, linear, or step -- applied at query time, not stored
|
|
361
396
|
- Scored retrieval with pre/post filters, min_score threshold, and decay
|
|
@@ -374,13 +409,15 @@ This is what `exportSlice` / `importSlice` enables at the library level. The tra
|
|
|
374
409
|
|
|
375
410
|
**Intent graph:**
|
|
376
411
|
- Status machine: active ↔ paused → completed / cancelled
|
|
377
|
-
-
|
|
412
|
+
- Sub-intent hierarchies via `parent_id`
|
|
413
|
+
- Query by owner, status, priority, parent, linked memory items
|
|
378
414
|
- Invalid transitions throw typed errors
|
|
379
415
|
|
|
380
416
|
**Task graph:**
|
|
381
417
|
- Status machine: pending → running → completed / failed, with retry support (failed → running)
|
|
418
|
+
- Subtask hierarchies via `parent_id`
|
|
382
419
|
- Links to parent intent, input/output memory items, agent assignment
|
|
383
|
-
- Query by intent, action, status, agent, linked memory items
|
|
420
|
+
- Query by intent, action, status, agent, parent, linked memory items
|
|
384
421
|
|
|
385
422
|
**Transplant (export / import):**
|
|
386
423
|
- Export a self-contained slice by walking provenance chains, aliases, related intents/tasks
|
|
@@ -464,7 +501,10 @@ item.meta.session_id // "session-abc"
|
|
|
464
501
|
item.meta.crew_id // "crew:investigation-42"
|
|
465
502
|
|
|
466
503
|
// which intent spawned this?
|
|
467
|
-
item.
|
|
504
|
+
item.intent_id // "i1"
|
|
505
|
+
|
|
506
|
+
// which task produced this?
|
|
507
|
+
item.task_id // "t1"
|
|
468
508
|
```
|
|
469
509
|
|
|
470
510
|
All of these are queryable via `meta` and `meta_has` filters. The graph is one shared structure; segmentation is just queries.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -98,6 +98,7 @@ export {
|
|
|
98
98
|
applyIntentCommand,
|
|
99
99
|
getIntents,
|
|
100
100
|
getIntentById,
|
|
101
|
+
getChildIntents,
|
|
101
102
|
IntentNotFoundError,
|
|
102
103
|
DuplicateIntentError,
|
|
103
104
|
InvalidIntentTransitionError,
|
|
@@ -119,6 +120,7 @@ export {
|
|
|
119
120
|
getTasks,
|
|
120
121
|
getTaskById,
|
|
121
122
|
getTasksByIntent,
|
|
123
|
+
getChildTasks,
|
|
122
124
|
TaskNotFoundError,
|
|
123
125
|
DuplicateTaskError,
|
|
124
126
|
InvalidTaskTransitionError,
|
package/src/intent.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type IntentStatus = "active" | "paused" | "completed" | "cancelled";
|
|
|
8
8
|
|
|
9
9
|
export interface Intent {
|
|
10
10
|
id: string;
|
|
11
|
+
parent_id?: string; // parent intent for sub-intent hierarchies
|
|
11
12
|
label: string;
|
|
12
13
|
description?: string;
|
|
13
14
|
|
|
@@ -268,6 +269,8 @@ export interface IntentFilter {
|
|
|
268
269
|
statuses?: IntentStatus[];
|
|
269
270
|
min_priority?: number;
|
|
270
271
|
has_memory_id?: string;
|
|
272
|
+
parent_id?: string;
|
|
273
|
+
is_root?: boolean; // true = no parent, false = has parent
|
|
271
274
|
}
|
|
272
275
|
|
|
273
276
|
export function getIntents(
|
|
@@ -298,6 +301,13 @@ export function getIntents(
|
|
|
298
301
|
)
|
|
299
302
|
continue;
|
|
300
303
|
}
|
|
304
|
+
if (filter.parent_id !== undefined && intent.parent_id !== filter.parent_id)
|
|
305
|
+
continue;
|
|
306
|
+
if (filter.is_root !== undefined) {
|
|
307
|
+
const hasParent = intent.parent_id !== undefined;
|
|
308
|
+
if (filter.is_root && hasParent) continue;
|
|
309
|
+
if (!filter.is_root && !hasParent) continue;
|
|
310
|
+
}
|
|
301
311
|
results.push(intent);
|
|
302
312
|
}
|
|
303
313
|
return results;
|
|
@@ -309,3 +319,10 @@ export function getIntentById(
|
|
|
309
319
|
): Intent | undefined {
|
|
310
320
|
return state.intents.get(id);
|
|
311
321
|
}
|
|
322
|
+
|
|
323
|
+
export function getChildIntents(
|
|
324
|
+
state: IntentState,
|
|
325
|
+
parentId: string,
|
|
326
|
+
): Intent[] {
|
|
327
|
+
return getIntents(state, { parent_id: parentId });
|
|
328
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -60,6 +60,16 @@ function matchesFilter(item: MemoryItem, filter: MemoryFilter): boolean {
|
|
|
60
60
|
)
|
|
61
61
|
return false;
|
|
62
62
|
|
|
63
|
+
// cross-graph links
|
|
64
|
+
if (filter.intent_id !== undefined && item.intent_id !== filter.intent_id)
|
|
65
|
+
return false;
|
|
66
|
+
if (filter.intent_ids !== undefined && (!item.intent_id || !filter.intent_ids.includes(item.intent_id)))
|
|
67
|
+
return false;
|
|
68
|
+
if (filter.task_id !== undefined && item.task_id !== filter.task_id)
|
|
69
|
+
return false;
|
|
70
|
+
if (filter.task_ids !== undefined && (!item.task_id || !filter.task_ids.includes(item.task_id)))
|
|
71
|
+
return false;
|
|
72
|
+
|
|
63
73
|
// score ranges
|
|
64
74
|
if (filter.range) {
|
|
65
75
|
if (!matchesRange(item.authority, filter.range.authority)) return false;
|
package/src/task.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type TaskStatus =
|
|
|
14
14
|
export interface Task {
|
|
15
15
|
id: string;
|
|
16
16
|
intent_id: string;
|
|
17
|
+
parent_id?: string; // parent task for subtask hierarchies
|
|
17
18
|
|
|
18
19
|
action: string; // "search_linkedin", "summarize_case"
|
|
19
20
|
label?: string;
|
|
@@ -325,6 +326,8 @@ export interface TaskFilter {
|
|
|
325
326
|
min_priority?: number;
|
|
326
327
|
has_input_memory_id?: string;
|
|
327
328
|
has_output_memory_id?: string;
|
|
329
|
+
parent_id?: string;
|
|
330
|
+
is_root?: boolean; // true = no parent, false = has parent
|
|
328
331
|
}
|
|
329
332
|
|
|
330
333
|
export function getTasks(state: TaskState, filter?: TaskFilter): Task[] {
|
|
@@ -359,6 +362,13 @@ export function getTasks(state: TaskState, filter?: TaskFilter): Task[] {
|
|
|
359
362
|
)
|
|
360
363
|
continue;
|
|
361
364
|
}
|
|
365
|
+
if (filter.parent_id !== undefined && task.parent_id !== filter.parent_id)
|
|
366
|
+
continue;
|
|
367
|
+
if (filter.is_root !== undefined) {
|
|
368
|
+
const hasParent = task.parent_id !== undefined;
|
|
369
|
+
if (filter.is_root && hasParent) continue;
|
|
370
|
+
if (!filter.is_root && !hasParent) continue;
|
|
371
|
+
}
|
|
362
372
|
results.push(task);
|
|
363
373
|
}
|
|
364
374
|
return results;
|
|
@@ -371,3 +381,10 @@ export function getTaskById(state: TaskState, id: string): Task | undefined {
|
|
|
371
381
|
export function getTasksByIntent(state: TaskState, intentId: string): Task[] {
|
|
372
382
|
return getTasks(state, { intent_id: intentId });
|
|
373
383
|
}
|
|
384
|
+
|
|
385
|
+
export function getChildTasks(
|
|
386
|
+
state: TaskState,
|
|
387
|
+
parentId: string,
|
|
388
|
+
): Task[] {
|
|
389
|
+
return getTasks(state, { parent_id: parentId });
|
|
390
|
+
}
|
package/src/transplant.ts
CHANGED
|
@@ -148,12 +148,10 @@ export function exportSlice(
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
// also check memory
|
|
151
|
+
// also check memory items for linked intent
|
|
152
152
|
for (const mid of memoryIds) {
|
|
153
153
|
const item = memState.items.get(mid);
|
|
154
|
-
if (item?.
|
|
155
|
-
intentIds.add(item.meta.creation_intent_id as string);
|
|
156
|
-
}
|
|
154
|
+
if (item?.intent_id) intentIds.add(item.intent_id);
|
|
157
155
|
}
|
|
158
156
|
}
|
|
159
157
|
|
|
@@ -172,12 +170,10 @@ export function exportSlice(
|
|
|
172
170
|
taskIds.add(task.id);
|
|
173
171
|
}
|
|
174
172
|
}
|
|
175
|
-
// also check memory
|
|
173
|
+
// also check memory items for linked task
|
|
176
174
|
for (const mid of memoryIds) {
|
|
177
175
|
const item = memState.items.get(mid);
|
|
178
|
-
if (item?.
|
|
179
|
-
taskIds.add(item.meta.creation_task_id as string);
|
|
180
|
-
}
|
|
176
|
+
if (item?.task_id) taskIds.add(item.task_id);
|
|
181
177
|
}
|
|
182
178
|
}
|
|
183
179
|
|
|
@@ -404,6 +400,9 @@ export function importSlice(
|
|
|
404
400
|
const remapped: Intent = {
|
|
405
401
|
...intent,
|
|
406
402
|
id: newId,
|
|
403
|
+
parent_id: intent.parent_id
|
|
404
|
+
? rewriteId(intent.parent_id, intentIdMap)
|
|
405
|
+
: undefined,
|
|
407
406
|
root_memory_ids: rewriteIds(intent.root_memory_ids, memIdMap),
|
|
408
407
|
};
|
|
409
408
|
const result = applyIntentCommand(currentIntent, {
|
|
@@ -423,6 +422,9 @@ export function importSlice(
|
|
|
423
422
|
}
|
|
424
423
|
const remapped: Intent = {
|
|
425
424
|
...intent,
|
|
425
|
+
parent_id: intent.parent_id
|
|
426
|
+
? rewriteId(intent.parent_id, intentIdMap)
|
|
427
|
+
: undefined,
|
|
426
428
|
root_memory_ids: rewriteIds(intent.root_memory_ids, memIdMap),
|
|
427
429
|
};
|
|
428
430
|
const result = applyIntentCommand(currentIntent, {
|
|
@@ -447,6 +449,9 @@ export function importSlice(
|
|
|
447
449
|
...task,
|
|
448
450
|
id: newId,
|
|
449
451
|
intent_id: rewriteId(task.intent_id, intentIdMap),
|
|
452
|
+
parent_id: task.parent_id
|
|
453
|
+
? rewriteId(task.parent_id, taskIdMap)
|
|
454
|
+
: undefined,
|
|
450
455
|
input_memory_ids: rewriteIds(task.input_memory_ids, memIdMap),
|
|
451
456
|
output_memory_ids: rewriteIds(task.output_memory_ids, memIdMap),
|
|
452
457
|
};
|
|
@@ -468,6 +473,9 @@ export function importSlice(
|
|
|
468
473
|
const remapped: Task = {
|
|
469
474
|
...task,
|
|
470
475
|
intent_id: rewriteId(task.intent_id, intentIdMap),
|
|
476
|
+
parent_id: task.parent_id
|
|
477
|
+
? rewriteId(task.parent_id, taskIdMap)
|
|
478
|
+
: undefined,
|
|
471
479
|
input_memory_ids: rewriteIds(task.input_memory_ids, memIdMap),
|
|
472
480
|
output_memory_ids: rewriteIds(task.output_memory_ids, memIdMap),
|
|
473
481
|
};
|
package/src/types.ts
CHANGED
|
@@ -46,6 +46,9 @@ export interface MemoryItem {
|
|
|
46
46
|
conviction?: number; // 0..1 -- how sure was the author?
|
|
47
47
|
importance?: number; // 0..1 -- how much attention does this need right now? (salience)
|
|
48
48
|
|
|
49
|
+
intent_id?: string; // intent that produced this item
|
|
50
|
+
task_id?: string; // task that produced this item
|
|
51
|
+
|
|
49
52
|
meta?: {
|
|
50
53
|
agent_id?: string;
|
|
51
54
|
session_id?: string;
|
|
@@ -181,6 +184,11 @@ export interface MemoryFilter {
|
|
|
181
184
|
importance?: { min?: number; max?: number };
|
|
182
185
|
};
|
|
183
186
|
|
|
187
|
+
intent_id?: string; // exact match on intent_id
|
|
188
|
+
intent_ids?: string[]; // match any of these intent_ids
|
|
189
|
+
task_id?: string; // exact match on task_id
|
|
190
|
+
task_ids?: string[]; // match any of these task_ids
|
|
191
|
+
|
|
184
192
|
has_parent?: string; // sugar for parents.includes
|
|
185
193
|
is_root?: boolean; // sugar for parents.count.max = 0
|
|
186
194
|
parents?: {
|
|
@@ -621,13 +621,13 @@ describe("resolveContradiction with multiple CONTRADICTS edges", () => {
|
|
|
621
621
|
});
|
|
622
622
|
|
|
623
623
|
// =========================================================================
|
|
624
|
-
// 10. exportSlice with include_related_intents via
|
|
624
|
+
// 10. exportSlice with include_related_intents via intent_id
|
|
625
625
|
// =========================================================================
|
|
626
626
|
|
|
627
|
-
describe("exportSlice walks
|
|
628
|
-
it("includes intents referenced by memory
|
|
627
|
+
describe("exportSlice walks intent_id", () => {
|
|
628
|
+
it("includes intents referenced by memory intent_id", () => {
|
|
629
629
|
const memState = stateWith([
|
|
630
|
-
makeItem("m1", {
|
|
630
|
+
makeItem("m1", { intent_id: "i1" }),
|
|
631
631
|
]);
|
|
632
632
|
let intentState = createIntentState();
|
|
633
633
|
const intent = createIntent({
|
|
@@ -653,13 +653,13 @@ describe("exportSlice walks meta.creation_intent_id", () => {
|
|
|
653
653
|
});
|
|
654
654
|
|
|
655
655
|
// =========================================================================
|
|
656
|
-
// 11. exportSlice with include_related_tasks via
|
|
656
|
+
// 11. exportSlice with include_related_tasks via task_id
|
|
657
657
|
// =========================================================================
|
|
658
658
|
|
|
659
|
-
describe("exportSlice walks
|
|
660
|
-
it("includes tasks referenced by memory
|
|
659
|
+
describe("exportSlice walks task_id", () => {
|
|
660
|
+
it("includes tasks referenced by memory task_id", () => {
|
|
661
661
|
const memState = stateWith([
|
|
662
|
-
makeItem("m1", {
|
|
662
|
+
makeItem("m1", { task_id: "t1" }),
|
|
663
663
|
]);
|
|
664
664
|
const intentState = createIntentState();
|
|
665
665
|
let taskState = createTaskState();
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createGraphState } from "../src/graph.js";
|
|
3
|
+
import { applyCommand } from "../src/reducer.js";
|
|
4
|
+
import { getItems } from "../src/query.js";
|
|
5
|
+
import {
|
|
6
|
+
createIntentState,
|
|
7
|
+
createIntent,
|
|
8
|
+
applyIntentCommand,
|
|
9
|
+
getIntents,
|
|
10
|
+
getIntentById,
|
|
11
|
+
getChildIntents,
|
|
12
|
+
} from "../src/intent.js";
|
|
13
|
+
import {
|
|
14
|
+
createTaskState,
|
|
15
|
+
createTask,
|
|
16
|
+
applyTaskCommand,
|
|
17
|
+
getTasks,
|
|
18
|
+
getTaskById,
|
|
19
|
+
getChildTasks,
|
|
20
|
+
} from "../src/task.js";
|
|
21
|
+
import { exportSlice, importSlice } from "../src/transplant.js";
|
|
22
|
+
import type { MemoryItem, GraphState } from "../src/types.js";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function makeItem(
|
|
29
|
+
id: string,
|
|
30
|
+
overrides: Partial<MemoryItem> = {},
|
|
31
|
+
): MemoryItem {
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
scope: "test",
|
|
35
|
+
kind: "observation",
|
|
36
|
+
content: { text: `item ${id}` },
|
|
37
|
+
author: "agent:test",
|
|
38
|
+
source_kind: "observed",
|
|
39
|
+
authority: 0.8,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stateWith(items: MemoryItem[]): GraphState {
|
|
45
|
+
let state = createGraphState();
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
state = applyCommand(state, { type: "memory.create", item }).state;
|
|
48
|
+
}
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// =========================================================================
|
|
53
|
+
// MemoryFilter: intent_id / task_id exact match
|
|
54
|
+
// =========================================================================
|
|
55
|
+
|
|
56
|
+
describe("MemoryFilter intent_id and task_id", () => {
|
|
57
|
+
it("filters by exact intent_id", () => {
|
|
58
|
+
const state = stateWith([
|
|
59
|
+
makeItem("m1", { intent_id: "i1" }),
|
|
60
|
+
makeItem("m2", { intent_id: "i2" }),
|
|
61
|
+
makeItem("m3"),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const results = getItems(state, { intent_id: "i1" });
|
|
65
|
+
expect(results.map((i) => i.id)).toEqual(["m1"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("filters by exact task_id", () => {
|
|
69
|
+
const state = stateWith([
|
|
70
|
+
makeItem("m1", { task_id: "t1" }),
|
|
71
|
+
makeItem("m2", { task_id: "t2" }),
|
|
72
|
+
makeItem("m3"),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const results = getItems(state, { task_id: "t1" });
|
|
76
|
+
expect(results.map((i) => i.id)).toEqual(["m1"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("excludes items without intent_id when filtering", () => {
|
|
80
|
+
const state = stateWith([
|
|
81
|
+
makeItem("m1"),
|
|
82
|
+
makeItem("m2", { intent_id: "i1" }),
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const results = getItems(state, { intent_id: "i1" });
|
|
86
|
+
expect(results.length).toBe(1);
|
|
87
|
+
expect(results[0].id).toBe("m2");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// =========================================================================
|
|
92
|
+
// MemoryFilter: intent_ids / task_ids (any-of)
|
|
93
|
+
// =========================================================================
|
|
94
|
+
|
|
95
|
+
describe("MemoryFilter intent_ids and task_ids", () => {
|
|
96
|
+
it("filters by any of intent_ids", () => {
|
|
97
|
+
const state = stateWith([
|
|
98
|
+
makeItem("m1", { intent_id: "i1" }),
|
|
99
|
+
makeItem("m2", { intent_id: "i2" }),
|
|
100
|
+
makeItem("m3", { intent_id: "i3" }),
|
|
101
|
+
makeItem("m4"),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const results = getItems(state, { intent_ids: ["i1", "i3"] });
|
|
105
|
+
expect(results.map((i) => i.id).sort()).toEqual(["m1", "m3"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("filters by any of task_ids", () => {
|
|
109
|
+
const state = stateWith([
|
|
110
|
+
makeItem("m1", { task_id: "t1" }),
|
|
111
|
+
makeItem("m2", { task_id: "t2" }),
|
|
112
|
+
makeItem("m3", { task_id: "t3" }),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const results = getItems(state, { task_ids: ["t2", "t3"] });
|
|
116
|
+
expect(results.map((i) => i.id).sort()).toEqual(["m2", "m3"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("excludes items without task_id when using task_ids filter", () => {
|
|
120
|
+
const state = stateWith([
|
|
121
|
+
makeItem("m1"),
|
|
122
|
+
makeItem("m2", { task_id: "t1" }),
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const results = getItems(state, { task_ids: ["t1"] });
|
|
126
|
+
expect(results.length).toBe(1);
|
|
127
|
+
expect(results[0].id).toBe("m2");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("combines intent_id and task_id filters (AND)", () => {
|
|
131
|
+
const state = stateWith([
|
|
132
|
+
makeItem("m1", { intent_id: "i1", task_id: "t1" }),
|
|
133
|
+
makeItem("m2", { intent_id: "i1", task_id: "t2" }),
|
|
134
|
+
makeItem("m3", { intent_id: "i2", task_id: "t1" }),
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const results = getItems(state, { intent_id: "i1", task_id: "t1" });
|
|
138
|
+
expect(results.map((i) => i.id)).toEqual(["m1"]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// =========================================================================
|
|
143
|
+
// IntentFilter: parent_id / is_root
|
|
144
|
+
// =========================================================================
|
|
145
|
+
|
|
146
|
+
describe("IntentFilter parent_id and is_root", () => {
|
|
147
|
+
function setupIntents() {
|
|
148
|
+
let state = createIntentState();
|
|
149
|
+
const root = createIntent({
|
|
150
|
+
id: "i1",
|
|
151
|
+
label: "investigate",
|
|
152
|
+
priority: 0.9,
|
|
153
|
+
owner: "user:laz",
|
|
154
|
+
});
|
|
155
|
+
const child1 = createIntent({
|
|
156
|
+
id: "i2",
|
|
157
|
+
parent_id: "i1",
|
|
158
|
+
label: "find associates",
|
|
159
|
+
priority: 0.7,
|
|
160
|
+
owner: "user:laz",
|
|
161
|
+
});
|
|
162
|
+
const child2 = createIntent({
|
|
163
|
+
id: "i3",
|
|
164
|
+
parent_id: "i1",
|
|
165
|
+
label: "map finances",
|
|
166
|
+
priority: 0.8,
|
|
167
|
+
owner: "user:laz",
|
|
168
|
+
});
|
|
169
|
+
state = applyIntentCommand(state, { type: "intent.create", intent: root }).state;
|
|
170
|
+
state = applyIntentCommand(state, { type: "intent.create", intent: child1 }).state;
|
|
171
|
+
state = applyIntentCommand(state, { type: "intent.create", intent: child2 }).state;
|
|
172
|
+
return state;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
it("filters by parent_id", () => {
|
|
176
|
+
const state = setupIntents();
|
|
177
|
+
const children = getIntents(state, { parent_id: "i1" });
|
|
178
|
+
expect(children.map((i) => i.id).sort()).toEqual(["i2", "i3"]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("is_root: true returns only root intents", () => {
|
|
182
|
+
const state = setupIntents();
|
|
183
|
+
const roots = getIntents(state, { is_root: true });
|
|
184
|
+
expect(roots.map((i) => i.id)).toEqual(["i1"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("is_root: false returns only child intents", () => {
|
|
188
|
+
const state = setupIntents();
|
|
189
|
+
const children = getIntents(state, { is_root: false });
|
|
190
|
+
expect(children.map((i) => i.id).sort()).toEqual(["i2", "i3"]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("getChildIntents returns children of a parent", () => {
|
|
194
|
+
const state = setupIntents();
|
|
195
|
+
const children = getChildIntents(state, "i1");
|
|
196
|
+
expect(children.map((i) => i.id).sort()).toEqual(["i2", "i3"]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("getChildIntents returns empty for leaf intents", () => {
|
|
200
|
+
const state = setupIntents();
|
|
201
|
+
const children = getChildIntents(state, "i2");
|
|
202
|
+
expect(children).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// =========================================================================
|
|
207
|
+
// TaskFilter: parent_id / is_root
|
|
208
|
+
// =========================================================================
|
|
209
|
+
|
|
210
|
+
describe("TaskFilter parent_id and is_root", () => {
|
|
211
|
+
function setupTasks() {
|
|
212
|
+
let state = createTaskState();
|
|
213
|
+
const root = createTask({
|
|
214
|
+
id: "t1",
|
|
215
|
+
intent_id: "i1",
|
|
216
|
+
action: "search",
|
|
217
|
+
priority: 0.9,
|
|
218
|
+
});
|
|
219
|
+
const sub1 = createTask({
|
|
220
|
+
id: "t2",
|
|
221
|
+
intent_id: "i1",
|
|
222
|
+
parent_id: "t1",
|
|
223
|
+
action: "parse_profile",
|
|
224
|
+
priority: 0.7,
|
|
225
|
+
});
|
|
226
|
+
const sub2 = createTask({
|
|
227
|
+
id: "t3",
|
|
228
|
+
intent_id: "i1",
|
|
229
|
+
parent_id: "t1",
|
|
230
|
+
action: "extract_contacts",
|
|
231
|
+
priority: 0.6,
|
|
232
|
+
});
|
|
233
|
+
state = applyTaskCommand(state, { type: "task.create", task: root }).state;
|
|
234
|
+
state = applyTaskCommand(state, { type: "task.create", task: sub1 }).state;
|
|
235
|
+
state = applyTaskCommand(state, { type: "task.create", task: sub2 }).state;
|
|
236
|
+
return state;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
it("filters by parent_id", () => {
|
|
240
|
+
const state = setupTasks();
|
|
241
|
+
const subs = getTasks(state, { parent_id: "t1" });
|
|
242
|
+
expect(subs.map((t) => t.id).sort()).toEqual(["t2", "t3"]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("is_root: true returns only root tasks", () => {
|
|
246
|
+
const state = setupTasks();
|
|
247
|
+
const roots = getTasks(state, { is_root: true });
|
|
248
|
+
expect(roots.map((t) => t.id)).toEqual(["t1"]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("is_root: false returns only subtasks", () => {
|
|
252
|
+
const state = setupTasks();
|
|
253
|
+
const subs = getTasks(state, { is_root: false });
|
|
254
|
+
expect(subs.map((t) => t.id).sort()).toEqual(["t2", "t3"]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("getChildTasks returns children of a parent", () => {
|
|
258
|
+
const state = setupTasks();
|
|
259
|
+
const subs = getChildTasks(state, "t1");
|
|
260
|
+
expect(subs.map((t) => t.id).sort()).toEqual(["t2", "t3"]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("getChildTasks returns empty for leaf tasks", () => {
|
|
264
|
+
const state = setupTasks();
|
|
265
|
+
const subs = getChildTasks(state, "t3");
|
|
266
|
+
expect(subs).toEqual([]);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// =========================================================================
|
|
271
|
+
// Transplant: parent_id rewriting
|
|
272
|
+
// =========================================================================
|
|
273
|
+
|
|
274
|
+
describe("transplant rewrites parent_id", () => {
|
|
275
|
+
it("rewrites intent parent_id on re-id", () => {
|
|
276
|
+
const memState = createGraphState();
|
|
277
|
+
let intentState = createIntentState();
|
|
278
|
+
const taskState = createTaskState();
|
|
279
|
+
|
|
280
|
+
// create parent intent in destination
|
|
281
|
+
const existing = createIntent({
|
|
282
|
+
id: "i1",
|
|
283
|
+
label: "existing",
|
|
284
|
+
priority: 0.5,
|
|
285
|
+
owner: "user:laz",
|
|
286
|
+
});
|
|
287
|
+
intentState = applyIntentCommand(intentState, {
|
|
288
|
+
type: "intent.create",
|
|
289
|
+
intent: existing,
|
|
290
|
+
}).state;
|
|
291
|
+
|
|
292
|
+
// slice has same id but different data, plus a child
|
|
293
|
+
const sliceParent = createIntent({
|
|
294
|
+
id: "i1",
|
|
295
|
+
label: "different",
|
|
296
|
+
priority: 0.9,
|
|
297
|
+
owner: "user:laz",
|
|
298
|
+
});
|
|
299
|
+
const sliceChild = createIntent({
|
|
300
|
+
id: "i2",
|
|
301
|
+
parent_id: "i1",
|
|
302
|
+
label: "child",
|
|
303
|
+
priority: 0.7,
|
|
304
|
+
owner: "user:laz",
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const result = importSlice(memState, intentState, taskState, {
|
|
308
|
+
memories: [],
|
|
309
|
+
edges: [],
|
|
310
|
+
intents: [sliceParent, sliceChild],
|
|
311
|
+
tasks: [],
|
|
312
|
+
}, {
|
|
313
|
+
skipExistingIds: true,
|
|
314
|
+
shallowCompareExisting: true,
|
|
315
|
+
reIdOnDifference: true,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// parent got re-id'd
|
|
319
|
+
const newParentId = result.report.created.intents[0];
|
|
320
|
+
expect(newParentId).not.toBe("i1");
|
|
321
|
+
|
|
322
|
+
// child should reference the new parent id
|
|
323
|
+
const child = result.intentState.intents.get("i2")!;
|
|
324
|
+
expect(child.parent_id).toBe(newParentId);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("rewrites task parent_id on re-id", () => {
|
|
328
|
+
const memState = createGraphState();
|
|
329
|
+
const intentState = createIntentState();
|
|
330
|
+
let taskState = createTaskState();
|
|
331
|
+
|
|
332
|
+
// create parent task in destination
|
|
333
|
+
const existing = createTask({
|
|
334
|
+
id: "t1",
|
|
335
|
+
intent_id: "i1",
|
|
336
|
+
action: "search",
|
|
337
|
+
priority: 0.5,
|
|
338
|
+
});
|
|
339
|
+
taskState = applyTaskCommand(taskState, {
|
|
340
|
+
type: "task.create",
|
|
341
|
+
task: existing,
|
|
342
|
+
}).state;
|
|
343
|
+
|
|
344
|
+
// slice has same id but different data, plus a child
|
|
345
|
+
const sliceParent = createTask({
|
|
346
|
+
id: "t1",
|
|
347
|
+
intent_id: "i1",
|
|
348
|
+
action: "different_search",
|
|
349
|
+
priority: 0.9,
|
|
350
|
+
});
|
|
351
|
+
const sliceChild = createTask({
|
|
352
|
+
id: "t2",
|
|
353
|
+
intent_id: "i1",
|
|
354
|
+
parent_id: "t1",
|
|
355
|
+
action: "parse",
|
|
356
|
+
priority: 0.7,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const result = importSlice(memState, intentState, taskState, {
|
|
360
|
+
memories: [],
|
|
361
|
+
edges: [],
|
|
362
|
+
intents: [],
|
|
363
|
+
tasks: [sliceParent, sliceChild],
|
|
364
|
+
}, {
|
|
365
|
+
skipExistingIds: true,
|
|
366
|
+
shallowCompareExisting: true,
|
|
367
|
+
reIdOnDifference: true,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// parent got re-id'd
|
|
371
|
+
const newParentId = result.report.created.tasks[0];
|
|
372
|
+
expect(newParentId).not.toBe("t1");
|
|
373
|
+
|
|
374
|
+
// child should reference the new parent id
|
|
375
|
+
const child = result.taskState.tasks.get("t2")!;
|
|
376
|
+
expect(child.parent_id).toBe(newParentId);
|
|
377
|
+
});
|
|
378
|
+
});
|