@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.
- package/README.md +3 -3
- package/dist/agent.d.ts +8 -7
- package/dist/agent.js +15 -8
- package/dist/architecture.d.ts +7 -5
- package/dist/architecture.js +20 -12
- package/dist/embeddings/limiter.js +14 -3
- package/dist/index.d.ts +12 -2
- package/dist/index.js +12 -2
- package/dist/inspect/client.d.ts +1 -1
- package/dist/inspect/client.js +440 -26
- package/dist/inspect/css.d.ts +1 -1
- package/dist/inspect/css.js +49 -0
- package/dist/inspect/index.d.ts +3 -3
- package/dist/inspect/index.js +1 -1
- package/dist/inspect/page.js +16 -0
- package/dist/inspect/report.d.ts +25 -0
- package/dist/inspect/report.js +45 -0
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.js +40 -23
- package/dist/memory/files.d.ts +24 -0
- package/dist/memory/files.js +85 -0
- package/dist/memory/graph.d.ts +56 -0
- package/dist/memory/graph.js +159 -0
- package/dist/memory/index.d.ts +93 -0
- package/dist/memory/index.js +357 -0
- package/dist/memory/instructions.d.ts +15 -0
- package/dist/memory/instructions.js +103 -0
- package/dist/memory/page.d.ts +8 -0
- package/dist/memory/page.js +797 -0
- package/dist/memory/recall.d.ts +35 -0
- package/dist/memory/recall.js +161 -0
- package/dist/memory/render.d.ts +10 -0
- package/dist/memory/render.js +228 -0
- package/dist/memory/report.d.ts +69 -0
- package/dist/memory/report.js +131 -0
- package/dist/memory/store.d.ts +49 -0
- package/dist/memory/store.js +226 -0
- package/dist/memory/tools.d.ts +11 -0
- package/dist/memory/tools.js +337 -0
- package/dist/memory/types.d.ts +193 -0
- package/dist/memory/types.js +105 -0
- package/dist/memory/vectors.d.ts +29 -0
- package/dist/memory/vectors.js +152 -0
- package/dist/project/config.d.ts +49 -1
- package/dist/project/config.js +63 -0
- package/dist/project/index.d.ts +2 -2
- package/dist/project/index.js +1 -1
- package/dist/project/load.d.ts +16 -2
- package/dist/project/load.js +67 -1
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +29 -30
- package/dist/services.d.ts +23 -6
- package/dist/services.js +25 -15
- package/dist/tools/workspace.js +195 -17
- package/dist/trajectory.d.ts +14 -12
- package/dist/types.d.ts +5 -5
- package/dist/types.js +3 -3
- package/package.json +2 -5
- package/dist/memory-stores/factory.d.ts +0 -15
- package/dist/memory-stores/factory.js +0 -29
- package/dist/memory-stores/file.d.ts +0 -37
- package/dist/memory-stores/file.js +0 -174
- package/dist/memory-stores/in-memory.d.ts +0 -17
- package/dist/memory-stores/in-memory.js +0 -80
- package/dist/memory-stores/index.d.ts +0 -4
- package/dist/memory-stores/index.js +0 -5
- package/dist/memory.d.ts +0 -115
- package/dist/memory.js +0 -213
package/dist/inspect/css.js
CHANGED
|
@@ -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;
|
package/dist/inspect/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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. */
|
package/dist/inspect/index.js
CHANGED
|
@@ -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);
|
package/dist/inspect/page.js
CHANGED
|
@@ -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>
|
package/dist/inspect/report.d.ts
CHANGED
|
@@ -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
|
package/dist/inspect/report.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
* the cacheable
|
|
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 === '
|
|
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
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|
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
|