@zenera/neo 1.1.11 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +3 -3
  2. package/dist/agent.d.ts +8 -7
  3. package/dist/agent.js +15 -8
  4. package/dist/architecture.d.ts +7 -5
  5. package/dist/architecture.js +20 -12
  6. package/dist/index.d.ts +12 -2
  7. package/dist/index.js +12 -2
  8. package/dist/inspect/client.d.ts +1 -1
  9. package/dist/inspect/client.js +42 -26
  10. package/dist/kernel.d.ts +1 -1
  11. package/dist/kernel.js +40 -23
  12. package/dist/memory/files.d.ts +24 -0
  13. package/dist/memory/files.js +85 -0
  14. package/dist/memory/graph.d.ts +56 -0
  15. package/dist/memory/graph.js +159 -0
  16. package/dist/memory/index.d.ts +93 -0
  17. package/dist/memory/index.js +357 -0
  18. package/dist/memory/instructions.d.ts +15 -0
  19. package/dist/memory/instructions.js +103 -0
  20. package/dist/memory/page.d.ts +8 -0
  21. package/dist/memory/page.js +797 -0
  22. package/dist/memory/recall.d.ts +35 -0
  23. package/dist/memory/recall.js +161 -0
  24. package/dist/memory/render.d.ts +10 -0
  25. package/dist/memory/render.js +228 -0
  26. package/dist/memory/report.d.ts +57 -0
  27. package/dist/memory/report.js +113 -0
  28. package/dist/memory/store.d.ts +49 -0
  29. package/dist/memory/store.js +226 -0
  30. package/dist/memory/tools.d.ts +11 -0
  31. package/dist/memory/tools.js +310 -0
  32. package/dist/memory/types.d.ts +192 -0
  33. package/dist/memory/types.js +105 -0
  34. package/dist/memory/vectors.d.ts +29 -0
  35. package/dist/memory/vectors.js +152 -0
  36. package/dist/project/config.d.ts +49 -1
  37. package/dist/project/config.js +63 -0
  38. package/dist/project/index.d.ts +2 -2
  39. package/dist/project/index.js +1 -1
  40. package/dist/project/load.d.ts +16 -2
  41. package/dist/project/load.js +67 -1
  42. package/dist/runner.d.ts +4 -2
  43. package/dist/runner.js +29 -30
  44. package/dist/services.d.ts +23 -6
  45. package/dist/services.js +25 -15
  46. package/dist/tools/workspace.js +195 -17
  47. package/dist/trajectory.d.ts +14 -12
  48. package/dist/types.d.ts +5 -5
  49. package/dist/types.js +3 -3
  50. package/package.json +2 -5
  51. package/dist/memory-stores/factory.d.ts +0 -15
  52. package/dist/memory-stores/factory.js +0 -29
  53. package/dist/memory-stores/file.d.ts +0 -37
  54. package/dist/memory-stores/file.js +0 -174
  55. package/dist/memory-stores/in-memory.d.ts +0 -17
  56. package/dist/memory-stores/in-memory.js +0 -80
  57. package/dist/memory-stores/index.d.ts +0 -4
  58. package/dist/memory-stores/index.js +0 -5
  59. package/dist/memory.d.ts +0 -115
  60. package/dist/memory.js +0 -213
@@ -0,0 +1,93 @@
1
+ import type { Embedder } from '../embedding.ts';
2
+ import type { IdClock } from '../ids.ts';
3
+ import type { MemoryGraph } from './graph.ts';
4
+ import type { MemoryStore } from './store.ts';
5
+ import { type MemoryNode, type MemoryQuery, type Recollection } from './types.ts';
6
+ /** Past this a `load` returns the path and lets the agent open it itself. */
7
+ export declare const INLINE_FILE_BYTES: number;
8
+ /**
9
+ * Cosine at or above this is one memory written twice, not two memories. Set
10
+ * high on purpose: merging two things that only resembled each other loses a
11
+ * memory outright, while missing a merge only wastes a node.
12
+ */
13
+ export declare const DUPLICATE_SCORE = 0.97;
14
+ export interface MemoryIndexOptions {
15
+ store: MemoryStore;
16
+ /** absent ranks by term overlap and stores no vectors */
17
+ embedder?: Embedder;
18
+ /** the project's vocabulary, when it extends the built-in one */
19
+ kinds?: readonly string[];
20
+ relations?: readonly string[];
21
+ }
22
+ export interface CommitNode {
23
+ /** names this node within the transaction; for a node being created */
24
+ ref?: string;
25
+ /** an existing node to update; mutually exclusive with `ref` */
26
+ id?: string;
27
+ kind?: string;
28
+ text?: string;
29
+ audience?: string[];
30
+ metadata?: Record<string, unknown>;
31
+ /** absolute host path of a file to copy in, already resolved by the caller */
32
+ file?: {
33
+ source: string;
34
+ };
35
+ expectedRevision?: number;
36
+ }
37
+ export interface CommitEdge {
38
+ from: string;
39
+ to: string;
40
+ relation: string;
41
+ }
42
+ export interface MemoryTransaction {
43
+ nodes?: CommitNode[];
44
+ edges?: CommitEdge[];
45
+ }
46
+ export interface CommitOptions {
47
+ /** audience labels this agent may assign */
48
+ writes: readonly string[];
49
+ /** labels it may read; a duplicate is only a duplicate if it can see it */
50
+ sees: readonly string[];
51
+ clock: IdClock;
52
+ }
53
+ export interface CommitResult {
54
+ /** local ref (or id, for an update) to the id it resolved to */
55
+ ids: Record<string, string>;
56
+ created: number;
57
+ updated: number;
58
+ edges: number;
59
+ files: number;
60
+ /** new nodes that turned out to already exist, and were folded into them */
61
+ merged: number;
62
+ }
63
+ export interface LoadedNode {
64
+ node: MemoryNode;
65
+ /** the file's bytes, when it was small enough to inline */
66
+ content?: string;
67
+ }
68
+ export declare class MemoryIndex {
69
+ #private;
70
+ readonly store: MemoryStore;
71
+ constructor(opts: MemoryIndexOptions);
72
+ get graph(): MemoryGraph;
73
+ get kinds(): readonly string[];
74
+ get relations(): readonly string[];
75
+ /** Read-only: a search returns nodes the model has not read, so nothing is touched. */
76
+ search(query: MemoryQuery, sees: readonly string[]): Promise<Recollection>;
77
+ /**
78
+ * Listed rather than ranked, and deliberately not routed through `recall`:
79
+ * ranking decays by `lastUsedAt`, only `memory_load` bumps that, and a
80
+ * preference is injected rather than loaded — so it would age out of its
81
+ * own list. Ordered so the rendered block is byte-stable between runs,
82
+ * because an unstable one would invalidate the prompt prefix for nothing.
83
+ */
84
+ preferences(sees: readonly string[]): MemoryNode[];
85
+ /**
86
+ * The only path that counts as use — recency decay is meant to track what
87
+ * an agent actually read, and a search hands back context it never opened.
88
+ */
89
+ load(ids: readonly string[], sees: readonly string[], clock: IdClock): Promise<LoadedNode[]>;
90
+ commit(tx: MemoryTransaction, opts: CommitOptions): Promise<CommitResult>;
91
+ forget(ids: readonly string[], sees: readonly string[]): Promise<MemoryNode[]>;
92
+ }
93
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,357 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { forgetFile, hostPath, rememberFile } from "./files.js";
3
+ import { recall } from "./recall.js";
4
+ import { ALL_AGENTS, MEMORY_KINDS, MEMORY_RELATIONS, MemoryError, PREFERENCE_KIND, visible, } from "./types.js";
5
+ // ---------------------------------------------------------------------------
6
+ // The memory, as the engine uses it
7
+ //
8
+ // `MemoryStore` knows how to persist a graph; this knows what the operations
9
+ // mean. It owns the two things that must not drift: the vector for a node's
10
+ // text, and the bytes of a remembered file.
11
+ //
12
+ // `commit` is one transaction. The model names nodes it is creating by a local
13
+ // `ref` — it cannot know an id that has not been minted — and edges join refs
14
+ // and existing ids indiscriminately, so a whole subgraph arrives in one call.
15
+ // Everything is validated, and every source file stat-ed, before the first
16
+ // mutation, on the same principle as `runPatch` in tools/workspace.ts: a
17
+ // commit that fails must leave no half-built graph behind.
18
+ // ---------------------------------------------------------------------------
19
+ /** Past this a `load` returns the path and lets the agent open it itself. */
20
+ export const INLINE_FILE_BYTES = 32 * 1024;
21
+ /**
22
+ * Cosine at or above this is one memory written twice, not two memories. Set
23
+ * high on purpose: merging two things that only resembled each other loses a
24
+ * memory outright, while missing a merge only wastes a node.
25
+ */
26
+ export const DUPLICATE_SCORE = 0.97;
27
+ export class MemoryIndex {
28
+ store;
29
+ #embedder;
30
+ #kinds;
31
+ #relations;
32
+ constructor(opts) {
33
+ this.store = opts.store;
34
+ this.#embedder = opts.embedder;
35
+ this.#kinds = new Set([...(opts.kinds ?? MEMORY_KINDS), PREFERENCE_KIND]);
36
+ this.#relations = new Set(opts.relations ?? MEMORY_RELATIONS);
37
+ }
38
+ get graph() {
39
+ return this.store.graph;
40
+ }
41
+ get kinds() {
42
+ return [...this.#kinds];
43
+ }
44
+ get relations() {
45
+ return [...this.#relations];
46
+ }
47
+ /** Read-only: a search returns nodes the model has not read, so nothing is touched. */
48
+ async search(query, sees) {
49
+ const vector = query.text ? await this.#embed([query.text], 'query') : undefined;
50
+ return recall({
51
+ graph: this.graph,
52
+ query,
53
+ sees,
54
+ vector: vector?.[0],
55
+ vectors: this.store.vectors,
56
+ });
57
+ }
58
+ /**
59
+ * Listed rather than ranked, and deliberately not routed through `recall`:
60
+ * ranking decays by `lastUsedAt`, only `memory_load` bumps that, and a
61
+ * preference is injected rather than loaded — so it would age out of its
62
+ * own list. Ordered so the rendered block is byte-stable between runs,
63
+ * because an unstable one would invalidate the prompt prefix for nothing.
64
+ */
65
+ preferences(sees) {
66
+ const stale = this.graph.superseded(sees);
67
+ return this.graph
68
+ .nodes(sees)
69
+ .filter((n) => n.kind === PREFERENCE_KIND && !stale.has(n.id))
70
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
71
+ }
72
+ /**
73
+ * The only path that counts as use — recency decay is meant to track what
74
+ * an agent actually read, and a search hands back context it never opened.
75
+ */
76
+ async load(ids, sees, clock) {
77
+ const at = clock.now();
78
+ const out = [];
79
+ for (const id of ids) {
80
+ const node = this.graph.get(id);
81
+ // Indistinguishable from "no such node" on purpose: a refusal that
82
+ // said "you may not see this" would confirm the node exists.
83
+ if (!node || !visible(node, sees)) {
84
+ throw new MemoryError(`no memory ${id}`, 'use an id from the legend of a memory_search result');
85
+ }
86
+ const touched = this.graph.touch(id, at) ?? node;
87
+ const file = touched.file;
88
+ out.push({
89
+ node: touched,
90
+ content: file && file.bytes <= INLINE_FILE_BYTES
91
+ ? await readFile(hostPath(this.store.dir, file), 'utf8')
92
+ : undefined,
93
+ });
94
+ }
95
+ await this.store.commit();
96
+ return out;
97
+ }
98
+ async commit(tx, opts) {
99
+ const nodes = tx.nodes ?? [];
100
+ const edges = tx.edges ?? [];
101
+ if (!nodes.length && !edges.length) {
102
+ throw new MemoryError('a commit needs at least one node or edge');
103
+ }
104
+ const plan = this.#plan(nodes, edges, opts);
105
+ const vectors = await this.#embed(plan.embed.map((e) => e.text), 'document');
106
+ // Keyed before anything is dropped: `#merge` reshapes `plan.embed`, and
107
+ // the vectors only line up with it positionally until it does.
108
+ const vector = new Map(plan.embed.map((e, i) => [e.id, vectors?.[i]]));
109
+ const merged = this.#merge(plan, vector, opts.sees);
110
+ // Files first, so a failed copy can be undone before the graph knows.
111
+ const written = [];
112
+ try {
113
+ for (const item of plan.files) {
114
+ const file = await rememberFile(this.store.dir, item.source, item.id);
115
+ written.push(file);
116
+ item.target.file = file;
117
+ }
118
+ }
119
+ catch (err) {
120
+ await Promise.all(written.map((f) => forgetFile(this.store.dir, f)));
121
+ throw err;
122
+ }
123
+ const at = opts.clock.now();
124
+ let created = 0;
125
+ let updated = 0;
126
+ for (const item of plan.nodes) {
127
+ if (item.existing) {
128
+ this.graph.update(item.id, item.patch, at);
129
+ updated++;
130
+ }
131
+ else {
132
+ this.graph.add(item.draft, at);
133
+ created++;
134
+ }
135
+ }
136
+ for (const edge of plan.edges) {
137
+ this.graph.link(edge.source, edge.target, edge.relation, at);
138
+ }
139
+ for (const e of plan.embed) {
140
+ const v = vector.get(e.id);
141
+ if (v) {
142
+ this.store.vectors?.set(e.id, v);
143
+ }
144
+ }
145
+ await this.store.commit();
146
+ return {
147
+ ids: plan.ids,
148
+ created,
149
+ updated,
150
+ edges: plan.edges.length,
151
+ files: plan.files.length,
152
+ merged,
153
+ };
154
+ }
155
+ /**
156
+ * A second run that learns the same thing should join the graph, not fork
157
+ * it. Nothing tells an agent the id of a node it is about to write, so two
158
+ * runs of one job commit two identical tasks and split every edge between
159
+ * them — which is what makes a five-seed recall return two memories.
160
+ *
161
+ * Detected here rather than in `#plan` because the vectors that answer it
162
+ * have just been paid for, so the check is a dot product over what is
163
+ * already in memory and costs no extra call. A merged node keeps its
164
+ * original id, and every edge and ref in this commit is re-pointed at it,
165
+ * so the caller is told the canonical id and can link to it next time.
166
+ *
167
+ * File-bearing nodes never merge: their text is a summary of the artifact,
168
+ * and two summaries can read alike while the bytes underneath differ.
169
+ */
170
+ #merge(plan, vector, sees) {
171
+ const minted = new Set(plan.nodes.map((n) => n.id));
172
+ const carries = new Set(plan.files.map((f) => f.id));
173
+ const stale = this.graph.superseded(sees);
174
+ const twins = new Map();
175
+ for (const item of plan.nodes) {
176
+ if (item.existing || carries.has(item.id)) {
177
+ continue;
178
+ }
179
+ const { kind, text } = item.draft;
180
+ const eligible = (id) => {
181
+ if (minted.has(id) || stale.has(id)) {
182
+ return false;
183
+ }
184
+ const node = this.graph.get(id);
185
+ return !!node && node.kind === kind && !node.file && visible(node, sees);
186
+ };
187
+ const mine = vector.get(item.id);
188
+ const twin = mine
189
+ ? this.store.vectors
190
+ ?.topK(mine, 1, eligible)
191
+ .find((h) => h.score >= DUPLICATE_SCORE)?.id
192
+ : this.graph
193
+ .nodes(sees)
194
+ .find((n) => eligible(n.id) && flatten(n.text) === flatten(text))?.id;
195
+ if (twin) {
196
+ twins.set(item.id, twin);
197
+ }
198
+ }
199
+ if (!twins.size) {
200
+ return 0;
201
+ }
202
+ const to = (id) => twins.get(id) ?? id;
203
+ plan.nodes = plan.nodes.filter((n) => !twins.has(n.id));
204
+ plan.embed = plan.embed.filter((e) => !twins.has(e.id));
205
+ for (const [ref, id] of Object.entries(plan.ids)) {
206
+ plan.ids[ref] = to(id);
207
+ }
208
+ // A self-edge is what two refs collapsing onto one node leaves behind.
209
+ plan.edges = plan.edges
210
+ .map((e) => ({ ...e, source: to(e.source), target: to(e.target) }))
211
+ .filter((e) => e.source !== e.target);
212
+ return twins.size;
213
+ }
214
+ async forget(ids, sees) {
215
+ const doomed = ids.map((id) => {
216
+ const node = this.graph.get(id);
217
+ if (!node || !visible(node, sees)) {
218
+ throw new MemoryError(`no memory ${id}`);
219
+ }
220
+ return node;
221
+ });
222
+ for (const node of doomed) {
223
+ this.graph.forget(node.id);
224
+ this.store.vectors?.remove(node.id);
225
+ if (node.file) {
226
+ await forgetFile(this.store.dir, node.file);
227
+ }
228
+ }
229
+ await this.store.commit();
230
+ return doomed;
231
+ }
232
+ /**
233
+ * Everything that can be refused, refused here — before a file is copied or
234
+ * a node is added, so a rejected commit leaves the graph exactly as it was.
235
+ */
236
+ #plan(nodes, edges, opts) {
237
+ const ids = {};
238
+ const planned = [];
239
+ const files = [];
240
+ const embed = [];
241
+ for (const [i, node] of nodes.entries()) {
242
+ const where = node.ref ?? node.id ?? `nodes[${i}]`;
243
+ if ((node.ref === undefined) === (node.id === undefined)) {
244
+ throw new MemoryError(`${where}: give either a ref (to create) or an id (to update), not both`);
245
+ }
246
+ const audience = this.#audience(node, opts.writes, where);
247
+ if (node.id) {
248
+ const existing = this.graph.get(node.id);
249
+ if (!existing) {
250
+ throw new MemoryError(`${where}: no such memory`);
251
+ }
252
+ if (node.kind !== undefined) {
253
+ this.#kind(node.kind, where);
254
+ }
255
+ ids[node.id] = node.id;
256
+ const patch = {
257
+ ...(node.kind !== undefined ? { kind: node.kind } : {}),
258
+ ...(node.text !== undefined ? { text: node.text } : {}),
259
+ ...(audience ? { audience } : {}),
260
+ ...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
261
+ ...(node.expectedRevision !== undefined
262
+ ? { expectedRevision: node.expectedRevision }
263
+ : {}),
264
+ };
265
+ planned.push({ existing: true, id: node.id, patch });
266
+ // Only a text change invalidates the vector; re-embedding on an
267
+ // audience edit would burn a call to store the same numbers.
268
+ if (node.text !== undefined && node.text !== existing.text) {
269
+ embed.push({ id: node.id, text: node.text });
270
+ }
271
+ if (node.file) {
272
+ files.push({ id: node.id, source: node.file.source, target: patch });
273
+ }
274
+ continue;
275
+ }
276
+ const ref = node.ref;
277
+ if (ref in ids) {
278
+ throw new MemoryError(`${where}: two nodes share the ref "${ref}"`);
279
+ }
280
+ if (node.text === undefined || !node.text.trim()) {
281
+ throw new MemoryError(`${where}: a new node needs text`, 'text is the only thing a search can match, so it has to describe the node');
282
+ }
283
+ const kind = this.#kind(node.kind, where);
284
+ const id = opts.clock.newId();
285
+ ids[ref] = id;
286
+ const draft = {
287
+ id,
288
+ kind,
289
+ text: node.text,
290
+ audience: audience ?? [ALL_AGENTS],
291
+ ...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
292
+ };
293
+ planned.push({ existing: false, id, draft });
294
+ embed.push({ id, text: node.text });
295
+ if (node.file) {
296
+ files.push({ id, source: node.file.source, target: draft });
297
+ }
298
+ }
299
+ const resolved = edges.map((edge, i) => {
300
+ const where = `edges[${i}]`;
301
+ const relation = edge.relation;
302
+ if (!this.#relations.has(relation)) {
303
+ throw new MemoryError(`${where}: unknown relation "${relation}"`, `use one of ${[...this.#relations].join(', ')}`);
304
+ }
305
+ return {
306
+ source: this.#endpoint(edge.from, ids, where),
307
+ target: this.#endpoint(edge.to, ids, where),
308
+ relation: relation,
309
+ };
310
+ });
311
+ return { ids, nodes: planned, edges: resolved, files, embed };
312
+ }
313
+ #endpoint(name, ids, where) {
314
+ const id = ids[name];
315
+ if (id) {
316
+ return id;
317
+ }
318
+ if (this.graph.has(name)) {
319
+ return name;
320
+ }
321
+ throw new MemoryError(`${where}: "${name}" is neither a ref in this commit nor an existing memory id`);
322
+ }
323
+ #kind(kind, where) {
324
+ if (kind === undefined) {
325
+ throw new MemoryError(`${where}: a new node needs a kind`);
326
+ }
327
+ if (!this.#kinds.has(kind)) {
328
+ throw new MemoryError(`${where}: unknown kind "${kind}"`, `use one of ${[...this.#kinds].join(', ')}`);
329
+ }
330
+ return kind;
331
+ }
332
+ /** An agent can only label a node with something it was granted. */
333
+ #audience(node, writes, where) {
334
+ if (!node.audience?.length) {
335
+ return undefined;
336
+ }
337
+ for (const label of node.audience) {
338
+ if (!writes.includes(label)) {
339
+ throw new MemoryError(`${where}: not allowed to label a memory "${label}"`, `this agent may write ${writes.join(', ') || 'nothing'}`);
340
+ }
341
+ }
342
+ return [...node.audience];
343
+ }
344
+ async #embed(texts, taskType) {
345
+ if (!this.#embedder || !texts.length) {
346
+ return undefined;
347
+ }
348
+ const res = await this.#embedder.embed({ input: texts, taskType });
349
+ this.store.adopt({ model: this.#embedder.id, dimensions: res.dimensions });
350
+ return res.vectors;
351
+ }
352
+ }
353
+ /** Whitespace and case are not what makes two memories different. */
354
+ function flatten(text) {
355
+ return text.replace(/\s+/g, ' ').trim().toLowerCase();
356
+ }
357
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ import { type MemoryNode, type ResolvedMemoryBinding } from './types.ts';
2
+ export declare const PREFERENCES_TAG = "memory-preferences";
3
+ /**
4
+ * Deliberately not a function of `sees`: those labels come from the run's
5
+ * context, and naming them here would rewrite the prompt whenever the context
6
+ * moved. It also tells an agent the shape of a slice it was not given.
7
+ */
8
+ export declare function memoryInstructions(binding: ResolvedMemoryBinding): string;
9
+ /**
10
+ * The id on each line is what makes a preference revisable rather than merely
11
+ * readable: without it the model can read the instruction but cannot name the
12
+ * thing it would supersede.
13
+ */
14
+ export declare function renderPreferences(nodes: readonly MemoryNode[]): string;
15
+ //# sourceMappingURL=instructions.d.ts.map
@@ -0,0 +1,103 @@
1
+ import { MEMORY_COMMIT_TOOL, MEMORY_FORGET_TOOL, MEMORY_LOAD_TOOL, MEMORY_SEARCH_TOOL, } from "../types.js";
2
+ import { RECOLLECTION_TAG } from "./render.js";
3
+ import { canForget, canWrite } from "./types.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Teaching the model to read and write memory
6
+ //
7
+ // The tool schemas say what the arguments are; none of them says when a thing
8
+ // is worth remembering, or that a recollection is a subgraph rather than a
9
+ // ranked list. That is policy, it is the same for every agent, and it belongs
10
+ // in the system prompt — which is also the only place it can be stated once
11
+ // per run instead of once per call.
12
+ //
13
+ // Both halves go in through `composePrompt`'s `derived` array, so they sit in
14
+ // the cacheable prefix and are recorded in the `system_prompt` node like every
15
+ // other part of what the model read.
16
+ //
17
+ // The text is gated on the binding, not on the project: an agent that cannot
18
+ // write is never told how to commit. A rule it cannot act on is a rule it can
19
+ // only be confused by, and an unusable tool name in the prompt invites a call
20
+ // that will be refused.
21
+ // ---------------------------------------------------------------------------
22
+ export const PREFERENCES_TAG = 'memory-preferences';
23
+ /**
24
+ * Deliberately not a function of `sees`: those labels come from the run's
25
+ * context, and naming them here would rewrite the prompt whenever the context
26
+ * moved. It also tells an agent the shape of a slice it was not given.
27
+ */
28
+ export function memoryInstructions(binding) {
29
+ const parts = [READING];
30
+ if (canWrite(binding)) {
31
+ parts.push(WRITING);
32
+ }
33
+ if (canForget(binding)) {
34
+ parts.push(FORGETTING);
35
+ }
36
+ return parts.join('\n\n');
37
+ }
38
+ const READING = `Memory is a graph shared with other agents, and it outlives this run.
39
+
40
+ A <${RECOLLECTION_TAG}> block is a subgraph, not a ranked list — the links are
41
+ as much of the answer as the nodes. It is an outline, and the indentation is
42
+ the graph. A line at the left margin reads \`score kind id\` and is something
43
+ the search matched. A line indented under it reads \`<arrow>relation kind id\`
44
+ and is a neighbour: \`\u2192produced\` means the line above produced this one,
45
+ \`\u2190informed\` means this one informed the line above. A neighbour the
46
+ search matched in its own right carries a score too, after the arrow. Under
47
+ each header, indented further, is that node's text — and for a \`file\`, the
48
+ path under /memory you can open or run, its size and how often it has been read.
49
+
50
+ A line beginning \`+\` is a link to an id already shown above, for an edge the
51
+ outline could not nest. Text is clipped: copy an id into \`${MEMORY_LOAD_TOOL}\`
52
+ to read a node whole. \`${MEMORY_SEARCH_TOOL}\` finds ids, it does not return
53
+ contents.
54
+
55
+ You see part of the graph, not all of it. Finding nothing means nothing you can
56
+ see, not that it never happened.`;
57
+ const WRITING = `Commit when a run produced something a later run would otherwise have to work
58
+ out again: a script that ran, an interface that behaved differently from its
59
+ documentation, a call sequence that turned out to be necessary. Commit it in
60
+ one \`${MEMORY_COMMIT_TOOL}\` call — the artifact and what explains it are one
61
+ memory, and splitting them leaves a later run holding a script with no idea why
62
+ it exists.
63
+
64
+ Do not commit the request restated, progress notes, anything you have not
65
+ verified, or anything a search already returned. A node that says what one
66
+ already in memory says is folded into it and its existing id comes back under
67
+ your ref, so the links you asked for still land — on the memory that was
68
+ already there.
69
+
70
+ Never edit a memory to correct it. Add the new one and link it to the old with
71
+ SUPERSEDES; the wrong answer is evidence too, and the link is what hides it
72
+ from later recalls without destroying the history.
73
+
74
+ Commit a preference only when the user's own words generalise — "always", "from
75
+ now on", "I prefer", "never". A single request being fulfilled is not a
76
+ standing instruction.`;
77
+ const FORGETTING = `Forget only what is wrong and has no successor. A superseded memory is already
78
+ hidden from recall and should stay for the history, so \`${MEMORY_FORGET_TOOL}\`
79
+ is for what should never have been written, not for what has been replaced.`;
80
+ /**
81
+ * The id on each line is what makes a preference revisable rather than merely
82
+ * readable: without it the model can read the instruction but cannot name the
83
+ * thing it would supersede.
84
+ */
85
+ export function renderPreferences(nodes) {
86
+ if (!nodes.length) {
87
+ return '';
88
+ }
89
+ const lines = nodes.map((n) => `- ${oneLine(n.text)} [${n.id}]`);
90
+ return [
91
+ `<${PREFERENCES_TAG}>`,
92
+ "Standing instructions from earlier runs. They apply unless this run's",
93
+ 'request contradicts them; to change one, commit the replacement and link',
94
+ 'it to the id here with SUPERSEDES.',
95
+ ...lines,
96
+ `</${PREFERENCES_TAG}>`,
97
+ ].join('\n');
98
+ }
99
+ /** Never clipped: half an instruction is a different instruction. */
100
+ function oneLine(text) {
101
+ return text.replace(/\s+/g, ' ').trim();
102
+ }
103
+ //# sourceMappingURL=instructions.js.map
@@ -0,0 +1,8 @@
1
+ import type { MemoryReport } from './report.ts';
2
+ /** Pinned once, in the run inspector, and shared so the two pages cannot drift. */
3
+ export interface PageOptions {
4
+ mermaidUrl?: string;
5
+ }
6
+ /** Report → one standalone HTML document. */
7
+ export declare function renderMemoryHtml(report: MemoryReport, opts?: PageOptions): string;
8
+ //# sourceMappingURL=page.d.ts.map