getmnemo-anthropic 0.1.0
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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-tool.d.ts +80 -0
- package/dist/memory-tool.d.ts.map +1 -0
- package/dist/memory-tool.js +173 -0
- package/dist/memory-tool.js.map +1 -0
- package/package.json +54 -0
- package/src/index.ts +8 -0
- package/src/memory-tool.test.ts +144 -0
- package/src/memory-tool.ts +279 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mnemo, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @ledgermem/anthropic
|
|
2
|
+
|
|
3
|
+
LedgerMem helper for the Anthropic SDK. Wraps `messages.create` with a single
|
|
4
|
+
`memory` tool (search + add), prompt caching on the system block, and
|
|
5
|
+
optional extended thinking — the recommended setup for any long-lived Claude
|
|
6
|
+
assistant.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @ledgermem/anthropic @ledgermem/memory @anthropic-ai/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart (30 seconds)
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
18
|
+
import { LedgerMem } from "@ledgermem/memory";
|
|
19
|
+
import { withMemoryTool } from "@ledgermem/anthropic";
|
|
20
|
+
|
|
21
|
+
const claude = new Anthropic();
|
|
22
|
+
const ledgermem = new LedgerMem({
|
|
23
|
+
apiKey: process.env.LEDGERMEM_API_KEY!,
|
|
24
|
+
workspaceId: process.env.LEDGERMEM_WORKSPACE_ID!,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const agent = withMemoryTool({
|
|
28
|
+
client: claude,
|
|
29
|
+
ledgermem,
|
|
30
|
+
model: "claude-sonnet-4-7",
|
|
31
|
+
thinkingBudgetTokens: 4000,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = await agent.run({
|
|
35
|
+
system: "You are a personal assistant. Use memory before answering.",
|
|
36
|
+
messages: [{ role: "user", content: "What did I tell you about my schedule?" }],
|
|
37
|
+
metadata: { userId: "user-42" },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(result.text);
|
|
41
|
+
console.log("memory tool calls:", result.memoryToolCalls);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What the wrapper does
|
|
45
|
+
|
|
46
|
+
- Exposes one tool `memory` with `action: "search" | "add"` — the model
|
|
47
|
+
decides when to recall and when to remember.
|
|
48
|
+
- Runs the tool-use loop for you (default cap: 6 iterations).
|
|
49
|
+
- Adds `cache_control: { type: "ephemeral" }` to the system prompt so
|
|
50
|
+
long stable instructions hit the prompt cache.
|
|
51
|
+
- Optionally enables extended thinking with a configurable token budget.
|
|
52
|
+
- Returns the final transcript so you can persist it elsewhere.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,GAKjB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Mnemo } from "getmnemo";
|
|
2
|
+
/** Structural subset of `Anthropic` we depend on — keeps the peer dep loose. */
|
|
3
|
+
interface AnthropicLike {
|
|
4
|
+
messages: {
|
|
5
|
+
create: (params: Record<string, unknown>) => Promise<MessagesCreateResult>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
interface MessagesCreateResult {
|
|
9
|
+
id: string;
|
|
10
|
+
content: Array<MessageBlock>;
|
|
11
|
+
stop_reason: string;
|
|
12
|
+
usage?: Record<string, number>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
type MessageBlock = {
|
|
16
|
+
type: "text";
|
|
17
|
+
text: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: "thinking";
|
|
20
|
+
thinking: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "tool_use";
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
input: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
interface ChatMessage {
|
|
28
|
+
role: "user" | "assistant";
|
|
29
|
+
content: string | Array<Record<string, unknown>>;
|
|
30
|
+
}
|
|
31
|
+
export interface WithMemoryToolOptions {
|
|
32
|
+
client: AnthropicLike;
|
|
33
|
+
getmnemo: Mnemo;
|
|
34
|
+
/** Defaults to claude-sonnet-4-7. Override per call too. */
|
|
35
|
+
model?: string;
|
|
36
|
+
/** Defaults to 1024. */
|
|
37
|
+
maxTokens?: number;
|
|
38
|
+
/** Default top-k for memory search. */
|
|
39
|
+
searchLimit?: number;
|
|
40
|
+
/** Wrap the system prompt with `cache_control: { type: "ephemeral" }`. */
|
|
41
|
+
cacheSystem?: boolean;
|
|
42
|
+
/** Enable extended thinking with this token budget. */
|
|
43
|
+
thinkingBudgetTokens?: number;
|
|
44
|
+
/** Extra static metadata merged into every persisted memory. */
|
|
45
|
+
metadata?: Record<string, unknown>;
|
|
46
|
+
/** Hard cap on tool-use loop iterations. */
|
|
47
|
+
maxIterations?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface WithMemoryRunInput {
|
|
50
|
+
system?: string;
|
|
51
|
+
messages: ChatMessage[];
|
|
52
|
+
/** Per-call overrides. */
|
|
53
|
+
model?: string;
|
|
54
|
+
maxTokens?: number;
|
|
55
|
+
metadata?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
export interface WithMemoryRunResult {
|
|
58
|
+
/** Final assistant text concatenated across blocks. */
|
|
59
|
+
text: string;
|
|
60
|
+
/** All raw API responses produced during the tool-use loop. */
|
|
61
|
+
responses: MessagesCreateResult[];
|
|
62
|
+
/** Number of memory tool invocations. */
|
|
63
|
+
memoryToolCalls: number;
|
|
64
|
+
/** Final messages array after tool loop (useful for storing transcript). */
|
|
65
|
+
messages: ChatMessage[];
|
|
66
|
+
}
|
|
67
|
+
export interface WithMemoryToolWrapper {
|
|
68
|
+
run(input: WithMemoryRunInput): Promise<WithMemoryRunResult>;
|
|
69
|
+
}
|
|
70
|
+
export declare const MEMORY_TOOL_NAME = "memory";
|
|
71
|
+
/**
|
|
72
|
+
* Wrap an `Anthropic` client so a single `memory` tool (with `search`/`add`
|
|
73
|
+
* actions) is exposed to the model and the tool-use loop is handled for you.
|
|
74
|
+
*
|
|
75
|
+
* Also opts into prompt caching of the system prompt and (optionally) extended
|
|
76
|
+
* thinking — both are recommended for any long-lived assistant.
|
|
77
|
+
*/
|
|
78
|
+
export declare function withMemoryTool(options: WithMemoryToolOptions): WithMemoryToolWrapper;
|
|
79
|
+
export {};
|
|
80
|
+
//# sourceMappingURL=memory-tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-tool.d.ts","sourceRoot":"","sources":["../src/memory-tool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEtC,gFAAgF;AAChF,UAAU,aAAa;IACrB,QAAQ,EAAE;QACR,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;KAC5E,CAAC;CACH;AAED,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,KAAK,YAAY,GACb;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtC;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAEN,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC;IAChB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,4CAA4C;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,SAAS,EAAE,oBAAoB,EAAE,CAAC;IAClC,yCAAyC;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CAC9D;AAED,eAAO,MAAM,gBAAgB,WAAW,CAAC;AAEzC;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,qBAAqB,GAC7B,qBAAqB,CAgIvB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export const MEMORY_TOOL_NAME = "memory";
|
|
2
|
+
/**
|
|
3
|
+
* Wrap an `Anthropic` client so a single `memory` tool (with `search`/`add`
|
|
4
|
+
* actions) is exposed to the model and the tool-use loop is handled for you.
|
|
5
|
+
*
|
|
6
|
+
* Also opts into prompt caching of the system prompt and (optionally) extended
|
|
7
|
+
* thinking — both are recommended for any long-lived assistant.
|
|
8
|
+
*/
|
|
9
|
+
export function withMemoryTool(options) {
|
|
10
|
+
const defaults = {
|
|
11
|
+
model: options.model ?? "claude-sonnet-4-7",
|
|
12
|
+
maxTokens: options.maxTokens ?? 1024,
|
|
13
|
+
searchLimit: options.searchLimit ?? 5,
|
|
14
|
+
cacheSystem: options.cacheSystem ?? true,
|
|
15
|
+
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
|
16
|
+
metadata: options.metadata ?? {},
|
|
17
|
+
maxIterations: options.maxIterations ?? 6,
|
|
18
|
+
};
|
|
19
|
+
const tool = {
|
|
20
|
+
name: MEMORY_TOOL_NAME,
|
|
21
|
+
description: "Long-term memory store. Use action='search' to recall facts about the user before answering, and action='add' to save a new fact worth remembering across conversations.",
|
|
22
|
+
input_schema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
// additionalProperties:false lets Anthropic enforce strict JSON schema
|
|
25
|
+
// for tool inputs — without it the model can pass arbitrary keys
|
|
26
|
+
// (including keys that look like trusted metadata) and we silently
|
|
27
|
+
// accept them.
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
properties: {
|
|
30
|
+
action: {
|
|
31
|
+
type: "string",
|
|
32
|
+
enum: ["search", "add"],
|
|
33
|
+
description: "Whether to read from or write to memory.",
|
|
34
|
+
},
|
|
35
|
+
query: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Required when action='search'. Natural-language query.",
|
|
38
|
+
},
|
|
39
|
+
content: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Required when action='add'. The fact to remember.",
|
|
42
|
+
},
|
|
43
|
+
limit: {
|
|
44
|
+
type: "integer",
|
|
45
|
+
minimum: 1,
|
|
46
|
+
maximum: 50,
|
|
47
|
+
description: "Optional max results for search.",
|
|
48
|
+
},
|
|
49
|
+
tags: {
|
|
50
|
+
type: "object",
|
|
51
|
+
description: "Optional metadata merged into the stored memory.",
|
|
52
|
+
additionalProperties: true,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["action"],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
async run(input) {
|
|
60
|
+
const messages = [...input.messages];
|
|
61
|
+
const responses = [];
|
|
62
|
+
let toolCalls = 0;
|
|
63
|
+
const system = buildSystem(input.system, defaults.cacheSystem);
|
|
64
|
+
const baseParams = {
|
|
65
|
+
model: input.model ?? defaults.model,
|
|
66
|
+
max_tokens: input.maxTokens ?? defaults.maxTokens,
|
|
67
|
+
tools: [tool],
|
|
68
|
+
...(system ? { system } : {}),
|
|
69
|
+
};
|
|
70
|
+
if (defaults.thinkingBudgetTokens) {
|
|
71
|
+
baseParams.thinking = {
|
|
72
|
+
type: "enabled",
|
|
73
|
+
budget_tokens: defaults.thinkingBudgetTokens,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i < defaults.maxIterations; i++) {
|
|
77
|
+
const resp = await options.client.messages.create({
|
|
78
|
+
...baseParams,
|
|
79
|
+
messages,
|
|
80
|
+
});
|
|
81
|
+
responses.push(resp);
|
|
82
|
+
// Anthropic requires every tool_use block in an assistant turn to be
|
|
83
|
+
// answered with a matching tool_result in the next user turn. Collect
|
|
84
|
+
// ALL tool_use blocks (not just memory ones) so the message list stays
|
|
85
|
+
// valid; non-memory tools get a structured error result.
|
|
86
|
+
const allToolUses = resp.content.filter((b) => b.type === "tool_use");
|
|
87
|
+
messages.push({ role: "assistant", content: resp.content });
|
|
88
|
+
if (resp.stop_reason !== "tool_use" || allToolUses.length === 0) {
|
|
89
|
+
return finalize(messages, responses, toolCalls);
|
|
90
|
+
}
|
|
91
|
+
const toolResults = [];
|
|
92
|
+
for (const use of allToolUses) {
|
|
93
|
+
if (use.name !== MEMORY_TOOL_NAME) {
|
|
94
|
+
toolResults.push({
|
|
95
|
+
type: "tool_result",
|
|
96
|
+
tool_use_id: use.id,
|
|
97
|
+
is_error: true,
|
|
98
|
+
content: JSON.stringify({
|
|
99
|
+
error: `Unknown tool: ${use.name}. Only '${MEMORY_TOOL_NAME}' is available.`,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
toolCalls += 1;
|
|
105
|
+
const result = await runMemoryTool(use.input, options.getmnemo,
|
|
106
|
+
// Trusted server metadata (input.metadata, defaults.metadata)
|
|
107
|
+
// wins over any tool input fields a model could fabricate.
|
|
108
|
+
{ ...defaults.metadata, ...(input.metadata ?? {}) }, defaults.searchLimit);
|
|
109
|
+
toolResults.push({
|
|
110
|
+
type: "tool_result",
|
|
111
|
+
tool_use_id: use.id,
|
|
112
|
+
content: JSON.stringify(result),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
messages.push({ role: "user", content: toolResults });
|
|
116
|
+
}
|
|
117
|
+
return finalize(messages, responses, toolCalls);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function finalize(messages, responses, memoryToolCalls) {
|
|
122
|
+
const last = responses[responses.length - 1];
|
|
123
|
+
const text = last
|
|
124
|
+
? last.content
|
|
125
|
+
.filter((b) => b.type === "text")
|
|
126
|
+
.map((b) => b.text)
|
|
127
|
+
.join("\n")
|
|
128
|
+
.trim()
|
|
129
|
+
: "";
|
|
130
|
+
return { text, responses, memoryToolCalls, messages };
|
|
131
|
+
}
|
|
132
|
+
function buildSystem(system, cache) {
|
|
133
|
+
if (!system)
|
|
134
|
+
return undefined;
|
|
135
|
+
if (!cache)
|
|
136
|
+
return [{ type: "text", text: system }];
|
|
137
|
+
return [
|
|
138
|
+
{ type: "text", text: system, cache_control: { type: "ephemeral" } },
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
async function runMemoryTool(raw, client, baseMetadata, defaultLimit) {
|
|
142
|
+
const action = String(raw.action ?? "");
|
|
143
|
+
if (action === "search") {
|
|
144
|
+
const query = String(raw.query ?? "").trim();
|
|
145
|
+
if (!query)
|
|
146
|
+
return { error: "query is required for search" };
|
|
147
|
+
// Clamp the limit into [1, 50] regardless of what the model asked for —
|
|
148
|
+
// a hostile/buggy tool input could otherwise pass a huge value (or 0)
|
|
149
|
+
// that bypasses the JSON-schema bounds and torches the context window.
|
|
150
|
+
const requested = Number(raw.limit ?? defaultLimit);
|
|
151
|
+
const limit = Number.isFinite(requested)
|
|
152
|
+
? Math.min(50, Math.max(1, Math.floor(requested)))
|
|
153
|
+
: defaultLimit;
|
|
154
|
+
const results = (await client.search({ query, limit })).hits ?? [];
|
|
155
|
+
return { results };
|
|
156
|
+
}
|
|
157
|
+
if (action === "add") {
|
|
158
|
+
const content = String(raw.content ?? "").trim();
|
|
159
|
+
if (!content)
|
|
160
|
+
return { error: "content is required for add" };
|
|
161
|
+
const tags = typeof raw.tags === "object" && raw.tags
|
|
162
|
+
? raw.tags
|
|
163
|
+
: {};
|
|
164
|
+
// Model-supplied `tags` are merged FIRST so trusted server-controlled
|
|
165
|
+
// baseMetadata (userId, workspaceId, etc.) cannot be overwritten by an
|
|
166
|
+
// LLM via prompt injection — e.g. a model that emits
|
|
167
|
+
// tags={"userId":"victim"} would otherwise reattribute the memory.
|
|
168
|
+
const memory = await client.add({ content, metadata: { ...tags, ...baseMetadata } });
|
|
169
|
+
return { memory };
|
|
170
|
+
}
|
|
171
|
+
return { error: `unknown action: ${action}` };
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=memory-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-tool.js","sourceRoot":"","sources":["../src/memory-tool.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "getmnemo-anthropic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mnemo helper for the Anthropic SDK — memory tool, prompt caching, and extended thinking wired together.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"lint": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"getmnemo",
|
|
27
|
+
"anthropic",
|
|
28
|
+
"claude",
|
|
29
|
+
"memory",
|
|
30
|
+
"tool-use",
|
|
31
|
+
"prompt-caching"
|
|
32
|
+
],
|
|
33
|
+
"author": "Mnemo <founders@getmnemo.xyz>",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/ledgermem/getmnemo-anthropic.git"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"getmnemo": "^0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@anthropic-ai/sdk": ">=0.30.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"typescript": "^5.6.0",
|
|
48
|
+
"vitest": "^2.1.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.17.0"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://getmnemo.xyz"
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { withMemoryTool, MEMORY_TOOL_NAME } from "./memory-tool.js";
|
|
3
|
+
|
|
4
|
+
function fakeMnemo() {
|
|
5
|
+
return {
|
|
6
|
+
search: vi
|
|
7
|
+
.fn()
|
|
8
|
+
.mockResolvedValue([{ id: "m1", content: "user prefers dark mode" }]),
|
|
9
|
+
add: vi.fn().mockResolvedValue({ id: "m2", content: "stored" }),
|
|
10
|
+
update: vi.fn(),
|
|
11
|
+
delete: vi.fn(),
|
|
12
|
+
list: vi.fn(),
|
|
13
|
+
} as any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("withMemoryTool", () => {
|
|
17
|
+
it("short-circuits when the model returns text without tool use", async () => {
|
|
18
|
+
const client = {
|
|
19
|
+
messages: {
|
|
20
|
+
create: vi.fn().mockResolvedValue({
|
|
21
|
+
id: "r1",
|
|
22
|
+
stop_reason: "end_turn",
|
|
23
|
+
content: [{ type: "text", text: "Hi there." }],
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const wrap = withMemoryTool({ client, getmnemo: fakeMnemo() });
|
|
28
|
+
const out = await wrap.run({
|
|
29
|
+
system: "You are helpful.",
|
|
30
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
31
|
+
});
|
|
32
|
+
expect(out.text).toBe("Hi there.");
|
|
33
|
+
expect(out.memoryToolCalls).toBe(0);
|
|
34
|
+
expect(client.messages.create).toHaveBeenCalledOnce();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("runs the memory tool loop and feeds tool_result back", async () => {
|
|
38
|
+
const getmnemo = fakeMnemo();
|
|
39
|
+
const client = {
|
|
40
|
+
messages: {
|
|
41
|
+
create: vi
|
|
42
|
+
.fn()
|
|
43
|
+
.mockResolvedValueOnce({
|
|
44
|
+
id: "r1",
|
|
45
|
+
stop_reason: "tool_use",
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "tool_use",
|
|
49
|
+
id: "tu_1",
|
|
50
|
+
name: MEMORY_TOOL_NAME,
|
|
51
|
+
input: { action: "search", query: "ui prefs" },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
.mockResolvedValueOnce({
|
|
56
|
+
id: "r2",
|
|
57
|
+
stop_reason: "end_turn",
|
|
58
|
+
content: [{ type: "text", text: "You prefer dark mode." }],
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const wrap = withMemoryTool({ client, getmnemo });
|
|
63
|
+
const out = await wrap.run({
|
|
64
|
+
messages: [{ role: "user", content: "What do I prefer?" }],
|
|
65
|
+
});
|
|
66
|
+
expect(out.text).toBe("You prefer dark mode.");
|
|
67
|
+
expect(out.memoryToolCalls).toBe(1);
|
|
68
|
+
expect(getmnemo.search).toHaveBeenCalledWith("ui prefs", { limit: 5 });
|
|
69
|
+
// 2nd call should include the tool_result
|
|
70
|
+
const secondCall = client.messages.create.mock.calls[1]![0] as any;
|
|
71
|
+
const toolResultMsg = secondCall.messages.at(-1);
|
|
72
|
+
expect(toolResultMsg.role).toBe("user");
|
|
73
|
+
expect((toolResultMsg.content as any[])[0].type).toBe("tool_result");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("opts into prompt caching on the system block by default", async () => {
|
|
77
|
+
const client = {
|
|
78
|
+
messages: {
|
|
79
|
+
create: vi.fn().mockResolvedValue({
|
|
80
|
+
id: "r1",
|
|
81
|
+
stop_reason: "end_turn",
|
|
82
|
+
content: [{ type: "text", text: "ok" }],
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const wrap = withMemoryTool({ client, getmnemo: fakeMnemo() });
|
|
87
|
+
await wrap.run({
|
|
88
|
+
system: "stable instructions",
|
|
89
|
+
messages: [{ role: "user", content: "hi" }],
|
|
90
|
+
});
|
|
91
|
+
const params = client.messages.create.mock.calls[0]![0] as any;
|
|
92
|
+
expect(params.system[0].cache_control).toEqual({ type: "ephemeral" });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("enables extended thinking when configured", async () => {
|
|
96
|
+
const client = {
|
|
97
|
+
messages: {
|
|
98
|
+
create: vi.fn().mockResolvedValue({
|
|
99
|
+
id: "r1",
|
|
100
|
+
stop_reason: "end_turn",
|
|
101
|
+
content: [{ type: "text", text: "ok" }],
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const wrap = withMemoryTool({
|
|
106
|
+
client,
|
|
107
|
+
getmnemo: fakeMnemo(),
|
|
108
|
+
thinkingBudgetTokens: 4000,
|
|
109
|
+
});
|
|
110
|
+
await wrap.run({ messages: [{ role: "user", content: "hi" }] });
|
|
111
|
+
const params = client.messages.create.mock.calls[0]![0] as any;
|
|
112
|
+
expect(params.thinking).toEqual({ type: "enabled", budget_tokens: 4000 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("respects maxIterations and stops the loop", async () => {
|
|
116
|
+
const getmnemo = fakeMnemo();
|
|
117
|
+
const client = {
|
|
118
|
+
messages: {
|
|
119
|
+
create: vi.fn().mockResolvedValue({
|
|
120
|
+
id: "r",
|
|
121
|
+
stop_reason: "tool_use",
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: "tool_use",
|
|
125
|
+
id: "tu",
|
|
126
|
+
name: MEMORY_TOOL_NAME,
|
|
127
|
+
input: { action: "search", query: "x" },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const wrap = withMemoryTool({
|
|
134
|
+
client,
|
|
135
|
+
getmnemo,
|
|
136
|
+
maxIterations: 2,
|
|
137
|
+
});
|
|
138
|
+
const out = await wrap.run({
|
|
139
|
+
messages: [{ role: "user", content: "loop" }],
|
|
140
|
+
});
|
|
141
|
+
expect(client.messages.create).toHaveBeenCalledTimes(2);
|
|
142
|
+
expect(out.memoryToolCalls).toBe(2);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { Mnemo } from "getmnemo";
|
|
2
|
+
|
|
3
|
+
/** Structural subset of `Anthropic` we depend on — keeps the peer dep loose. */
|
|
4
|
+
interface AnthropicLike {
|
|
5
|
+
messages: {
|
|
6
|
+
create: (params: Record<string, unknown>) => Promise<MessagesCreateResult>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface MessagesCreateResult {
|
|
11
|
+
id: string;
|
|
12
|
+
content: Array<MessageBlock>;
|
|
13
|
+
stop_reason: string;
|
|
14
|
+
usage?: Record<string, number>;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type MessageBlock =
|
|
19
|
+
| { type: "text"; text: string }
|
|
20
|
+
| { type: "thinking"; thinking: string }
|
|
21
|
+
| {
|
|
22
|
+
type: "tool_use";
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
input: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interface ChatMessage {
|
|
29
|
+
role: "user" | "assistant";
|
|
30
|
+
content: string | Array<Record<string, unknown>>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WithMemoryToolOptions {
|
|
34
|
+
client: AnthropicLike;
|
|
35
|
+
getmnemo: Mnemo;
|
|
36
|
+
/** Defaults to claude-sonnet-4-7. Override per call too. */
|
|
37
|
+
model?: string;
|
|
38
|
+
/** Defaults to 1024. */
|
|
39
|
+
maxTokens?: number;
|
|
40
|
+
/** Default top-k for memory search. */
|
|
41
|
+
searchLimit?: number;
|
|
42
|
+
/** Wrap the system prompt with `cache_control: { type: "ephemeral" }`. */
|
|
43
|
+
cacheSystem?: boolean;
|
|
44
|
+
/** Enable extended thinking with this token budget. */
|
|
45
|
+
thinkingBudgetTokens?: number;
|
|
46
|
+
/** Extra static metadata merged into every persisted memory. */
|
|
47
|
+
metadata?: Record<string, unknown>;
|
|
48
|
+
/** Hard cap on tool-use loop iterations. */
|
|
49
|
+
maxIterations?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WithMemoryRunInput {
|
|
53
|
+
system?: string;
|
|
54
|
+
messages: ChatMessage[];
|
|
55
|
+
/** Per-call overrides. */
|
|
56
|
+
model?: string;
|
|
57
|
+
maxTokens?: number;
|
|
58
|
+
metadata?: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface WithMemoryRunResult {
|
|
62
|
+
/** Final assistant text concatenated across blocks. */
|
|
63
|
+
text: string;
|
|
64
|
+
/** All raw API responses produced during the tool-use loop. */
|
|
65
|
+
responses: MessagesCreateResult[];
|
|
66
|
+
/** Number of memory tool invocations. */
|
|
67
|
+
memoryToolCalls: number;
|
|
68
|
+
/** Final messages array after tool loop (useful for storing transcript). */
|
|
69
|
+
messages: ChatMessage[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface WithMemoryToolWrapper {
|
|
73
|
+
run(input: WithMemoryRunInput): Promise<WithMemoryRunResult>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const MEMORY_TOOL_NAME = "memory";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wrap an `Anthropic` client so a single `memory` tool (with `search`/`add`
|
|
80
|
+
* actions) is exposed to the model and the tool-use loop is handled for you.
|
|
81
|
+
*
|
|
82
|
+
* Also opts into prompt caching of the system prompt and (optionally) extended
|
|
83
|
+
* thinking — both are recommended for any long-lived assistant.
|
|
84
|
+
*/
|
|
85
|
+
export function withMemoryTool(
|
|
86
|
+
options: WithMemoryToolOptions,
|
|
87
|
+
): WithMemoryToolWrapper {
|
|
88
|
+
const defaults = {
|
|
89
|
+
model: options.model ?? "claude-sonnet-4-7",
|
|
90
|
+
maxTokens: options.maxTokens ?? 1024,
|
|
91
|
+
searchLimit: options.searchLimit ?? 5,
|
|
92
|
+
cacheSystem: options.cacheSystem ?? true,
|
|
93
|
+
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
|
94
|
+
metadata: options.metadata ?? {},
|
|
95
|
+
maxIterations: options.maxIterations ?? 6,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const tool = {
|
|
99
|
+
name: MEMORY_TOOL_NAME,
|
|
100
|
+
description:
|
|
101
|
+
"Long-term memory store. Use action='search' to recall facts about the user before answering, and action='add' to save a new fact worth remembering across conversations.",
|
|
102
|
+
input_schema: {
|
|
103
|
+
type: "object" as const,
|
|
104
|
+
// additionalProperties:false lets Anthropic enforce strict JSON schema
|
|
105
|
+
// for tool inputs — without it the model can pass arbitrary keys
|
|
106
|
+
// (including keys that look like trusted metadata) and we silently
|
|
107
|
+
// accept them.
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
properties: {
|
|
110
|
+
action: {
|
|
111
|
+
type: "string",
|
|
112
|
+
enum: ["search", "add"],
|
|
113
|
+
description: "Whether to read from or write to memory.",
|
|
114
|
+
},
|
|
115
|
+
query: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "Required when action='search'. Natural-language query.",
|
|
118
|
+
},
|
|
119
|
+
content: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "Required when action='add'. The fact to remember.",
|
|
122
|
+
},
|
|
123
|
+
limit: {
|
|
124
|
+
type: "integer",
|
|
125
|
+
minimum: 1,
|
|
126
|
+
maximum: 50,
|
|
127
|
+
description: "Optional max results for search.",
|
|
128
|
+
},
|
|
129
|
+
tags: {
|
|
130
|
+
type: "object",
|
|
131
|
+
description: "Optional metadata merged into the stored memory.",
|
|
132
|
+
additionalProperties: true,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
required: ["action"],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
async run(input: WithMemoryRunInput): Promise<WithMemoryRunResult> {
|
|
141
|
+
const messages: ChatMessage[] = [...input.messages];
|
|
142
|
+
const responses: MessagesCreateResult[] = [];
|
|
143
|
+
let toolCalls = 0;
|
|
144
|
+
|
|
145
|
+
const system = buildSystem(input.system, defaults.cacheSystem);
|
|
146
|
+
const baseParams: Record<string, unknown> = {
|
|
147
|
+
model: input.model ?? defaults.model,
|
|
148
|
+
max_tokens: input.maxTokens ?? defaults.maxTokens,
|
|
149
|
+
tools: [tool],
|
|
150
|
+
...(system ? { system } : {}),
|
|
151
|
+
};
|
|
152
|
+
if (defaults.thinkingBudgetTokens) {
|
|
153
|
+
baseParams.thinking = {
|
|
154
|
+
type: "enabled",
|
|
155
|
+
budget_tokens: defaults.thinkingBudgetTokens,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < defaults.maxIterations; i++) {
|
|
160
|
+
const resp = await options.client.messages.create({
|
|
161
|
+
...baseParams,
|
|
162
|
+
messages,
|
|
163
|
+
});
|
|
164
|
+
responses.push(resp);
|
|
165
|
+
|
|
166
|
+
// Anthropic requires every tool_use block in an assistant turn to be
|
|
167
|
+
// answered with a matching tool_result in the next user turn. Collect
|
|
168
|
+
// ALL tool_use blocks (not just memory ones) so the message list stays
|
|
169
|
+
// valid; non-memory tools get a structured error result.
|
|
170
|
+
const allToolUses = resp.content.filter(
|
|
171
|
+
(b): b is Extract<MessageBlock, { type: "tool_use" }> =>
|
|
172
|
+
b.type === "tool_use",
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
messages.push({ role: "assistant", content: resp.content });
|
|
176
|
+
|
|
177
|
+
if (resp.stop_reason !== "tool_use" || allToolUses.length === 0) {
|
|
178
|
+
return finalize(messages, responses, toolCalls);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const toolResults: Array<Record<string, unknown>> = [];
|
|
182
|
+
for (const use of allToolUses) {
|
|
183
|
+
if (use.name !== MEMORY_TOOL_NAME) {
|
|
184
|
+
toolResults.push({
|
|
185
|
+
type: "tool_result",
|
|
186
|
+
tool_use_id: use.id,
|
|
187
|
+
is_error: true,
|
|
188
|
+
content: JSON.stringify({
|
|
189
|
+
error: `Unknown tool: ${use.name}. Only '${MEMORY_TOOL_NAME}' is available.`,
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
toolCalls += 1;
|
|
195
|
+
const result = await runMemoryTool(
|
|
196
|
+
use.input,
|
|
197
|
+
options.getmnemo,
|
|
198
|
+
// Trusted server metadata (input.metadata, defaults.metadata)
|
|
199
|
+
// wins over any tool input fields a model could fabricate.
|
|
200
|
+
{ ...defaults.metadata, ...(input.metadata ?? {}) },
|
|
201
|
+
defaults.searchLimit,
|
|
202
|
+
);
|
|
203
|
+
toolResults.push({
|
|
204
|
+
type: "tool_result",
|
|
205
|
+
tool_use_id: use.id,
|
|
206
|
+
content: JSON.stringify(result),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
messages.push({ role: "user", content: toolResults });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return finalize(messages, responses, toolCalls);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function finalize(
|
|
218
|
+
messages: ChatMessage[],
|
|
219
|
+
responses: MessagesCreateResult[],
|
|
220
|
+
memoryToolCalls: number,
|
|
221
|
+
): WithMemoryRunResult {
|
|
222
|
+
const last = responses[responses.length - 1];
|
|
223
|
+
const text = last
|
|
224
|
+
? last.content
|
|
225
|
+
.filter((b): b is Extract<MessageBlock, { type: "text" }> => b.type === "text")
|
|
226
|
+
.map((b) => b.text)
|
|
227
|
+
.join("\n")
|
|
228
|
+
.trim()
|
|
229
|
+
: "";
|
|
230
|
+
return { text, responses, memoryToolCalls, messages };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildSystem(
|
|
234
|
+
system: string | undefined,
|
|
235
|
+
cache: boolean,
|
|
236
|
+
): Array<Record<string, unknown>> | undefined {
|
|
237
|
+
if (!system) return undefined;
|
|
238
|
+
if (!cache) return [{ type: "text", text: system }];
|
|
239
|
+
return [
|
|
240
|
+
{ type: "text", text: system, cache_control: { type: "ephemeral" } },
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function runMemoryTool(
|
|
245
|
+
raw: Record<string, unknown>,
|
|
246
|
+
client: Mnemo,
|
|
247
|
+
baseMetadata: Record<string, unknown>,
|
|
248
|
+
defaultLimit: number,
|
|
249
|
+
): Promise<unknown> {
|
|
250
|
+
const action = String(raw.action ?? "");
|
|
251
|
+
if (action === "search") {
|
|
252
|
+
const query = String(raw.query ?? "").trim();
|
|
253
|
+
if (!query) return { error: "query is required for search" };
|
|
254
|
+
// Clamp the limit into [1, 50] regardless of what the model asked for —
|
|
255
|
+
// a hostile/buggy tool input could otherwise pass a huge value (or 0)
|
|
256
|
+
// that bypasses the JSON-schema bounds and torches the context window.
|
|
257
|
+
const requested = Number(raw.limit ?? defaultLimit);
|
|
258
|
+
const limit = Number.isFinite(requested)
|
|
259
|
+
? Math.min(50, Math.max(1, Math.floor(requested)))
|
|
260
|
+
: defaultLimit;
|
|
261
|
+
const results = (await client.search({ query, limit })).hits ?? [];
|
|
262
|
+
return { results };
|
|
263
|
+
}
|
|
264
|
+
if (action === "add") {
|
|
265
|
+
const content = String(raw.content ?? "").trim();
|
|
266
|
+
if (!content) return { error: "content is required for add" };
|
|
267
|
+
const tags =
|
|
268
|
+
typeof raw.tags === "object" && raw.tags
|
|
269
|
+
? (raw.tags as Record<string, unknown>)
|
|
270
|
+
: {};
|
|
271
|
+
// Model-supplied `tags` are merged FIRST so trusted server-controlled
|
|
272
|
+
// baseMetadata (userId, workspaceId, etc.) cannot be overwritten by an
|
|
273
|
+
// LLM via prompt injection — e.g. a model that emits
|
|
274
|
+
// tags={"userId":"victim"} would otherwise reattribute the memory.
|
|
275
|
+
const memory = await client.add({ content, metadata: { ...tags, ...baseMetadata } });
|
|
276
|
+
return { memory };
|
|
277
|
+
}
|
|
278
|
+
return { error: `unknown action: ${action}` };
|
|
279
|
+
}
|