@zenera/neo 1.1.26 → 1.1.28

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 CHANGED
@@ -1,7 +1,17 @@
1
+ <div align="center">
2
+ <img
3
+ src="https://raw.githubusercontent.com/andreyryabov/ZeneraNeo/main/docs/imgs/zenera-logo-with-media-queries.svg"
4
+ alt="Zenera Logo"
5
+ width="20"
6
+ />
7
+ <div>Powered By Zenera</div>
8
+ </div>
9
+ <br>
10
+
1
11
  <div align="center">
2
12
  <picture>
3
13
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/andreyryabov/ZeneraNeo/main/docs/imgs/banner-neo-dark.svg">
4
- <img src="https://raw.githubusercontent.com/andreyryabov/ZeneraNeo/main/docs/imgs/banner-neo-light.svg" alt="ZENERA NEO" width="720">
14
+ <img src="https://raw.githubusercontent.com/andreyryabov/ZeneraNeo/main/docs/imgs/banner-neo-with-media-queries.svg" alt="ZENERA NEO" width="720">
5
15
  </picture>
6
16
 
7
17
  [![npm](https://img.shields.io/npm/v/@zenera/neo.svg)](https://www.npmjs.com/package/@zenera/neo)
@@ -48,10 +48,10 @@ declare const modelSpec: z.ZodObject<{
48
48
  thinkingBudgetTokens: z.ZodOptional<z.ZodInt>;
49
49
  thinkingBudget: z.ZodOptional<z.ZodInt>;
50
50
  thinkingLevel: z.ZodOptional<z.ZodEnum<{
51
- minimal: "minimal";
52
51
  low: "low";
53
52
  medium: "medium";
54
53
  high: "high";
54
+ minimal: "minimal";
55
55
  }>>;
56
56
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
57
57
  routing: z.ZodOptional<z.ZodObject<{
@@ -163,15 +163,15 @@ declare const sandbox: z.ZodObject<{
163
163
  cpus: z.ZodOptional<z.ZodNumber>;
164
164
  memory: z.ZodOptional<z.ZodInt>;
165
165
  network: z.ZodOptional<z.ZodEnum<{
166
- bridge: "bridge";
167
166
  none: "none";
168
167
  host: "host";
168
+ bridge: "bridge";
169
169
  }>>;
170
170
  workdir: z.ZodOptional<z.ZodString>;
171
171
  timeout: z.ZodOptional<z.ZodInt>;
172
172
  hardening: z.ZodOptional<z.ZodEnum<{
173
- standard: "standard";
174
173
  strict: "strict";
174
+ standard: "standard";
175
175
  }>>;
176
176
  user: z.ZodOptional<z.ZodString>;
177
177
  persist: z.ZodOptional<z.ZodBoolean>;
@@ -220,10 +220,10 @@ export declare const projectSchema: z.ZodObject<{
220
220
  thinkingBudgetTokens: z.ZodOptional<z.ZodInt>;
221
221
  thinkingBudget: z.ZodOptional<z.ZodInt>;
222
222
  thinkingLevel: z.ZodOptional<z.ZodEnum<{
223
- minimal: "minimal";
224
223
  low: "low";
225
224
  medium: "medium";
226
225
  high: "high";
226
+ minimal: "minimal";
227
227
  }>>;
228
228
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
229
229
  routing: z.ZodOptional<z.ZodObject<{
@@ -293,15 +293,15 @@ export declare const projectSchema: z.ZodObject<{
293
293
  cpus: z.ZodOptional<z.ZodNumber>;
294
294
  memory: z.ZodOptional<z.ZodInt>;
295
295
  network: z.ZodOptional<z.ZodEnum<{
296
- bridge: "bridge";
297
296
  none: "none";
298
297
  host: "host";
298
+ bridge: "bridge";
299
299
  }>>;
300
300
  workdir: z.ZodOptional<z.ZodString>;
301
301
  timeout: z.ZodOptional<z.ZodInt>;
302
302
  hardening: z.ZodOptional<z.ZodEnum<{
303
- standard: "standard";
304
303
  strict: "strict";
304
+ standard: "standard";
305
305
  }>>;
306
306
  user: z.ZodOptional<z.ZodString>;
307
307
  persist: z.ZodOptional<z.ZodBoolean>;
@@ -325,8 +325,8 @@ export declare const projectSchema: z.ZodObject<{
325
325
  provider: z.ZodOptional<z.ZodString>;
326
326
  discovery: z.ZodDefault<z.ZodEnum<{
327
327
  none: "none";
328
- index: "index";
329
328
  search: "search";
329
+ index: "index";
330
330
  }>>;
331
331
  allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
332
332
  preload: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -357,15 +357,15 @@ export declare const projectSchema: z.ZodObject<{
357
357
  cpus: z.ZodOptional<z.ZodNumber>;
358
358
  memory: z.ZodOptional<z.ZodInt>;
359
359
  network: z.ZodOptional<z.ZodEnum<{
360
- bridge: "bridge";
361
360
  none: "none";
362
361
  host: "host";
362
+ bridge: "bridge";
363
363
  }>>;
364
364
  workdir: z.ZodOptional<z.ZodString>;
365
365
  timeout: z.ZodOptional<z.ZodInt>;
366
366
  hardening: z.ZodOptional<z.ZodEnum<{
367
- standard: "standard";
368
367
  strict: "strict";
368
+ standard: "standard";
369
369
  }>>;
370
370
  user: z.ZodOptional<z.ZodString>;
371
371
  persist: z.ZodOptional<z.ZodBoolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/neo",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
4
4
  "description": "A multi-agent runtime for Node.js: agents, models, tools, skills, memory and an append-only trajectory.",
5
5
  "keywords": [
6
6
  "agents",
@@ -1,15 +0,0 @@
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
@@ -1,103 +0,0 @@
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
@@ -1,18 +0,0 @@
1
- /** Long enough for any honest pattern, short enough to bound a bad one. */
2
- export declare const MAX_PATTERN = 200;
3
- export declare class PatternError extends Error {
4
- }
5
- export interface MatchOptions {
6
- /** read the pattern as a regular expression rather than as a literal */
7
- regex?: boolean;
8
- caseSensitive?: boolean;
9
- }
10
- /** The offset of the first match in `text`, or -1. */
11
- export type Matcher = (text: string) => number;
12
- /**
13
- * A finder over a string. Literal by default: someone typing `user.id` means
14
- * those seven characters, and a dot that quietly matched anything would be a
15
- * worse answer than no answer.
16
- */
17
- export declare function matcher(pattern: string, options?: MatchOptions): Matcher;
18
- //# sourceMappingURL=match.d.ts.map
@@ -1,61 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Matching, with nothing learned in between
3
- //
4
- // Everything here is exact. A substring is a substring, which is the whole
5
- // point of the surface built on it: the vector index answers "what is near
6
- // this", and near is a ranking, so it can only ever return the top of a list.
7
- // When the question is "does the word `password` appear anywhere at all", a
8
- // ranking is the wrong instrument and no amount of tuning makes it the right
9
- // one.
10
- //
11
- // This is a copy of `common/match.ts` in @zenera/rag, and has to be: the
12
- // dependency runs rag → cli → neo, so neo importing rag would close a cycle.
13
- // Two differences earn their place rather than being drift. A matcher returns
14
- // the offset it matched at, because grep reports *where*, not merely whether;
15
- // and there are no globs, because the haystack here is prose and a glob is
16
- // anchored to the whole string, so `error*` would match a line only when the
17
- // line began with the word.
18
- //
19
- // A pattern may arrive from a model, so a regex is a bounded promise: the
20
- // length is capped here and the scan that uses it keeps a deadline.
21
- // ---------------------------------------------------------------------------
22
- /** Long enough for any honest pattern, short enough to bound a bad one. */
23
- export const MAX_PATTERN = 200;
24
- export class PatternError extends Error {
25
- }
26
- /**
27
- * A finder over a string. Literal by default: someone typing `user.id` means
28
- * those seven characters, and a dot that quietly matched anything would be a
29
- * worse answer than no answer.
30
- */
31
- export function matcher(pattern, options = {}) {
32
- guard(pattern);
33
- if (!options.regex) {
34
- if (options.caseSensitive) {
35
- return (text) => text.indexOf(pattern);
36
- }
37
- const needle = pattern.toLowerCase();
38
- return (text) => text.toLowerCase().indexOf(needle);
39
- }
40
- const expression = compile(pattern, options.caseSensitive ? '' : 'i');
41
- // `lastIndex` is not carried between calls: the flags never include `g`.
42
- return (text) => text.search(expression);
43
- }
44
- // ---------------------------------------------------------------------------
45
- function guard(pattern) {
46
- if (pattern.length === 0) {
47
- throw new PatternError('the pattern is empty');
48
- }
49
- if (pattern.length > MAX_PATTERN) {
50
- throw new PatternError(`the pattern is longer than ${MAX_PATTERN} characters`);
51
- }
52
- }
53
- function compile(source, flags) {
54
- try {
55
- return new RegExp(source, flags);
56
- }
57
- catch (err) {
58
- throw new PatternError(`invalid pattern: ${err.message}`);
59
- }
60
- }
61
- //# sourceMappingURL=match.js.map
@@ -1,15 +0,0 @@
1
- import type { MemoryStore } from '../memory.ts';
2
- import { type FileMemoryStoreOptions } from './file.ts';
3
- export interface MemoryMemoryStoreSpec {
4
- kind: 'memory';
5
- id?: string;
6
- }
7
- export interface FileMemoryStoreSpec extends FileMemoryStoreOptions {
8
- kind: 'file';
9
- }
10
- /** Grows a member per backend (`pgvector`, `redis`, …). */
11
- export type MemoryStoreSpec = MemoryMemoryStoreSpec | FileMemoryStoreSpec;
12
- /** Shorthand: `mem`, `memory`, `file:./.data/memory`. */
13
- export type MemoryStoreRef = MemoryStoreSpec | string;
14
- export declare function createMemoryStore(ref: MemoryStoreRef): MemoryStore;
15
- //# sourceMappingURL=factory.d.ts.map
@@ -1,29 +0,0 @@
1
- import { FileMemoryStore } from "./file.js";
2
- import { InMemoryMemoryStore } from "./in-memory.js";
3
- export function createMemoryStore(ref) {
4
- const spec = typeof ref === 'string' ? parseRef(ref) : ref;
5
- switch (spec.kind) {
6
- case 'memory':
7
- return new InMemoryMemoryStore(spec.id);
8
- case 'file':
9
- return new FileMemoryStore(spec);
10
- default:
11
- throw new TypeError(`unknown memory store kind: ${spec.kind}`);
12
- }
13
- }
14
- function parseRef(ref) {
15
- if (ref === 'mem' || ref === 'memory') {
16
- return { kind: 'memory' };
17
- }
18
- const colon = ref.indexOf(':');
19
- const scheme = colon < 0 ? ref : ref.slice(0, colon);
20
- const rest = colon < 0 ? '' : ref.slice(colon + 1);
21
- if (scheme === 'file') {
22
- if (!rest) {
23
- throw new TypeError(`missing directory in "${ref}" (expected file:<dir>)`);
24
- }
25
- return { kind: 'file', dir: rest };
26
- }
27
- throw new TypeError(`unknown memory store ref: "${ref}"`);
28
- }
29
- //# sourceMappingURL=factory.js.map
@@ -1,37 +0,0 @@
1
- import { type MemoryDraft, type MemoryHit, type MemoryPatch, type MemoryQuery, type MemoryRecord, type MemoryStore } from '../memory.ts';
2
- export interface FileMemoryStoreOptions {
3
- /** root directory; created on demand */
4
- dir: string;
5
- /** logical store id agents bind to; defaults to `file` */
6
- id?: string;
7
- }
8
- /**
9
- * One JSON file per record, under one directory per scope:
10
- *
11
- * ```
12
- * <dir>/<scope>/scope.json the unescaped scope name, for humans
13
- * <dir>/<scope>/records/<id>.json
14
- * <dir>/<scope>/ops/<opId>.json idempotency ledger
15
- * ```
16
- *
17
- * A file per record (rather than one appended log) keeps writes atomic and
18
- * deletes cheap without a compaction step. Search reads the whole scope and
19
- * ranks in process, which is exactly the trade this backend makes: no server,
20
- * no index, fine up to a few thousand records per scope.
21
- *
22
- * There is no cross-process lock. Concurrent writers to the *same* record can
23
- * lose an update; `expectedRevision` narrows that window, and a deployment
24
- * that needs real isolation should use a database-backed store.
25
- */
26
- export declare class FileMemoryStore implements MemoryStore {
27
- #private;
28
- readonly id: string;
29
- constructor(opts: FileMemoryStoreOptions | string);
30
- get dir(): string;
31
- search(scope: string, q: MemoryQuery): Promise<MemoryHit[]>;
32
- get(scope: string, id: string): Promise<MemoryRecord | undefined>;
33
- write(scope: string, rec: MemoryDraft, opId: string): Promise<MemoryRecord>;
34
- update(scope: string, id: string, patch: MemoryPatch, opId: string): Promise<MemoryRecord>;
35
- delete(scope: string, id: string, opId: string): Promise<void>;
36
- }
37
- //# sourceMappingURL=file.d.ts.map
@@ -1,174 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
3
- import { join, resolve } from 'node:path';
4
- import { rankMemories, } from "../memory.js";
5
- // ---------------------------------------------------------------------------
6
- // Filesystem-backed memory store
7
- // ---------------------------------------------------------------------------
8
- /**
9
- * Record ids and op ids reach this store from model-authored tool arguments
10
- * (`memory_update`, `memory_delete`), so they are validated before becoming
11
- * path segments. Dots are excluded outright, which makes `.` and `..`
12
- * unrepresentable.
13
- */
14
- const SAFE_ID = /^[A-Za-z0-9_-]+$/;
15
- /**
16
- * One JSON file per record, under one directory per scope:
17
- *
18
- * ```
19
- * <dir>/<scope>/scope.json the unescaped scope name, for humans
20
- * <dir>/<scope>/records/<id>.json
21
- * <dir>/<scope>/ops/<opId>.json idempotency ledger
22
- * ```
23
- *
24
- * A file per record (rather than one appended log) keeps writes atomic and
25
- * deletes cheap without a compaction step. Search reads the whole scope and
26
- * ranks in process, which is exactly the trade this backend makes: no server,
27
- * no index, fine up to a few thousand records per scope.
28
- *
29
- * There is no cross-process lock. Concurrent writers to the *same* record can
30
- * lose an update; `expectedRevision` narrows that window, and a deployment
31
- * that needs real isolation should use a database-backed store.
32
- */
33
- export class FileMemoryStore {
34
- id;
35
- #dir;
36
- constructor(opts) {
37
- const o = typeof opts === 'string' ? { dir: opts } : opts;
38
- this.#dir = resolve(o.dir);
39
- this.id = o.id ?? 'file';
40
- }
41
- get dir() {
42
- return this.#dir;
43
- }
44
- async search(scope, q) {
45
- return rankMemories(await this.#all(scope), q);
46
- }
47
- async get(scope, id) {
48
- return this.#readJson(this.#recordPath(scope, id));
49
- }
50
- async write(scope, rec, opId) {
51
- const known = await this.#readOp(scope, opId);
52
- if (known) {
53
- const existing = await this.get(scope, known);
54
- if (existing) {
55
- return existing;
56
- }
57
- }
58
- const now = new Date().toISOString();
59
- const record = {
60
- id: randomUUID(),
61
- scope,
62
- kind: rec.kind ?? 'fact',
63
- text: rec.text,
64
- metadata: rec.metadata,
65
- createdAt: now,
66
- updatedAt: now,
67
- revision: 1,
68
- };
69
- await this.#writeRecord(record);
70
- await this.#writeOp(scope, opId, record.id);
71
- return record;
72
- }
73
- async update(scope, id, patch, opId) {
74
- const cur = await this.get(scope, id);
75
- if (!cur) {
76
- throw new Error(`memory record not found: ${scope}/${id}`);
77
- }
78
- if ((await this.#readOp(scope, opId)) === id) {
79
- return cur;
80
- }
81
- if (patch.expectedRevision !== undefined && patch.expectedRevision !== cur.revision) {
82
- throw new Error(`memory conflict on ${scope}/${id}: expected revision ` +
83
- `${patch.expectedRevision}, found ${cur.revision}`);
84
- }
85
- const next = {
86
- ...cur,
87
- kind: patch.kind ?? cur.kind,
88
- text: patch.text ?? cur.text,
89
- metadata: patch.metadata ?? cur.metadata,
90
- updatedAt: new Date().toISOString(),
91
- revision: cur.revision + 1,
92
- };
93
- await this.#writeRecord(next);
94
- await this.#writeOp(scope, opId, id);
95
- return next;
96
- }
97
- async delete(scope, id, opId) {
98
- await rm(this.#recordPath(scope, id), { force: true });
99
- await this.#writeOp(scope, opId, id);
100
- }
101
- // --- layout -----------------------------------------------------------
102
- /**
103
- * Percent-encoding is reversible and, with dots escaped too, cannot
104
- * produce a traversal segment out of a scope like `../etc`.
105
- */
106
- #scopeDir(scope) {
107
- return join(this.#dir, encodeURIComponent(scope).replace(/\./g, '%2E'));
108
- }
109
- #recordPath(scope, id) {
110
- return join(this.#scopeDir(scope), 'records', `${safeId(id, 'record id')}.json`);
111
- }
112
- #opPath(scope, opId) {
113
- return join(this.#scopeDir(scope), 'ops', `${safeId(opId, 'op id')}.json`);
114
- }
115
- // --- io ---------------------------------------------------------------
116
- async #all(scope) {
117
- const dir = join(this.#scopeDir(scope), 'records');
118
- let names;
119
- try {
120
- names = await readdir(dir);
121
- }
122
- catch (err) {
123
- if (err.code === 'ENOENT') {
124
- return [];
125
- }
126
- throw err;
127
- }
128
- const records = await Promise.all(names
129
- .filter((n) => n.endsWith('.json'))
130
- .map((n) => this.#readJson(join(dir, n))));
131
- return records.filter((r) => r !== undefined);
132
- }
133
- async #readOp(scope, opId) {
134
- const op = await this.#readJson(this.#opPath(scope, opId));
135
- return op?.recordId;
136
- }
137
- async #writeOp(scope, opId, recordId) {
138
- await writeJson(this.#opPath(scope, opId), { recordId });
139
- }
140
- async #writeRecord(record) {
141
- await writeJson(join(this.#scopeDir(record.scope), 'scope.json'), { scope: record.scope });
142
- await writeJson(this.#recordPath(record.scope, record.id), record);
143
- }
144
- async #readJson(path) {
145
- try {
146
- return JSON.parse(await readFile(path, 'utf8'));
147
- }
148
- catch (err) {
149
- if (err.code === 'ENOENT') {
150
- return undefined;
151
- }
152
- throw err;
153
- }
154
- }
155
- }
156
- function safeId(value, what) {
157
- if (!SAFE_ID.test(value)) {
158
- throw new Error(`invalid ${what}: ${JSON.stringify(value)}`);
159
- }
160
- return value;
161
- }
162
- async function writeJson(path, value) {
163
- await mkdir(join(path, '..'), { recursive: true });
164
- const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
165
- await writeFile(tmp, JSON.stringify(value), 'utf8');
166
- try {
167
- await rename(tmp, path);
168
- }
169
- catch (err) {
170
- await rm(tmp, { force: true });
171
- throw err;
172
- }
173
- }
174
- //# sourceMappingURL=file.js.map
@@ -1,17 +0,0 @@
1
- import { type MemoryDraft, type MemoryHit, type MemoryPatch, type MemoryQuery, type MemoryRecord, type MemoryStore } from '../memory.ts';
2
- /**
3
- * Reference implementation: scoped maps plus token-overlap scoring. Good enough
4
- * for tests and local runs; production deployments swap in a vector store
5
- * without touching the kernel.
6
- */
7
- export declare class InMemoryMemoryStore implements MemoryStore {
8
- #private;
9
- readonly id: string;
10
- constructor(id?: string);
11
- search(scope: string, q: MemoryQuery): Promise<MemoryHit[]>;
12
- get(scope: string, id: string): Promise<MemoryRecord | undefined>;
13
- write(scope: string, rec: MemoryDraft, opId: string): Promise<MemoryRecord>;
14
- update(scope: string, id: string, patch: MemoryPatch, opId: string): Promise<MemoryRecord>;
15
- delete(scope: string, id: string, opId: string): Promise<void>;
16
- }
17
- //# sourceMappingURL=in-memory.d.ts.map
@@ -1,80 +0,0 @@
1
- import { rankMemories, } from "../memory.js";
2
- /**
3
- * Reference implementation: scoped maps plus token-overlap scoring. Good enough
4
- * for tests and local runs; production deployments swap in a vector store
5
- * without touching the kernel.
6
- */
7
- export class InMemoryMemoryStore {
8
- id;
9
- #scopes = new Map();
10
- /** opId → record id, so a replayed op returns the original result */
11
- #ops = new Map();
12
- #seq = 0;
13
- constructor(id = 'mem') {
14
- this.id = id;
15
- }
16
- async search(scope, q) {
17
- return rankMemories(this.#space(scope).values(), q);
18
- }
19
- async get(scope, id) {
20
- return this.#space(scope).get(id);
21
- }
22
- async write(scope, rec, opId) {
23
- const known = this.#ops.get(opId);
24
- if (known) {
25
- const existing = this.#space(scope).get(known);
26
- if (existing) {
27
- return existing;
28
- }
29
- }
30
- const now = new Date().toISOString();
31
- const record = {
32
- id: `m${++this.#seq}`,
33
- scope,
34
- kind: rec.kind ?? 'fact',
35
- text: rec.text,
36
- metadata: rec.metadata,
37
- createdAt: now,
38
- updatedAt: now,
39
- revision: 1,
40
- };
41
- this.#space(scope).set(record.id, record);
42
- this.#ops.set(opId, record.id);
43
- return record;
44
- }
45
- async update(scope, id, patch, opId) {
46
- const space = this.#space(scope);
47
- const cur = space.get(id);
48
- if (!cur) {
49
- throw new Error(`memory record not found: ${scope}/${id}`);
50
- }
51
- if (this.#ops.get(opId) === id) {
52
- return cur;
53
- }
54
- if (patch.expectedRevision !== undefined && patch.expectedRevision !== cur.revision) {
55
- throw new Error(`memory conflict on ${scope}/${id}: expected revision ` +
56
- `${patch.expectedRevision}, found ${cur.revision}`);
57
- }
58
- const next = {
59
- ...cur,
60
- kind: patch.kind ?? cur.kind,
61
- text: patch.text ?? cur.text,
62
- metadata: patch.metadata ?? cur.metadata,
63
- updatedAt: new Date().toISOString(),
64
- revision: cur.revision + 1,
65
- };
66
- space.set(id, next);
67
- this.#ops.set(opId, id);
68
- return next;
69
- }
70
- async delete(scope, id, opId) {
71
- this.#space(scope).delete(id);
72
- this.#ops.set(opId, id);
73
- }
74
- #space(scope) {
75
- const s = this.#scopes.get(scope) ?? new Map();
76
- this.#scopes.set(scope, s);
77
- return s;
78
- }
79
- }
80
- //# sourceMappingURL=in-memory.js.map
@@ -1,4 +0,0 @@
1
- export * from './factory.ts';
2
- export * from './file.ts';
3
- export * from './in-memory.ts';
4
- //# sourceMappingURL=index.d.ts.map
@@ -1,5 +0,0 @@
1
- // Backends for the `MemoryStore` contract in `../memory.ts`.
2
- export * from "./factory.js";
3
- export * from "./file.js";
4
- export * from "./in-memory.js";
5
- //# sourceMappingURL=index.js.map
package/dist/memory.d.ts DELETED
@@ -1,115 +0,0 @@
1
- import { type AnyTool } from './types.ts';
2
- export interface MemoryRecord {
3
- id: string;
4
- /** memory space this record belongs to */
5
- scope: string;
6
- /** app-defined: 'fact' | 'preference' | 'episode' | … */
7
- kind: string;
8
- text: string;
9
- metadata?: Record<string, unknown>;
10
- createdAt: string;
11
- updatedAt: string;
12
- /** optimistic-concurrency token */
13
- revision: number;
14
- }
15
- export interface MemoryDraft {
16
- kind?: string;
17
- text: string;
18
- metadata?: Record<string, unknown>;
19
- }
20
- export interface MemoryPatch {
21
- kind?: string;
22
- text?: string;
23
- metadata?: Record<string, unknown>;
24
- /** when given, the update fails unless the stored revision matches */
25
- expectedRevision?: number;
26
- }
27
- export interface MemoryQuery {
28
- /** semantic query; omitted ⇒ pure filter listing */
29
- text?: string;
30
- filter?: Record<string, unknown>;
31
- kind?: string;
32
- limit?: number;
33
- minScore?: number;
34
- }
35
- export interface MemoryHit {
36
- record: MemoryRecord;
37
- /** 0..1, backend-normalized */
38
- score: number;
39
- }
40
- /**
41
- * Embedding generation is the store's business, not the kernel's: a pgvector
42
- * implementation embeds inside `write`/`search`, the in-memory one below falls
43
- * back to token overlap. The kernel never sees vectors.
44
- */
45
- export interface MemoryStore {
46
- readonly id: string;
47
- search(scope: string, q: MemoryQuery): Promise<MemoryHit[]>;
48
- get(scope: string, id: string): Promise<MemoryRecord | undefined>;
49
- /** `opId` makes writes idempotent under retry/replay */
50
- write(scope: string, rec: MemoryDraft, opId: string): Promise<MemoryRecord>;
51
- update(scope: string, id: string, patch: MemoryPatch, opId: string): Promise<MemoryRecord>;
52
- delete(scope: string, id: string, opId: string): Promise<void>;
53
- }
54
- export interface MemoryBinding<TCtx = unknown> {
55
- /** MemoryStore id */
56
- store: string;
57
- /** namespace; default `agent:${agent.name}`. A function is resolved per run. */
58
- scope?: string | ((ctx: TCtx) => string);
59
- access: 'read' | 'read-write';
60
- /** inject top-k matches before an LLM call */
61
- autoRecall?: {
62
- query: 'last_user_input' | 'none';
63
- limit: number;
64
- };
65
- }
66
- /** A binding with its scope already resolved against the run context. */
67
- export interface ResolvedBinding {
68
- store: string;
69
- scope: string;
70
- access: 'read' | 'read-write';
71
- autoRecall?: {
72
- query: 'last_user_input' | 'none';
73
- limit: number;
74
- };
75
- }
76
- export interface MemoryRecallSpec {
77
- kind: 'recall';
78
- store: string;
79
- scope: string;
80
- query: MemoryQuery;
81
- hits: {
82
- id: string;
83
- score: number;
84
- revision: number;
85
- }[];
86
- /** the rendered block the model actually saw */
87
- content: string;
88
- }
89
- export interface MemoryOpSpec {
90
- kind: 'op';
91
- op: 'write' | 'update' | 'delete';
92
- store: string;
93
- scope: string;
94
- /** sha256(runId, callId) — deterministic, so replay re-issues the same write */
95
- opId: string;
96
- recordId: string;
97
- revision: number;
98
- before?: string;
99
- after?: string;
100
- }
101
- export declare function memoryOpId(runId: string, callId: string): string;
102
- export declare function renderMemories(hits: MemoryHit[]): string;
103
- /**
104
- * Filtering plus token-overlap scoring, shared by the backends that have no
105
- * embeddings of their own. A vector store ignores this and ranks in the
106
- * database instead — the kernel only ever sees `MemoryHit[]`.
107
- */
108
- export declare function rankMemories(records: Iterable<MemoryRecord>, q: MemoryQuery): MemoryHit[];
109
- /**
110
- * Memory is reachable both explicitly (these tools) and implicitly (auto-recall
111
- * in the runner). Both land in the trajectory; only the explicit path is
112
- * visible to the model as a tool.
113
- */
114
- export declare function memoryTools<TCtx>(bindings: ResolvedBinding[]): AnyTool<TCtx>[];
115
- //# sourceMappingURL=memory.d.ts.map
package/dist/memory.js DELETED
@@ -1,213 +0,0 @@
1
- import { hash } from "./payload.js";
2
- import { MEMORY_DELETE_TOOL, MEMORY_SEARCH_TOOL, MEMORY_UPDATE_TOOL, MEMORY_WRITE_TOOL, tool, withEffects, } from "./types.js";
3
- export function memoryOpId(runId, callId) {
4
- return hash(`${runId}\u0000${callId}`);
5
- }
6
- export function renderMemories(hits) {
7
- if (!hits.length) {
8
- return '';
9
- }
10
- const lines = hits.map((h) => `- [${h.record.id}] (${h.record.kind}, score ${h.score.toFixed(2)}) ${h.record.text}`);
11
- return `Relevant memories:\n${lines.join('\n')}`;
12
- }
13
- // ---------------------------------------------------------------------------
14
- // Shared ranking
15
- // ---------------------------------------------------------------------------
16
- /**
17
- * Filtering plus token-overlap scoring, shared by the backends that have no
18
- * embeddings of their own. A vector store ignores this and ranks in the
19
- * database instead — the kernel only ever sees `MemoryHit[]`.
20
- */
21
- export function rankMemories(records, q) {
22
- const limit = q.limit ?? 8;
23
- const terms = tokenize(q.text ?? '');
24
- const hits = [];
25
- for (const rec of records) {
26
- if (q.kind && rec.kind !== q.kind) {
27
- continue;
28
- }
29
- if (q.filter && !matches(rec.metadata, q.filter)) {
30
- continue;
31
- }
32
- const score = terms.length ? overlap(terms, tokenize(rec.text)) : 1;
33
- if (score <= 0 || (q.minScore !== undefined && score < q.minScore)) {
34
- continue;
35
- }
36
- hits.push({ record: rec, score });
37
- }
38
- return hits.sort((a, b) => b.score - a.score).slice(0, limit);
39
- }
40
- function tokenize(v) {
41
- return v
42
- .toLowerCase()
43
- .split(/[^a-z0-9]+/)
44
- .filter((t) => t.length > 2);
45
- }
46
- function overlap(a, b) {
47
- const set = new Set(b);
48
- const shared = a.filter((t) => set.has(t)).length;
49
- return a.length ? shared / a.length : 0;
50
- }
51
- function matches(meta, filter) {
52
- return Object.entries(filter).every(([k, v]) => meta?.[k] === v);
53
- }
54
- // ---------------------------------------------------------------------------
55
- // Built-in memory tools
56
- // ---------------------------------------------------------------------------
57
- function spaceParam(bindings) {
58
- return bindings.length > 1
59
- ? { type: 'string', enum: bindings.map((b) => b.scope), description: 'memory space' }
60
- : undefined;
61
- }
62
- function pick(bindings, space) {
63
- const b = space ? bindings.find((x) => x.scope === space) : bindings[0];
64
- if (!b) {
65
- throw new Error(`unknown memory space: ${space ?? '(default)'}`);
66
- }
67
- return b;
68
- }
69
- /**
70
- * Memory is reachable both explicitly (these tools) and implicitly (auto-recall
71
- * in the runner). Both land in the trajectory; only the explicit path is
72
- * visible to the model as a tool.
73
- */
74
- export function memoryTools(bindings) {
75
- if (!bindings.length) {
76
- return [];
77
- }
78
- const writable = bindings.filter((b) => b.access === 'read-write');
79
- const space = spaceParam(bindings);
80
- const withSpace = (props) => (space ? { ...props, space } : props);
81
- const tools = [
82
- tool({
83
- name: MEMORY_SEARCH_TOOL,
84
- description: 'Search long-lived memories.',
85
- parameters: {
86
- type: 'object',
87
- properties: withSpace({
88
- query: { type: 'string' },
89
- kind: { type: 'string' },
90
- limit: { type: 'integer' },
91
- }),
92
- required: ['query'],
93
- additionalProperties: false,
94
- },
95
- execute: async (args, tc) => {
96
- const b = pick(bindings, args.space);
97
- const store = tc.services.memoryStore(b.store);
98
- const hits = await store.search(b.scope, {
99
- text: args.query,
100
- kind: args.kind,
101
- limit: args.limit,
102
- });
103
- return hits.map((h) => ({
104
- id: h.record.id,
105
- kind: h.record.kind,
106
- text: h.record.text,
107
- revision: h.record.revision,
108
- score: Number(h.score.toFixed(3)),
109
- }));
110
- },
111
- }),
112
- ];
113
- if (!writable.length) {
114
- return tools;
115
- }
116
- const wSpace = spaceParam(writable);
117
- const withWSpace = (props) => wSpace ? { ...props, space: wSpace } : props;
118
- tools.push(tool({
119
- name: MEMORY_WRITE_TOOL,
120
- description: 'Store a new long-lived memory.',
121
- parameters: {
122
- type: 'object',
123
- properties: withWSpace({ text: { type: 'string' }, kind: { type: 'string' } }),
124
- required: ['text'],
125
- additionalProperties: false,
126
- },
127
- execute: async (args, tc) => {
128
- const b = pick(writable, args.space);
129
- const opId = memoryOpId(tc.state.runId, tc.callId);
130
- const rec = await tc.services
131
- .memoryStore(b.store)
132
- .write(b.scope, { text: args.text, kind: args.kind }, opId);
133
- return withEffects(`stored memory ${rec.id}`, {
134
- kind: 'memory_op',
135
- spec: {
136
- kind: 'op',
137
- op: 'write',
138
- store: b.store,
139
- scope: b.scope,
140
- opId,
141
- recordId: rec.id,
142
- revision: rec.revision,
143
- after: rec.text,
144
- },
145
- });
146
- },
147
- }), tool({
148
- name: MEMORY_UPDATE_TOOL,
149
- description: 'Update an existing memory by id.',
150
- parameters: {
151
- type: 'object',
152
- properties: withWSpace({
153
- id: { type: 'string' },
154
- text: { type: 'string' },
155
- kind: { type: 'string' },
156
- }),
157
- required: ['id'],
158
- additionalProperties: false,
159
- },
160
- execute: async (args, tc) => {
161
- const b = pick(writable, args.space);
162
- const store = tc.services.memoryStore(b.store);
163
- const before = await store.get(b.scope, args.id);
164
- const opId = memoryOpId(tc.state.runId, tc.callId);
165
- const rec = await store.update(b.scope, args.id, { text: args.text, kind: args.kind }, opId);
166
- return withEffects(`updated memory ${rec.id}`, {
167
- kind: 'memory_op',
168
- spec: {
169
- kind: 'op',
170
- op: 'update',
171
- store: b.store,
172
- scope: b.scope,
173
- opId,
174
- recordId: rec.id,
175
- revision: rec.revision,
176
- before: before?.text,
177
- after: rec.text,
178
- },
179
- });
180
- },
181
- }), tool({
182
- name: MEMORY_DELETE_TOOL,
183
- description: 'Delete a memory by id.',
184
- parameters: {
185
- type: 'object',
186
- properties: withWSpace({ id: { type: 'string' } }),
187
- required: ['id'],
188
- additionalProperties: false,
189
- },
190
- execute: async (args, tc) => {
191
- const b = pick(writable, args.space);
192
- const store = tc.services.memoryStore(b.store);
193
- const before = await store.get(b.scope, args.id);
194
- const opId = memoryOpId(tc.state.runId, tc.callId);
195
- await store.delete(b.scope, args.id, opId);
196
- return withEffects(`deleted memory ${args.id}`, {
197
- kind: 'memory_op',
198
- spec: {
199
- kind: 'op',
200
- op: 'delete',
201
- store: b.store,
202
- scope: b.scope,
203
- opId,
204
- recordId: args.id,
205
- revision: before?.revision ?? 0,
206
- before: before?.text,
207
- },
208
- });
209
- },
210
- }));
211
- return tools;
212
- }
213
- //# sourceMappingURL=memory.js.map