@zenera/neo 1.1.11 → 1.1.14

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 (68) 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/embeddings/limiter.js +14 -3
  7. package/dist/index.d.ts +12 -2
  8. package/dist/index.js +12 -2
  9. package/dist/inspect/client.d.ts +1 -1
  10. package/dist/inspect/client.js +440 -26
  11. package/dist/inspect/css.d.ts +1 -1
  12. package/dist/inspect/css.js +49 -0
  13. package/dist/inspect/index.d.ts +3 -3
  14. package/dist/inspect/index.js +1 -1
  15. package/dist/inspect/page.js +16 -0
  16. package/dist/inspect/report.d.ts +25 -0
  17. package/dist/inspect/report.js +45 -0
  18. package/dist/kernel.d.ts +1 -1
  19. package/dist/kernel.js +40 -23
  20. package/dist/memory/files.d.ts +24 -0
  21. package/dist/memory/files.js +85 -0
  22. package/dist/memory/graph.d.ts +56 -0
  23. package/dist/memory/graph.js +159 -0
  24. package/dist/memory/index.d.ts +93 -0
  25. package/dist/memory/index.js +357 -0
  26. package/dist/memory/instructions.d.ts +15 -0
  27. package/dist/memory/instructions.js +103 -0
  28. package/dist/memory/page.d.ts +8 -0
  29. package/dist/memory/page.js +797 -0
  30. package/dist/memory/recall.d.ts +35 -0
  31. package/dist/memory/recall.js +161 -0
  32. package/dist/memory/render.d.ts +10 -0
  33. package/dist/memory/render.js +228 -0
  34. package/dist/memory/report.d.ts +69 -0
  35. package/dist/memory/report.js +131 -0
  36. package/dist/memory/store.d.ts +49 -0
  37. package/dist/memory/store.js +226 -0
  38. package/dist/memory/tools.d.ts +11 -0
  39. package/dist/memory/tools.js +337 -0
  40. package/dist/memory/types.d.ts +193 -0
  41. package/dist/memory/types.js +105 -0
  42. package/dist/memory/vectors.d.ts +29 -0
  43. package/dist/memory/vectors.js +152 -0
  44. package/dist/project/config.d.ts +49 -1
  45. package/dist/project/config.js +63 -0
  46. package/dist/project/index.d.ts +2 -2
  47. package/dist/project/index.js +1 -1
  48. package/dist/project/load.d.ts +16 -2
  49. package/dist/project/load.js +67 -1
  50. package/dist/runner.d.ts +4 -2
  51. package/dist/runner.js +29 -30
  52. package/dist/services.d.ts +23 -6
  53. package/dist/services.js +25 -15
  54. package/dist/tools/workspace.js +195 -17
  55. package/dist/trajectory.d.ts +14 -12
  56. package/dist/types.d.ts +5 -5
  57. package/dist/types.js +3 -3
  58. package/package.json +2 -5
  59. package/dist/memory-stores/factory.d.ts +0 -15
  60. package/dist/memory-stores/factory.js +0 -29
  61. package/dist/memory-stores/file.d.ts +0 -37
  62. package/dist/memory-stores/file.js +0 -174
  63. package/dist/memory-stores/in-memory.d.ts +0 -17
  64. package/dist/memory-stores/in-memory.js +0 -80
  65. package/dist/memory-stores/index.d.ts +0 -4
  66. package/dist/memory-stores/index.js +0 -5
  67. package/dist/memory.d.ts +0 -115
  68. package/dist/memory.js +0 -213
@@ -87,6 +87,55 @@ table.st tfoot td:first-child { color: var(--dim); text-transform: uppercase;
87
87
  .gtools button:hover { border-color: var(--accent); }
88
88
  .gtools .hint:first-of-type { margin-left: 8px; }
89
89
  .gtools .lvl { margin-left: auto; font-family: var(--mono); }
90
+ .gtools label.check { display: flex; align-items: center; gap: 5px; margin-left: 8px;
91
+ color: var(--dim); font-size: 12px; cursor: pointer; user-select: none; }
92
+ /* The memory view reads left to right: the graph is the index, the panel is
93
+ the page. Putting the body under a tab of its own would mean losing the
94
+ picture every time you asked what a node actually says. */
95
+ #memory .split { display: flex; flex: 1; overflow: hidden; }
96
+ #mdetail { width: 380px; flex: none; padding: 16px; overflow: auto;
97
+ border-right: none; border-left: 1px solid var(--line); }
98
+ #mdetail .dt { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 10px; }
99
+ #mdetail .idrow { display: flex; gap: 8px; align-items: center; margin: 0 0 14px; }
100
+ #mdetail .idrow code { flex: 1; min-width: 0; font: 12px var(--mono); color: var(--fg);
101
+ background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
102
+ padding: 4px 8px; word-break: break-all; }
103
+ #mdetail .idrow button { flex: 0 0 auto; background: var(--bg); border: 1px solid var(--line);
104
+ color: var(--dim); border-radius: 6px; padding: 4px 9px; font: 11px var(--mono);
105
+ cursor: pointer; }
106
+ #mdetail .idrow button:hover { border-color: var(--accent); color: var(--fg); }
107
+ #mdetail dl.props { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 5px 14px;
108
+ margin: 0 0 14px; font: 12px var(--mono); }
109
+ #mdetail dl.props dt { color: var(--dim); }
110
+ #mdetail dl.props dd { margin: 0; word-break: break-word; }
111
+ /* A block that shares the panel's own background has no edges to show. */
112
+ #mdetail section.blk { background: var(--bg); }
113
+ #mdetail ul.links { list-style: none; margin: 0; padding: 0; }
114
+ #mdetail ul.links li { display: grid; grid-template-columns: 96px auto minmax(0, 1fr);
115
+ gap: 8px; align-items: baseline; padding: 6px 12px; }
116
+ #mdetail ul.links li + li { border-top: 1px solid var(--line); }
117
+ #mdetail ul.links .rel { font: 11px var(--mono); color: var(--warn); white-space: nowrap; }
118
+ #mdetail button.link { font: 12px/1.4 system-ui, sans-serif; padding: 0; text-align: left;
119
+ overflow-wrap: anywhere; }
120
+ #mdetail button.link:hover { text-decoration: underline; }
121
+ #mcount .ok { color: var(--ok); }
122
+ #mcount .bad { color: var(--err); }
123
+ /* The kind palette is the memory page's, so a node reads the same in both. */
124
+ .kind { font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
125
+ padding: 1px 6px; border-radius: 4px; background: #232a38; color: var(--dim); }
126
+ .kind.task { background: #1e3357; color: #9dc1ff; }
127
+ .kind.plan { background: #3a2450; color: #d1a6ff; }
128
+ .kind.fact { background: #14392c; color: #8ee0c2; }
129
+ .kind.snippet { background: #1e3d33; color: #8ee0c2; }
130
+ .kind.file { background: #40331a; color: #f0dfb4; }
131
+ .kind.operation { background: #3d2a1b; color: #f2d9c2; }
132
+ .kind.preference { background: #3d1f2c; color: #f5b8cd; }
133
+ /* Selection is a class rather than attributes written onto the shapes: the
134
+ previous pick has to lose its outline without a redraw. White, because the
135
+ accent already means "the recall matched this one". */
136
+ #memory .node.picked rect, #memory .node.picked polygon,
137
+ #memory .node.picked path, #memory .node.picked circle {
138
+ stroke: var(--fg) !important; stroke-width: 4px !important; }
90
139
  /* The canvas is transformed, not scrolled: panning has to work past the edges
91
140
  of the diagram, which overflow:auto would forbid. */
92
141
  .viewport { flex: 1; overflow: hidden; position: relative; cursor: grab; touch-action: none;
@@ -1,7 +1,7 @@
1
- import { type ReportOptions, type RunReport } from './report.ts';
2
- import type { AgentState } from '../state.ts';
3
1
  import type { PayloadResolver } from '../payload.ts';
4
- export { buildRunReport, MAX_BLOB_BYTES, MERMAID_URL, type ReportOptions, type RunReport, } from './report.ts';
2
+ import type { AgentState } from '../state.ts';
3
+ import { type ReportOptions, type RunReport } from './report.ts';
4
+ export { buildRunReport, MAX_BLOB_BYTES, MAX_MEMORY_FILE_BYTES, MERMAID_URL, type ReportOptions, type RunMemory, type RunReport, } from './report.ts';
5
5
  /** Report → one standalone HTML document. */
6
6
  export declare function renderReportHtml(report: RunReport, opts?: ReportOptions): string;
7
7
  /** The usual entry point: state in, HTML out. */
@@ -1,6 +1,6 @@
1
1
  import { page } from "./page.js";
2
2
  import { buildRunReport, MERMAID_URL } from "./report.js";
3
- export { buildRunReport, MAX_BLOB_BYTES, MERMAID_URL, } from "./report.js";
3
+ export { buildRunReport, MAX_BLOB_BYTES, MAX_MEMORY_FILE_BYTES, MERMAID_URL, } from "./report.js";
4
4
  /** Report → one standalone HTML document. */
5
5
  export function renderReportHtml(report, opts = {}) {
6
6
  return page(report, opts.mermaidUrl ?? MERMAID_URL);
@@ -29,6 +29,7 @@ export function page(report, mermaidUrl) {
29
29
  <button data-view="detail">Detail</button>
30
30
  <button data-view="stats">Stats</button>
31
31
  <button data-view="agents">Agents</button>
32
+ <button data-view="memory" hidden>Memory</button>
32
33
  </div>
33
34
  <div id="graph" class="pane">
34
35
  <div class="gtools">
@@ -54,6 +55,21 @@ export function page(report, mermaidUrl) {
54
55
  </div>
55
56
  <div class="viewport"><div class="canvas"><div class="hint pad">rendering…</div></div></div>
56
57
  </div>
58
+ <div id="memory" class="pane" hidden>
59
+ <div class="gtools">
60
+ <button data-zoom="out" title="zoom out">−</button>
61
+ <button data-zoom="in" title="zoom in">+</button>
62
+ <button data-zoom="fit" title="fit to window (double-click the canvas)">fit</button>
63
+ <button data-zoom="reset" title="actual size">1:1</button>
64
+ <label class="check"><input type="checkbox" id="madded"> show what it wrote</label>
65
+ <span class="hint" id="mcount"></span>
66
+ <span class="hint lvl">100%</span>
67
+ </div>
68
+ <div class="split">
69
+ <div class="viewport"><div class="canvas"><div class="hint pad">rendering…</div></div></div>
70
+ <aside id="mdetail"><div class="hint pad">Pick a node.</div></aside>
71
+ </div>
72
+ </div>
57
73
  </section>
58
74
  </main>
59
75
  <script id="run-data" type="application/json">${embedJson(report)}</script>
@@ -1,10 +1,16 @@
1
1
  import type { Architecture } from '../architecture.ts';
2
+ import { type ReportNode } from '../memory/report.ts';
3
+ import type { MemoryStore } from '../memory/store.ts';
4
+ import type { MemoryEdge } from '../memory/types.ts';
2
5
  import { type PayloadResolver } from '../payload.ts';
3
6
  import type { AgentState } from '../state.ts';
4
7
  /** Default per-payload inline cap. Keeps one runaway tool result from turning
5
8
  * a report into a file no editor will open. */
6
9
  export declare const MAX_BLOB_BYTES: number;
7
10
  export declare const MERMAID_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
11
+ /** Deliberately meaner than the memory page's own cap: this file already holds
12
+ * a whole run, and one remembered artifact should not decide whether it opens. */
13
+ export declare const MAX_MEMORY_FILE_BYTES: number;
8
14
  export interface ReportOptions {
9
15
  title?: string;
10
16
  /** per-payload inline cap; larger values are cut, with a visible marker */
@@ -22,6 +28,14 @@ export interface ReportOptions {
22
28
  * strictly smaller picture but never a wrong one.
23
29
  */
24
30
  architecture?: Architecture;
31
+ /**
32
+ * The memory to resolve recalled and committed nodes against. A trajectory
33
+ * records ids, kinds and edges but no bodies, so without this the memory
34
+ * view can draw the shape of what was read and not a word of it.
35
+ */
36
+ memory?: MemoryStore;
37
+ /** ceiling on an inlined remembered file; 0 to inline none */
38
+ maxMemoryFileBytes?: number;
25
39
  }
26
40
  /** Everything the page needs, and nothing that needs a store to interpret. */
27
41
  export interface RunReport {
@@ -44,6 +58,17 @@ export interface RunReport {
44
58
  media: string[];
45
59
  /** the declared wiring, when the caller had a runner to ask */
46
60
  architecture?: Architecture;
61
+ /**
62
+ * The memory nodes this run read or wrote, as the graph holds them *now* —
63
+ * which is later than the run. The page says so where it matters: a node
64
+ * can have moved on a revision since, or be gone from the graph entirely.
65
+ */
66
+ memory?: RunMemory;
67
+ }
68
+ export interface RunMemory {
69
+ dir: string;
70
+ nodes: ReportNode[];
71
+ edges: MemoryEdge[];
47
72
  }
48
73
  /**
49
74
  * Resolves a state into a self-describing report. Separate from rendering so a
@@ -1,3 +1,4 @@
1
+ import { buildMemorySlice } from "../memory/report.js";
1
2
  import { collectPayloads } from "../payload.js";
2
3
  // ---------------------------------------------------------------------------
3
4
  // Run inspector — a single HTML file that explains a run
@@ -12,6 +13,9 @@ import { collectPayloads } from "../payload.js";
12
13
  * a report into a file no editor will open. */
13
14
  export const MAX_BLOB_BYTES = 512 * 1024;
14
15
  export const MERMAID_URL = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
16
+ /** Deliberately meaner than the memory page's own cap: this file already holds
17
+ * a whole run, and one remembered artifact should not decide whether it opens. */
18
+ export const MAX_MEMORY_FILE_BYTES = 64 * 1024;
15
19
  /** Base64 data uris only: the charset ends at the closing JSON quote. */
16
20
  const DATA_URL = /data:[\w.+-]+\/[\w.+-]+;base64,[A-Za-z0-9+/=]+/g;
17
21
  /** Below this a uri is cheaper to leave in place than to indirect through. */
@@ -57,6 +61,47 @@ export async function buildRunReport(state, payloads, opts = {}) {
57
61
  truncated,
58
62
  media,
59
63
  architecture: opts.architecture,
64
+ memory: opts.memory ? await remembered(opts.memory, state, media, opts) : undefined,
65
+ };
66
+ }
67
+ /** Every memory node the run touched, branches included. */
68
+ function memoryIds(nodes, into) {
69
+ for (const node of nodes) {
70
+ if (node.type === 'memory_recall' || node.type === 'memory_op') {
71
+ for (const n of node.nodes) {
72
+ into.add(n.id);
73
+ }
74
+ }
75
+ else if (node.type === 'join') {
76
+ for (const branch of node.branches) {
77
+ memoryIds(branch.nodes, into);
78
+ }
79
+ }
80
+ }
81
+ return into;
82
+ }
83
+ async function remembered(store, state, media, opts) {
84
+ const ids = memoryIds(state.trajectory, new Set());
85
+ if (!ids.size) {
86
+ return undefined;
87
+ }
88
+ let slice;
89
+ try {
90
+ slice = await buildMemorySlice(store, [...ids], {
91
+ maxContentBytes: opts.maxMemoryFileBytes ?? MAX_MEMORY_FILE_BYTES,
92
+ });
93
+ }
94
+ catch {
95
+ // A memory that cannot be read is a view the page does without; it is
96
+ // not a reason to lose the record of the run.
97
+ return undefined;
98
+ }
99
+ return {
100
+ dir: store.dir,
101
+ // Remembered images go through the same table as the run's own, so a
102
+ // picture the agent both saw and filed is carried once.
103
+ nodes: slice.nodes.map((n) => (n.image ? { ...n, image: hoistMedia(n.image, media) } : n)),
104
+ edges: slice.edges,
60
105
  };
61
106
  }
62
107
  //# sourceMappingURL=report.js.map
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