@zenera/neo 1.1.11 → 1.1.12

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 (60) hide show
  1. package/README.md +3 -3
  2. package/dist/agent.d.ts +8 -7
  3. package/dist/agent.js +15 -8
  4. package/dist/architecture.d.ts +7 -5
  5. package/dist/architecture.js +20 -12
  6. package/dist/index.d.ts +12 -2
  7. package/dist/index.js +12 -2
  8. package/dist/inspect/client.d.ts +1 -1
  9. package/dist/inspect/client.js +42 -26
  10. package/dist/kernel.d.ts +1 -1
  11. package/dist/kernel.js +40 -23
  12. package/dist/memory/files.d.ts +24 -0
  13. package/dist/memory/files.js +85 -0
  14. package/dist/memory/graph.d.ts +56 -0
  15. package/dist/memory/graph.js +159 -0
  16. package/dist/memory/index.d.ts +93 -0
  17. package/dist/memory/index.js +357 -0
  18. package/dist/memory/instructions.d.ts +15 -0
  19. package/dist/memory/instructions.js +103 -0
  20. package/dist/memory/page.d.ts +8 -0
  21. package/dist/memory/page.js +797 -0
  22. package/dist/memory/recall.d.ts +35 -0
  23. package/dist/memory/recall.js +161 -0
  24. package/dist/memory/render.d.ts +10 -0
  25. package/dist/memory/render.js +228 -0
  26. package/dist/memory/report.d.ts +57 -0
  27. package/dist/memory/report.js +113 -0
  28. package/dist/memory/store.d.ts +49 -0
  29. package/dist/memory/store.js +226 -0
  30. package/dist/memory/tools.d.ts +11 -0
  31. package/dist/memory/tools.js +310 -0
  32. package/dist/memory/types.d.ts +192 -0
  33. package/dist/memory/types.js +105 -0
  34. package/dist/memory/vectors.d.ts +29 -0
  35. package/dist/memory/vectors.js +152 -0
  36. package/dist/project/config.d.ts +49 -1
  37. package/dist/project/config.js +63 -0
  38. package/dist/project/index.d.ts +2 -2
  39. package/dist/project/index.js +1 -1
  40. package/dist/project/load.d.ts +16 -2
  41. package/dist/project/load.js +67 -1
  42. package/dist/runner.d.ts +4 -2
  43. package/dist/runner.js +29 -30
  44. package/dist/services.d.ts +23 -6
  45. package/dist/services.js +25 -15
  46. package/dist/tools/workspace.js +195 -17
  47. package/dist/trajectory.d.ts +14 -12
  48. package/dist/types.d.ts +5 -5
  49. package/dist/types.js +3 -3
  50. package/package.json +2 -5
  51. package/dist/memory-stores/factory.d.ts +0 -15
  52. package/dist/memory-stores/factory.js +0 -29
  53. package/dist/memory-stores/file.d.ts +0 -37
  54. package/dist/memory-stores/file.js +0 -174
  55. package/dist/memory-stores/in-memory.d.ts +0 -17
  56. package/dist/memory-stores/in-memory.js +0 -80
  57. package/dist/memory-stores/index.d.ts +0 -4
  58. package/dist/memory-stores/index.js +0 -5
  59. package/dist/memory.d.ts +0 -115
  60. package/dist/memory.js +0 -213
@@ -60,6 +60,15 @@ const ROOT = flatten(STATE.trajectory, 0, null);
60
60
 
61
61
  function names(list) { return list.join(', '); }
62
62
 
63
+ // The edges are what makes a recollection a subgraph rather than a hit list,
64
+ // so they are shown even though the injected block already renders them.
65
+ function edgeLines(edges) {
66
+ if (!edges || !edges.length) return '';
67
+ return '\\n\\n' + edges.map(function (e) {
68
+ return e.source + ' --' + e.relation + '--> ' + e.target;
69
+ }).join('\\n');
70
+ }
71
+
63
72
  function base(path) { return path.split('/').pop() || path; }
64
73
 
65
74
  function label(n) {
@@ -68,8 +77,8 @@ function label(n) {
68
77
  case 'system_prompt': return 'system prompt'
69
78
  + (n.sources && n.sources.length ? ': ' + names(n.sources.map(function (s) { return base(s.path); })) : '');
70
79
  case 'load_skills': return 'skills: ' + names(n.skills.map(function (s) { return s.name; }));
71
- case 'memory_recall': return 'memory recall: ' + n.scope;
72
- case 'memory_op': return 'memory ' + n.op;
80
+ case 'memory_recall': return 'memory recall: ' + n.nodes.length + ' nodes';
81
+ case 'memory_op': return 'memory ' + n.op + ': ' + n.nodes.length + ' nodes';
73
82
  case 'llm_call': return 'llm: ' + n.model + (n.toolCalls.length
74
83
  ? ' -> ' + names(n.toolCalls.map(function (c) { return c.name; })) : '');
75
84
  case 'tool_call': return 'call: ' + n.name;
@@ -512,18 +521,20 @@ function detailFor(e) {
512
521
  break;
513
522
 
514
523
  case 'memory_recall':
515
- out.appendChild(textBlock('Query', JSON.stringify(n.query, null, 2), n.store + ' · ' + n.scope));
516
- out.appendChild(textBlock('Hits', n.hits.map(function (hit) {
517
- return hit.id + ' score=' + hit.score + ' rev=' + hit.revision;
518
- }).join('\\n')));
524
+ out.appendChild(textBlock('Query', JSON.stringify(n.query, null, 2),
525
+ n.seeds.length + ' seed(s)'));
526
+ out.appendChild(textBlock('Subgraph', n.nodes.map(function (m) {
527
+ return m.id + ' ' + m.kind
528
+ + (n.seeds.indexOf(m.id) >= 0 ? ' score=' + m.score.toFixed(3) : ' (stitched)');
529
+ }).join('\\n') + edgeLines(n.edges)));
519
530
  out.appendChild(textBlock('Injected block', blob(n.content)));
520
531
  break;
521
532
 
522
533
  case 'memory_op':
523
- out.appendChild(textBlock('Operation',
524
- n.op + ' ' + n.recordId + ' (rev ' + n.revision + ')', n.store + ' · ' + n.scope));
525
- if (n.before) out.appendChild(textBlock('Before', blob(n.before)));
526
- if (n.after) out.appendChild(textBlock('After', blob(n.after)));
534
+ out.appendChild(textBlock('Operation', n.nodes.map(function (m) {
535
+ return n.op + ' ' + m.id + ' ' + m.kind + ' rev ' + m.revision;
536
+ }).join('\\n') + edgeLines(n.edges),
537
+ n.files ? n.files + ' file(s)' : undefined));
527
538
  break;
528
539
 
529
540
  case 'llm_call': {
@@ -998,6 +1009,14 @@ function diagram() {
998
1009
  return lines.join('\\n');
999
1010
  }
1000
1011
 
1012
+ // Mermaid refuses a diagram past either of these (defaults 500 edges and
1013
+ // 50000 characters, which a long run passes easily) and the pane then shows
1014
+ // only its error. Both are "secure" options, so initialize() is the one place
1015
+ // they can be raised from. The report is a file on disk, not a live page —
1016
+ // a slow render beats a refused one.
1017
+ const MAX_EDGES = 20000;
1018
+ const MAX_TEXT_SIZE = 5000000;
1019
+
1001
1020
  // One Mermaid, two diagrams. Loading it is the only network the page does, so
1002
1021
  // it happens once and both panes wait on the same promise.
1003
1022
  let mermaidLib = null;
@@ -1006,6 +1025,7 @@ function mermaid() {
1006
1025
  mermaidLib = import(MERMAID_URL).then(function (m) {
1007
1026
  m.default.initialize({
1008
1027
  startOnLoad: false, theme: 'dark', securityLevel: 'strict',
1028
+ maxEdges: MAX_EDGES, maxTextSize: MAX_TEXT_SIZE,
1009
1029
  flowchart: { htmlLabels: false }
1010
1030
  });
1011
1031
  return m.default;
@@ -1098,7 +1118,7 @@ ENTRIES.forEach(function (e) {
1098
1118
  } else if (n.type === 'handoff') {
1099
1119
  bump(USED.handoffs, n.from + SEP + n.to, e.key);
1100
1120
  } else if (n.type === 'memory_recall' || n.type === 'memory_op') {
1101
- bump(USED.memory, n.agent + SEP + n.store + '/' + n.scope, e.key);
1121
+ bump(USED.memory, n.agent, e.key);
1102
1122
  } else if (n.type === 'fork') {
1103
1123
  n.branches.forEach(function (b) { bump(USED.forks, n.agent + SEP + b.agent, e.key); });
1104
1124
  }
@@ -1113,7 +1133,7 @@ function observedArchitecture() {
1113
1133
  const byName = new Map();
1114
1134
  function agentOf(name) {
1115
1135
  if (!byName.has(name)) {
1116
- byName.set(name, { name: name, tools: [], handoffs: [], memory: [] });
1136
+ byName.set(name, { name: name, tools: [], handoffs: [] });
1117
1137
  }
1118
1138
  return byName.get(name);
1119
1139
  }
@@ -1142,15 +1162,10 @@ function observedArchitecture() {
1142
1162
  agentOf(n.to);
1143
1163
  if (from.handoffs.indexOf(n.to) < 0) from.handoffs.push(n.to);
1144
1164
  } else if (n.type === 'memory_recall' || n.type === 'memory_op') {
1145
- const known = a.memory.some(function (m) {
1146
- return m.store === n.store && m.scope === n.scope;
1147
- });
1148
- if (!known) {
1149
- a.memory.push({
1150
- store: n.store, scope: n.scope,
1151
- access: n.type === 'memory_op' ? 'read-write' : 'read'
1152
- });
1153
- }
1165
+ // A run shows that an agent reached the memory, never how wide its mask
1166
+ // was: nothing it did not read leaves a trace to read the mask off.
1167
+ if (!a.memory) a.memory = { access: 'read', sees: [], writes: [] };
1168
+ if (n.type === 'memory_op') a.memory.access = 'read-write';
1154
1169
  } else if (n.type === 'fork') {
1155
1170
  if (!a.fork) a.fork = { agents: [] };
1156
1171
  n.branches.forEach(function (b) {
@@ -1255,15 +1270,16 @@ function archDiagram() {
1255
1270
  lines.push(' ' + self + ' --- ' + k);
1256
1271
  }
1257
1272
 
1258
- a.memory.forEach(function (m) {
1259
- const used = USED.memory.get(a.name + SEP + m.store + '/' + m.scope);
1273
+ if (a.memory) {
1274
+ const used = USED.memory.get(a.name);
1260
1275
  const k = archKey(used);
1261
- lines.push(' ' + k + '[("' + safe(m.store) + ' / ' + safe(m.scope)
1262
- + (m.access === 'read-write' ? ' (rw)' : ' (ro)') + '")]');
1276
+ const sees = Array.isArray(a.memory.sees) ? a.memory.sees : [a.memory.sees];
1277
+ lines.push(' ' + k + '[("memory ' + (a.memory.access === 'read' ? '(ro)' : '(rw)')
1278
+ + ' sees ' + safe(sees.join(' ') || '*') + '")]');
1263
1279
  lines.push(' class ' + k + ' mem;');
1264
1280
  if (!used) lines.push(' class ' + k + ' idle;');
1265
1281
  lines.push(' ' + self + ' --- ' + k);
1266
- });
1282
+ }
1267
1283
  lines.push(' end');
1268
1284
  // Mermaid's default subgraph is a light box, which on a dark page reads as
1269
1285
  // the foreground rather than as the container. An agent nobody entered is
package/dist/kernel.d.ts CHANGED
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { Agent, AgentRegistry } from './agent.ts';
3
3
  import type { PendingToolCall } from './events.ts';
4
4
  import { type IdClock } from './ids.ts';
5
- import { type MemoryOpSpec, type MemoryRecallSpec } from './memory.ts';
5
+ import type { MemoryOpSpec, MemoryRecallSpec } from './memory/types.ts';
6
6
  import type { ModelRequest, ModelResponse } from './model.ts';
7
7
  import type { Services } from './services.ts';
8
8
  import { type Skill } from './skills.ts';
package/dist/kernel.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { Agent, AgentRegistry, handoffTarget, handoffTool } from "./agent.js";
3
3
  import { systemClock } from "./ids.js";
4
- import { memoryTools } from "./memory.js";
4
+ import { memoryInstructions, renderPreferences } from "./memory/instructions.js";
5
+ import { memoryTools } from "./memory/tools.js";
5
6
  import { hash } from "./payload.js";
6
7
  import { composePrompt } from "./prompt.js";
7
8
  import { allows, lockedSkillTools, renderSkillIndex, renderSkills, skillContentHash, skillTools, } from "./skills.js";
@@ -285,7 +286,10 @@ export async function resolveTools(state, reg, env) {
285
286
  for (const name of agent.handoffs) {
286
287
  tools.push(handoffTool(name, reg.find(name)?.description));
287
288
  }
288
- tools.push(...memoryTools(agent.memoryBindings(ctx)));
289
+ const binding = agent.memoryBinding(ctx);
290
+ if (binding && env.services.memory) {
291
+ tools.push(...memoryTools({ index: env.services.memory, binding }));
292
+ }
289
293
  if (agent.skills) {
290
294
  const binding = agent.skills;
291
295
  tools.push(...skillTools(binding));
@@ -318,14 +322,24 @@ function toSchema(t) {
318
322
  return { name: t.name, description: t.description, parameters: t.parameters };
319
323
  }
320
324
  /**
321
- * Prompt text the runtime owns rather than the author: the skill index and the
322
- * `final_output` instruction. Both used to be appended inside `buildRequest`,
323
- * which meant part of the system prompt existed in no node and showed up in no
324
- * audit. Composed in now, always last, so the volatile tail never invalidates
325
- * the cacheable prefix.
325
+ * Prompt text the runtime owns rather than the author: how to use memory, the
326
+ * user's standing preferences, the skill index and the `final_output`
327
+ * instruction. Most used to be appended inside `buildRequest`, which meant part
328
+ * of the system prompt existed in no node and showed up in no audit. Composed
329
+ * in now, always last, so the volatile tail never invalidates the cacheable
330
+ * prefix.
326
331
  */
327
332
  async function derivedPrompt(agent, state, env) {
328
333
  const out = [];
334
+ const memory = agent.memoryBinding(state.context);
335
+ if (memory && env.services.memory) {
336
+ // The one part of the prompt that reads mutable state. It is allowed
337
+ // because preferences change at most once in a run and
338
+ // `applySystemPrompt` is keyed on the rendered bytes, so a commit
339
+ // re-renders the prefix once rather than every turn.
340
+ out.push(memoryInstructions(memory));
341
+ out.push(renderPreferences(env.services.memory.preferences(memory.sees)));
342
+ }
329
343
  if (agent.skills?.discovery === 'index') {
330
344
  const binding = agent.skills;
331
345
  const provider = env.services.skillProvider(binding.provider);
@@ -654,11 +668,14 @@ export async function applyToolResult(state, callId, outcome, env) {
654
668
  durationMs: outcome.durationMs,
655
669
  });
656
670
  for (const effect of outcome.effects ?? []) {
657
- if (effect.kind === 'memory_op') {
671
+ if (effect.kind === 'skill_load') {
672
+ await addSkillLoad(b, env, effect.spec);
673
+ }
674
+ else if (effect.spec.kind === 'op') {
658
675
  await addMemoryOp(b, env, effect.spec);
659
676
  }
660
677
  else {
661
- await addSkillLoad(b, env, effect.spec);
678
+ await addMemoryRecall(b, env, effect.spec);
662
679
  }
663
680
  }
664
681
  const target = outcome.isError ? undefined : handoffTarget(call.name);
@@ -694,13 +711,20 @@ async function addMemoryOp(b, env, spec) {
694
711
  b.add({
695
712
  type: 'memory_op',
696
713
  op: spec.op,
697
- store: spec.store,
698
- scope: spec.scope,
699
714
  opId: spec.opId,
700
- recordId: spec.recordId,
701
- revision: spec.revision,
702
- before: spec.before === undefined ? undefined : await put(env, spec.before),
703
- after: spec.after === undefined ? undefined : await put(env, spec.after),
715
+ nodes: spec.nodes,
716
+ edges: spec.edges,
717
+ files: spec.files,
718
+ });
719
+ }
720
+ async function addMemoryRecall(b, env, spec) {
721
+ b.add({
722
+ type: 'memory_recall',
723
+ query: spec.query,
724
+ seeds: spec.seeds,
725
+ nodes: spec.nodes,
726
+ edges: spec.edges,
727
+ content: await put(env, spec.content),
704
728
  });
705
729
  }
706
730
  async function addSkillLoad(b, env, spec) {
@@ -724,14 +748,7 @@ export async function applyMemoryEffect(state, effect, env) {
724
748
  await addMemoryOp(b, env, effect);
725
749
  }
726
750
  else {
727
- b.add({
728
- type: 'memory_recall',
729
- store: effect.store,
730
- scope: effect.scope,
731
- query: effect.query,
732
- hits: effect.hits,
733
- content: await put(env, effect.content),
734
- });
751
+ await addMemoryRecall(b, env, effect);
735
752
  }
736
753
  return b.commit({});
737
754
  }
@@ -0,0 +1,24 @@
1
+ import { type MemoryFile } from './types.ts';
2
+ /** Where remembered files appear in the agent's namespace. */
3
+ export declare const MEMORY_MOUNT = "/memory";
4
+ /**
5
+ * Big enough for any script or config worth re-running, small enough that the
6
+ * memory directory cannot become a place where build outputs go to hide.
7
+ */
8
+ export declare const MAX_FILE_BYTES: number;
9
+ export interface RememberOptions {
10
+ maxBytes?: number;
11
+ }
12
+ /**
13
+ * Copy `source` into the memory directory under `id` and describe it.
14
+ *
15
+ * The bytes are read once and both hashed and written from that one buffer, so
16
+ * the digest provably belongs to the copy that was kept — re-reading the source
17
+ * to hash it would leave a window for it to change in between.
18
+ */
19
+ export declare function rememberFile(dir: string, source: string, id: string, opts?: RememberOptions): Promise<MemoryFile>;
20
+ /** The host path of a remembered file, for reading it back or deleting it. */
21
+ export declare function hostPath(dir: string, file: MemoryFile): string;
22
+ /** Drop the bytes of a forgotten node. Missing is success — the goal is absence. */
23
+ export declare function forgetFile(dir: string, file: MemoryFile): Promise<void>;
24
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1,85 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { basename, extname, join } from 'node:path';
4
+ import { FILES_DIR } from "./store.js";
5
+ import { MemoryError } from "./types.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Remembered files
8
+ //
9
+ // An agent that writes a working script should be able to run it again next
10
+ // week, and a summary of a script is not a script. So the bytes are copied out
11
+ // of the workspace — which is disposable, and may be a container that is about
12
+ // to be discarded — into the memory directory, under the id of the node that
13
+ // describes them. The copy is the point: a reference to a workspace path would
14
+ // dangle the moment the run ends.
15
+ //
16
+ // The tree is mounted read-only at /memory. Nothing writes there through the
17
+ // file tools, so every file under it has a node that explains what it is, and
18
+ // the graph cannot acquire orphans that no recollection can ever surface.
19
+ // ---------------------------------------------------------------------------
20
+ /** Where remembered files appear in the agent's namespace. */
21
+ export const MEMORY_MOUNT = '/memory';
22
+ /**
23
+ * Big enough for any script or config worth re-running, small enough that the
24
+ * memory directory cannot become a place where build outputs go to hide.
25
+ */
26
+ export const MAX_FILE_BYTES = 2 * 1024 * 1024;
27
+ /** Extensions are for `run_command` and syntax hints, not a type system. */
28
+ const MAX_FORMAT_LEN = 16;
29
+ /**
30
+ * Copy `source` into the memory directory under `id` and describe it.
31
+ *
32
+ * The bytes are read once and both hashed and written from that one buffer, so
33
+ * the digest provably belongs to the copy that was kept — re-reading the source
34
+ * to hash it would leave a window for it to change in between.
35
+ */
36
+ export async function rememberFile(dir, source, id, opts = {}) {
37
+ const maxBytes = opts.maxBytes ?? MAX_FILE_BYTES;
38
+ const info = await describe(source);
39
+ if (!info.isFile()) {
40
+ throw new MemoryError(`${source} is not a regular file`, 'only files can be remembered; write a `fact` node describing a directory instead');
41
+ }
42
+ if (info.size > maxBytes) {
43
+ throw new MemoryError(`${source} is ${info.size} bytes, over the ${maxBytes} byte limit for a remembered file`, 'keep the artifact in the workspace and remember a `snippet` of the part that matters');
44
+ }
45
+ const bytes = await readFile(source);
46
+ if (bytes.byteLength > maxBytes) {
47
+ throw new MemoryError(`${source} grew past the ${maxBytes} byte limit while being read`);
48
+ }
49
+ const format = formatOf(source);
50
+ const name = id + (format ? `.${format}` : '');
51
+ await mkdir(join(dir, FILES_DIR), { recursive: true });
52
+ await writeFile(join(dir, FILES_DIR, name), bytes);
53
+ return {
54
+ path: `${MEMORY_MOUNT}/${name}`,
55
+ bytes: bytes.byteLength,
56
+ sha256: createHash('sha256').update(bytes).digest('hex'),
57
+ format,
58
+ };
59
+ }
60
+ /** The host path of a remembered file, for reading it back or deleting it. */
61
+ export function hostPath(dir, file) {
62
+ // Through `basename` so a stored path can never climb out of the tree.
63
+ return join(dir, FILES_DIR, basename(file.path));
64
+ }
65
+ /** Drop the bytes of a forgotten node. Missing is success — the goal is absence. */
66
+ export async function forgetFile(dir, file) {
67
+ await rm(hostPath(dir, file), { force: true });
68
+ }
69
+ async function describe(source) {
70
+ try {
71
+ return await stat(source);
72
+ }
73
+ catch {
74
+ throw new MemoryError(`${source} does not exist`, 'remember a file the run actually wrote, by the path the file tools use');
75
+ }
76
+ }
77
+ /**
78
+ * The extension drives how the file is run and rendered, so it is kept, but it
79
+ * becomes part of a filename this module chooses — hence the character class.
80
+ */
81
+ function formatOf(source) {
82
+ const ext = extname(source).slice(1).toLowerCase();
83
+ return /^[a-z0-9]+$/.test(ext) && ext.length <= MAX_FORMAT_LEN ? ext : '';
84
+ }
85
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1,56 @@
1
+ import { MultiDirectedGraph } from 'graphology';
2
+ import { type MemoryEdge, type MemoryEdgeAttrs, type MemoryNode, type Relation } from './types.ts';
3
+ export type Graphology = MultiDirectedGraph<MemoryNode, MemoryEdgeAttrs>;
4
+ export interface NodeDraft {
5
+ id: string;
6
+ kind: string;
7
+ text: string;
8
+ audience: string[];
9
+ metadata?: Record<string, unknown>;
10
+ file?: MemoryNode['file'];
11
+ }
12
+ export interface NodePatch {
13
+ kind?: string;
14
+ text?: string;
15
+ audience?: string[];
16
+ metadata?: Record<string, unknown>;
17
+ file?: MemoryNode['file'];
18
+ /** when given, the update fails unless the stored revision matches */
19
+ expectedRevision?: number;
20
+ }
21
+ /** Deterministic, so the same link committed twice stays one edge. */
22
+ export declare const edgeKey: (source: string, target: string, relation: Relation) => string;
23
+ export declare class MemoryGraph {
24
+ #private;
25
+ static from(serialized: unknown): MemoryGraph;
26
+ export(): ReturnType<Graphology['export']>;
27
+ get order(): number;
28
+ get size(): number;
29
+ has(id: string): boolean;
30
+ get(id: string): MemoryNode | undefined;
31
+ add(draft: NodeDraft, at: string): MemoryNode;
32
+ update(id: string, patch: NodePatch, at: string): MemoryNode;
33
+ /** Reading is what counts as use — see `memory_load`, not `memory_search`. */
34
+ touch(id: string, at: string): MemoryNode | undefined;
35
+ link(source: string, target: string, relation: Relation, at: string): void;
36
+ /** Drops the node and every edge touching it; the file it names is the store's to unlink. */
37
+ forget(id: string): MemoryNode | undefined;
38
+ nodes(sees?: readonly string[]): MemoryNode[];
39
+ edges(sees?: readonly string[]): MemoryEdge[];
40
+ /** Both directions: a memory is as often reached from its artifact as from its request. */
41
+ neighbors(id: string, sees?: readonly string[]): MemoryEdge[];
42
+ /** Edges between nodes already chosen — what makes a materialized subgraph closed. */
43
+ between(ids: Iterable<string>, sees?: readonly string[]): MemoryEdge[];
44
+ /**
45
+ * The nodes some visible node replaces. Recall drops these, which is what
46
+ * makes a correction take effect without erasing what was corrected.
47
+ */
48
+ superseded(sees?: readonly string[]): Set<string>;
49
+ }
50
+ export declare class RevisionMismatch extends Error {
51
+ readonly id: string;
52
+ readonly expected: number;
53
+ readonly actual: number;
54
+ constructor(id: string, expected: number, actual: number);
55
+ }
56
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1,159 @@
1
+ import { MultiDirectedGraph } from 'graphology';
2
+ import { edgeVisible, visible, } from "./types.js";
3
+ /** Deterministic, so the same link committed twice stays one edge. */
4
+ export const edgeKey = (source, target, relation) => `${source}|${relation}|${target}`;
5
+ export class MemoryGraph {
6
+ #graph = new MultiDirectedGraph();
7
+ static from(serialized) {
8
+ const g = new MemoryGraph();
9
+ g.#graph.import(serialized);
10
+ return g;
11
+ }
12
+ export() {
13
+ return this.#graph.export();
14
+ }
15
+ get order() {
16
+ return this.#graph.order;
17
+ }
18
+ get size() {
19
+ return this.#graph.size;
20
+ }
21
+ has(id) {
22
+ return this.#graph.hasNode(id);
23
+ }
24
+ get(id) {
25
+ return this.#graph.hasNode(id) ? this.#graph.getNodeAttributes(id) : undefined;
26
+ }
27
+ add(draft, at) {
28
+ const node = {
29
+ id: draft.id,
30
+ kind: draft.kind,
31
+ text: draft.text,
32
+ audience: draft.audience.length ? [...draft.audience] : ['*'],
33
+ createdAt: at,
34
+ updatedAt: at,
35
+ lastUsedAt: at,
36
+ useCount: 0,
37
+ revision: 1,
38
+ ...(draft.metadata ? { metadata: draft.metadata } : {}),
39
+ ...(draft.file ? { file: draft.file } : {}),
40
+ };
41
+ this.#graph.addNode(node.id, node);
42
+ return node;
43
+ }
44
+ update(id, patch, at) {
45
+ const current = this.#graph.getNodeAttributes(id);
46
+ if (patch.expectedRevision !== undefined && patch.expectedRevision !== current.revision) {
47
+ throw new RevisionMismatch(id, patch.expectedRevision, current.revision);
48
+ }
49
+ const next = {
50
+ ...current,
51
+ ...(patch.kind !== undefined ? { kind: patch.kind } : {}),
52
+ ...(patch.text !== undefined ? { text: patch.text } : {}),
53
+ ...(patch.audience !== undefined ? { audience: [...patch.audience] } : {}),
54
+ ...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
55
+ ...(patch.file !== undefined ? { file: patch.file } : {}),
56
+ updatedAt: at,
57
+ revision: current.revision + 1,
58
+ };
59
+ this.#graph.replaceNodeAttributes(id, next);
60
+ return next;
61
+ }
62
+ /** Reading is what counts as use — see `memory_load`, not `memory_search`. */
63
+ touch(id, at) {
64
+ if (!this.#graph.hasNode(id)) {
65
+ return undefined;
66
+ }
67
+ const node = this.#graph.getNodeAttributes(id);
68
+ const next = { ...node, lastUsedAt: at, useCount: node.useCount + 1 };
69
+ this.#graph.replaceNodeAttributes(id, next);
70
+ return next;
71
+ }
72
+ link(source, target, relation, at) {
73
+ this.#graph.mergeEdgeWithKey(edgeKey(source, target, relation), source, target, {
74
+ relation,
75
+ createdAt: at,
76
+ });
77
+ }
78
+ /** Drops the node and every edge touching it; the file it names is the store's to unlink. */
79
+ forget(id) {
80
+ const node = this.get(id);
81
+ if (node) {
82
+ this.#graph.dropNode(id);
83
+ }
84
+ return node;
85
+ }
86
+ nodes(sees) {
87
+ const out = [];
88
+ this.#graph.forEachNode((_id, attrs) => {
89
+ if (!sees || visible(attrs, sees)) {
90
+ out.push(attrs);
91
+ }
92
+ });
93
+ return out;
94
+ }
95
+ edges(sees) {
96
+ const out = [];
97
+ this.#graph.forEachEdge((_key, attrs, source, target) => {
98
+ if (sees && !this.#passable(source, target, attrs, sees)) {
99
+ return;
100
+ }
101
+ out.push({ source, target, relation: attrs.relation });
102
+ });
103
+ return out;
104
+ }
105
+ /** Both directions: a memory is as often reached from its artifact as from its request. */
106
+ neighbors(id, sees) {
107
+ if (!this.#graph.hasNode(id)) {
108
+ return [];
109
+ }
110
+ const out = [];
111
+ this.#graph.forEachEdge(id, (_key, attrs, source, target) => {
112
+ if (sees && !this.#passable(source, target, attrs, sees)) {
113
+ return;
114
+ }
115
+ out.push({ source, target, relation: attrs.relation });
116
+ });
117
+ return out;
118
+ }
119
+ /** Edges between nodes already chosen — what makes a materialized subgraph closed. */
120
+ between(ids, sees) {
121
+ const set = new Set(ids);
122
+ return this.edges(sees).filter((e) => set.has(e.source) && set.has(e.target));
123
+ }
124
+ /**
125
+ * The nodes some visible node replaces. Recall drops these, which is what
126
+ * makes a correction take effect without erasing what was corrected.
127
+ */
128
+ superseded(sees) {
129
+ const out = new Set();
130
+ this.#graph.forEachEdge((_key, attrs, source, target) => {
131
+ if (attrs.relation !== 'SUPERSEDES') {
132
+ return;
133
+ }
134
+ if (sees && !this.#passable(source, target, attrs, sees)) {
135
+ return;
136
+ }
137
+ out.add(target);
138
+ });
139
+ return out;
140
+ }
141
+ #passable(source, target, attrs, sees) {
142
+ return (edgeVisible(attrs, sees) &&
143
+ visible(this.#graph.getNodeAttributes(source), sees) &&
144
+ visible(this.#graph.getNodeAttributes(target), sees));
145
+ }
146
+ }
147
+ export class RevisionMismatch extends Error {
148
+ id;
149
+ expected;
150
+ actual;
151
+ constructor(id, expected, actual) {
152
+ super(`memory ${id} has revision ${actual}, not ${expected}`);
153
+ this.name = 'RevisionMismatch';
154
+ this.id = id;
155
+ this.expected = expected;
156
+ this.actual = actual;
157
+ }
158
+ }
159
+ //# sourceMappingURL=graph.js.map