@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.
- 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/index.d.ts +12 -2
- package/dist/index.js +12 -2
- package/dist/inspect/client.d.ts +1 -1
- package/dist/inspect/client.js +42 -26
- 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 +57 -0
- package/dist/memory/report.js +113 -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 +310 -0
- package/dist/memory/types.d.ts +192 -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/client.js
CHANGED
|
@@ -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.
|
|
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),
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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 + ' ' +
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
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: []
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
if (
|
|
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
|
|
1259
|
-
const used = USED.memory.get(a.name
|
|
1273
|
+
if (a.memory) {
|
|
1274
|
+
const used = USED.memory.get(a.name);
|
|
1260
1275
|
const k = archKey(used);
|
|
1261
|
-
|
|
1262
|
-
|
|
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 {
|
|
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
|