@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 +11 -1
- package/dist/project/config.d.ts +9 -9
- package/package.json +1 -1
- package/dist/memory/instructions.d.ts +0 -15
- package/dist/memory/instructions.js +0 -103
- package/dist/memory/match.d.ts +0 -18
- package/dist/memory/match.js +0 -61
- 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/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-
|
|
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
|
[](https://www.npmjs.com/package/@zenera/neo)
|
package/dist/project/config.d.ts
CHANGED
|
@@ -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,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
|
package/dist/memory/match.d.ts
DELETED
|
@@ -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
|
package/dist/memory/match.js
DELETED
|
@@ -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
|
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
|