@radaros/core 0.3.2 → 0.3.3
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/package.json +1 -1
- package/src/agent/agent.ts +49 -0
- package/src/agent/llm-loop.ts +18 -0
- package/src/agent/types.ts +8 -1
- package/src/index.ts +4 -1
- package/src/logger/logger.ts +19 -2
- package/src/memory/user-memory.ts +191 -0
- package/src/models/providers/anthropic.ts +34 -4
- package/src/models/providers/google.ts +29 -4
- package/src/models/providers/openai.ts +28 -6
- package/src/models/providers/vertex.ts +31 -4
- package/src/models/types.ts +12 -0
- package/src/tools/define-tool.ts +3 -1
- package/src/tools/tool-executor.ts +63 -1
- package/src/tools/types.ts +7 -0
- package/dist/index.d.ts +0 -1317
- package/dist/index.js +0 -4823
package/package.json
CHANGED
package/src/agent/agent.ts
CHANGED
|
@@ -22,6 +22,7 @@ export class Agent {
|
|
|
22
22
|
private sessionManager: SessionManager;
|
|
23
23
|
private llmLoop: LLMLoop;
|
|
24
24
|
private logger: Logger;
|
|
25
|
+
private storageInitPromise: Promise<void> | null = null;
|
|
25
26
|
|
|
26
27
|
get tools() {
|
|
27
28
|
return this.config.tools ?? [];
|
|
@@ -50,6 +51,9 @@ export class Agent {
|
|
|
50
51
|
this.eventBus = config.eventBus ?? new EventBus();
|
|
51
52
|
|
|
52
53
|
const storage = config.storage ?? new InMemoryStorage();
|
|
54
|
+
if (typeof (storage as any).initialize === "function") {
|
|
55
|
+
this.storageInitPromise = (storage as any).initialize();
|
|
56
|
+
}
|
|
53
57
|
this.sessionManager = new SessionManager(storage);
|
|
54
58
|
|
|
55
59
|
this.logger = new Logger({
|
|
@@ -67,6 +71,7 @@ export class Agent {
|
|
|
67
71
|
temperature: config.temperature,
|
|
68
72
|
structuredOutput: config.structuredOutput,
|
|
69
73
|
logger: this.logger,
|
|
74
|
+
reasoning: config.reasoning,
|
|
70
75
|
});
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -76,6 +81,7 @@ export class Agent {
|
|
|
76
81
|
const userId = opts?.userId ?? this.config.userId;
|
|
77
82
|
const inputText = typeof input === "string" ? input : getTextContent(input);
|
|
78
83
|
|
|
84
|
+
if (this.storageInitPromise) await this.storageInitPromise;
|
|
79
85
|
const session = await this.sessionManager.getOrCreate(sessionId, userId);
|
|
80
86
|
|
|
81
87
|
const ctx = new RunContext({
|
|
@@ -139,10 +145,28 @@ export class Agent {
|
|
|
139
145
|
]);
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
if (this.config.userMemory && userId) {
|
|
149
|
+
try {
|
|
150
|
+
await this.config.userMemory.extractAndStore(
|
|
151
|
+
userId,
|
|
152
|
+
[
|
|
153
|
+
{ role: "user", content: inputText },
|
|
154
|
+
{ role: "assistant", content: output.text },
|
|
155
|
+
],
|
|
156
|
+
this.config.model
|
|
157
|
+
);
|
|
158
|
+
} catch (e: unknown) {
|
|
159
|
+
this.logger.warn(`UserMemory extraction failed: ${e}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
142
163
|
if (this.config.hooks?.afterRun) {
|
|
143
164
|
await this.config.hooks.afterRun(ctx, output);
|
|
144
165
|
}
|
|
145
166
|
|
|
167
|
+
if (output.thinking) {
|
|
168
|
+
this.logger.thinking(output.thinking);
|
|
169
|
+
}
|
|
146
170
|
this.logger.agentEnd(this.name, output.text, output.usage, output.durationMs);
|
|
147
171
|
|
|
148
172
|
this.eventBus.emit("run.complete", {
|
|
@@ -177,6 +201,7 @@ export class Agent {
|
|
|
177
201
|
const userId = opts?.userId ?? this.config.userId;
|
|
178
202
|
const inputText = typeof input === "string" ? input : getTextContent(input);
|
|
179
203
|
|
|
204
|
+
if (this.storageInitPromise) await this.storageInitPromise;
|
|
180
205
|
const session = await this.sessionManager.getOrCreate(sessionId, userId);
|
|
181
206
|
|
|
182
207
|
const ctx = new RunContext({
|
|
@@ -250,6 +275,21 @@ export class Agent {
|
|
|
250
275
|
]);
|
|
251
276
|
}
|
|
252
277
|
|
|
278
|
+
if (this.config.userMemory && userId) {
|
|
279
|
+
try {
|
|
280
|
+
await this.config.userMemory.extractAndStore(
|
|
281
|
+
userId,
|
|
282
|
+
[
|
|
283
|
+
{ role: "user", content: inputText },
|
|
284
|
+
{ role: "assistant", content: fullText },
|
|
285
|
+
],
|
|
286
|
+
this.config.model
|
|
287
|
+
);
|
|
288
|
+
} catch (e: unknown) {
|
|
289
|
+
this.logger.warn(`UserMemory extraction failed: ${e}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
253
293
|
this.eventBus.emit("run.complete", {
|
|
254
294
|
runId: ctx.runId,
|
|
255
295
|
output: {
|
|
@@ -288,6 +328,15 @@ export class Agent {
|
|
|
288
328
|
}
|
|
289
329
|
}
|
|
290
330
|
|
|
331
|
+
if (this.config.userMemory && ctx.userId) {
|
|
332
|
+
const userContext = await this.config.userMemory.getContextString(ctx.userId);
|
|
333
|
+
if (userContext) {
|
|
334
|
+
systemContent = systemContent
|
|
335
|
+
? `${systemContent}\n\n${userContext}`
|
|
336
|
+
: userContext;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
291
340
|
if (systemContent) {
|
|
292
341
|
messages.push({ role: "system", content: systemContent });
|
|
293
342
|
}
|
package/src/agent/llm-loop.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getTextContent,
|
|
6
6
|
type ChatMessage,
|
|
7
7
|
type ModelConfig,
|
|
8
|
+
type ReasoningConfig,
|
|
8
9
|
type StreamChunk,
|
|
9
10
|
type ToolDefinition,
|
|
10
11
|
} from "../models/types.js";
|
|
@@ -24,6 +25,7 @@ export class LLMLoop {
|
|
|
24
25
|
private maxTokens?: number;
|
|
25
26
|
private structuredOutput?: z.ZodSchema;
|
|
26
27
|
private logger?: Logger;
|
|
28
|
+
private reasoning?: ReasoningConfig;
|
|
27
29
|
|
|
28
30
|
constructor(
|
|
29
31
|
provider: ModelProvider,
|
|
@@ -34,6 +36,7 @@ export class LLMLoop {
|
|
|
34
36
|
maxTokens?: number;
|
|
35
37
|
structuredOutput?: z.ZodSchema;
|
|
36
38
|
logger?: Logger;
|
|
39
|
+
reasoning?: ReasoningConfig;
|
|
37
40
|
}
|
|
38
41
|
) {
|
|
39
42
|
this.provider = provider;
|
|
@@ -43,12 +46,15 @@ export class LLMLoop {
|
|
|
43
46
|
this.maxTokens = options.maxTokens;
|
|
44
47
|
this.structuredOutput = options.structuredOutput;
|
|
45
48
|
this.logger = options.logger;
|
|
49
|
+
this.reasoning = options.reasoning;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
async run(messages: ChatMessage[], ctx: RunContext, apiKey?: string): Promise<RunOutput> {
|
|
49
53
|
const allToolCalls: ToolCallResult[] = [];
|
|
50
54
|
let totalPromptTokens = 0;
|
|
51
55
|
let totalCompletionTokens = 0;
|
|
56
|
+
let totalReasoningTokens = 0;
|
|
57
|
+
let thinkingContent = "";
|
|
52
58
|
const currentMessages = [...messages];
|
|
53
59
|
const toolDefs = this.toolExecutor?.getToolDefinitions() ?? [];
|
|
54
60
|
|
|
@@ -59,6 +65,7 @@ export class LLMLoop {
|
|
|
59
65
|
modelConfig.temperature = this.temperature;
|
|
60
66
|
if (this.maxTokens !== undefined) modelConfig.maxTokens = this.maxTokens;
|
|
61
67
|
if (toolDefs.length > 0) modelConfig.tools = toolDefs;
|
|
68
|
+
if (this.reasoning) modelConfig.reasoning = this.reasoning;
|
|
62
69
|
|
|
63
70
|
if (this.structuredOutput) {
|
|
64
71
|
modelConfig.responseFormat = {
|
|
@@ -75,6 +82,11 @@ export class LLMLoop {
|
|
|
75
82
|
|
|
76
83
|
totalPromptTokens += response.usage.promptTokens;
|
|
77
84
|
totalCompletionTokens += response.usage.completionTokens;
|
|
85
|
+
if (response.usage.reasoningTokens) totalReasoningTokens += response.usage.reasoningTokens;
|
|
86
|
+
|
|
87
|
+
if ((response as any).thinking) {
|
|
88
|
+
thinkingContent += (thinkingContent ? "\n" : "") + (response as any).thinking;
|
|
89
|
+
}
|
|
78
90
|
|
|
79
91
|
currentMessages.push(response.message);
|
|
80
92
|
|
|
@@ -92,9 +104,12 @@ export class LLMLoop {
|
|
|
92
104
|
promptTokens: totalPromptTokens,
|
|
93
105
|
completionTokens: totalCompletionTokens,
|
|
94
106
|
totalTokens: totalPromptTokens + totalCompletionTokens,
|
|
107
|
+
...(totalReasoningTokens > 0 ? { reasoningTokens: totalReasoningTokens } : {}),
|
|
95
108
|
},
|
|
96
109
|
};
|
|
97
110
|
|
|
111
|
+
if (thinkingContent) output.thinking = thinkingContent;
|
|
112
|
+
|
|
98
113
|
if (this.structuredOutput && text) {
|
|
99
114
|
try {
|
|
100
115
|
const jsonStr = this.extractJson(text);
|
|
@@ -146,7 +161,9 @@ export class LLMLoop {
|
|
|
146
161
|
promptTokens: totalPromptTokens,
|
|
147
162
|
completionTokens: totalCompletionTokens,
|
|
148
163
|
totalTokens: totalPromptTokens + totalCompletionTokens,
|
|
164
|
+
...(totalReasoningTokens > 0 ? { reasoningTokens: totalReasoningTokens } : {}),
|
|
149
165
|
},
|
|
166
|
+
...(thinkingContent ? { thinking: thinkingContent } : {}),
|
|
150
167
|
};
|
|
151
168
|
}
|
|
152
169
|
|
|
@@ -165,6 +182,7 @@ export class LLMLoop {
|
|
|
165
182
|
modelConfig.temperature = this.temperature;
|
|
166
183
|
if (this.maxTokens !== undefined) modelConfig.maxTokens = this.maxTokens;
|
|
167
184
|
if (toolDefs.length > 0) modelConfig.tools = toolDefs;
|
|
185
|
+
if (this.reasoning) modelConfig.reasoning = this.reasoning;
|
|
168
186
|
|
|
169
187
|
let fullText = "";
|
|
170
188
|
const pendingToolCalls: Array<{
|
package/src/agent/types.ts
CHANGED
|
@@ -4,9 +4,10 @@ import type { ToolDef, ToolCallResult } from "../tools/types.js";
|
|
|
4
4
|
import type { Memory } from "../memory/memory.js";
|
|
5
5
|
import type { StorageDriver } from "../storage/driver.js";
|
|
6
6
|
import type { EventBus } from "../events/event-bus.js";
|
|
7
|
-
import type { TokenUsage, StreamChunk, MessageContent } from "../models/types.js";
|
|
7
|
+
import type { TokenUsage, StreamChunk, MessageContent, ReasoningConfig } from "../models/types.js";
|
|
8
8
|
import type { RunContext } from "./run-context.js";
|
|
9
9
|
import type { LogLevel } from "../logger/logger.js";
|
|
10
|
+
import type { UserMemory } from "../memory/user-memory.js";
|
|
10
11
|
|
|
11
12
|
export interface AgentConfig {
|
|
12
13
|
name: string;
|
|
@@ -30,6 +31,10 @@ export interface AgentConfig {
|
|
|
30
31
|
eventBus?: EventBus;
|
|
31
32
|
/** Logging level. Set to "debug" for tool call details, "info" for summaries, "silent" to disable. Default: "silent". */
|
|
32
33
|
logLevel?: LogLevel;
|
|
34
|
+
/** Enable extended thinking / reasoning for the model. */
|
|
35
|
+
reasoning?: ReasoningConfig;
|
|
36
|
+
/** User-scoped memory for cross-session personalization. */
|
|
37
|
+
userMemory?: UserMemory;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export interface RunOpts {
|
|
@@ -46,6 +51,8 @@ export interface RunOutput {
|
|
|
46
51
|
usage: TokenUsage;
|
|
47
52
|
/** Parsed structured output if structuredOutput schema is set. */
|
|
48
53
|
structured?: unknown;
|
|
54
|
+
/** Model's internal reasoning / thinking content (when reasoning is enabled). */
|
|
55
|
+
thinking?: string;
|
|
49
56
|
durationMs?: number;
|
|
50
57
|
}
|
|
51
58
|
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type {
|
|
|
47
47
|
ModelResponse,
|
|
48
48
|
StreamChunk,
|
|
49
49
|
ModelConfig,
|
|
50
|
+
ReasoningConfig,
|
|
50
51
|
} from "./models/types.js";
|
|
51
52
|
export { getTextContent, isMultiModal } from "./models/types.js";
|
|
52
53
|
export { ModelRegistry, registry, openai, anthropic, google, ollama, vertex } from "./models/registry.js";
|
|
@@ -60,7 +61,7 @@ export type { VertexAIConfig } from "./models/providers/vertex.js";
|
|
|
60
61
|
// Tools
|
|
61
62
|
export { defineTool } from "./tools/define-tool.js";
|
|
62
63
|
export { ToolExecutor } from "./tools/tool-executor.js";
|
|
63
|
-
export type { ToolDef, ToolResult, ToolCallResult, Artifact } from "./tools/types.js";
|
|
64
|
+
export type { ToolDef, ToolResult, ToolCallResult, Artifact, ToolCacheConfig } from "./tools/types.js";
|
|
64
65
|
|
|
65
66
|
// Storage
|
|
66
67
|
export type { StorageDriver } from "./storage/driver.js";
|
|
@@ -103,6 +104,8 @@ export type { Session } from "./session/types.js";
|
|
|
103
104
|
// Memory
|
|
104
105
|
export { Memory } from "./memory/memory.js";
|
|
105
106
|
export type { MemoryConfig, MemoryEntry } from "./memory/types.js";
|
|
107
|
+
export { UserMemory } from "./memory/user-memory.js";
|
|
108
|
+
export type { UserMemoryConfig, UserFact } from "./memory/user-memory.js";
|
|
106
109
|
|
|
107
110
|
// Events
|
|
108
111
|
export { EventBus } from "./events/event-bus.js";
|
package/src/logger/logger.ts
CHANGED
|
@@ -187,10 +187,23 @@ export class Logger {
|
|
|
187
187
|
console.log(this.pipe());
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
thinking(content: string): void {
|
|
191
|
+
if (!this.shouldLog("info")) return;
|
|
192
|
+
const truncated = content.length > 500 ? content.slice(0, 500) + "…" : content;
|
|
193
|
+
const label = this.c(C.dim + C.italic, "Thinking: ");
|
|
194
|
+
const lines = truncated.split("\n");
|
|
195
|
+
console.log(`${this.pipe()} ${label}${this.c(C.dim + C.italic, lines[0])}`);
|
|
196
|
+
const pad = " ".repeat(10);
|
|
197
|
+
for (let i = 1; i < lines.length; i++) {
|
|
198
|
+
console.log(`${this.pipe()} ${pad}${this.c(C.dim + C.italic, lines[i])}`);
|
|
199
|
+
}
|
|
200
|
+
console.log(this.pipe());
|
|
201
|
+
}
|
|
202
|
+
|
|
190
203
|
agentEnd(
|
|
191
204
|
agentName: string,
|
|
192
205
|
output: string,
|
|
193
|
-
usage: { promptTokens: number; completionTokens: number; totalTokens: number },
|
|
206
|
+
usage: { promptTokens: number; completionTokens: number; totalTokens: number; reasoningTokens?: number },
|
|
194
207
|
durationMs: number
|
|
195
208
|
): void {
|
|
196
209
|
if (!this.shouldLog("info")) return;
|
|
@@ -199,7 +212,7 @@ export class Logger {
|
|
|
199
212
|
this.printBoxLine("Output: ", output);
|
|
200
213
|
console.log(this.pipe());
|
|
201
214
|
|
|
202
|
-
|
|
215
|
+
let tokensLine =
|
|
203
216
|
this.c(C.dim, "Tokens: ") +
|
|
204
217
|
this.c(C.brightGreen, `↑ ${usage.promptTokens}`) +
|
|
205
218
|
this.c(C.dim, " ") +
|
|
@@ -207,6 +220,10 @@ export class Logger {
|
|
|
207
220
|
this.c(C.dim, " ") +
|
|
208
221
|
this.c(C.bold + C.brightGreen, `Σ ${usage.totalTokens}`);
|
|
209
222
|
|
|
223
|
+
if (usage.reasoningTokens) {
|
|
224
|
+
tokensLine += this.c(C.dim, " ") + this.c(C.brightMagenta, `🧠 ${usage.reasoningTokens}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
const duration =
|
|
211
228
|
this.c(C.dim, "Duration: ") +
|
|
212
229
|
this.c(C.yellow, this.formatDuration(durationMs));
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { InMemoryStorage } from "../storage/in-memory.js";
|
|
3
|
+
import type { StorageDriver } from "../storage/driver.js";
|
|
4
|
+
import type { ModelProvider } from "../models/provider.js";
|
|
5
|
+
import type { ChatMessage } from "../models/types.js";
|
|
6
|
+
|
|
7
|
+
const USER_MEMORY_NS = "memory:user";
|
|
8
|
+
|
|
9
|
+
export interface UserMemoryConfig {
|
|
10
|
+
storage?: StorageDriver;
|
|
11
|
+
/** LLM used for auto-extraction of facts from conversations. */
|
|
12
|
+
model?: ModelProvider;
|
|
13
|
+
/** Maximum number of facts stored per user (default 100). */
|
|
14
|
+
maxFacts?: number;
|
|
15
|
+
/** Whether auto-extraction is enabled (default true). */
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UserFact {
|
|
20
|
+
id: string;
|
|
21
|
+
fact: string;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
source: "auto" | "manual";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EXTRACTION_PROMPT = `You are a memory extraction assistant. Analyze the conversation below and extract important facts about the user that would be useful for future personalization.
|
|
27
|
+
|
|
28
|
+
Rules:
|
|
29
|
+
- Extract concrete facts like preferences, location, profession, interests, goals, communication style
|
|
30
|
+
- Each fact should be a short, self-contained statement (e.g., "Lives in Mumbai", "Prefers concise answers")
|
|
31
|
+
- Do NOT extract transient information (e.g., "asked about weather today")
|
|
32
|
+
- Do NOT extract information about the assistant
|
|
33
|
+
- If there are no new meaningful facts, return an empty array
|
|
34
|
+
- Return ONLY a valid JSON array of strings, nothing else
|
|
35
|
+
|
|
36
|
+
Existing facts about this user (avoid duplicates):
|
|
37
|
+
{existingFacts}
|
|
38
|
+
|
|
39
|
+
Conversation:
|
|
40
|
+
{conversation}
|
|
41
|
+
|
|
42
|
+
Return a JSON array of new fact strings:`;
|
|
43
|
+
|
|
44
|
+
export class UserMemory {
|
|
45
|
+
private storage: StorageDriver;
|
|
46
|
+
private model?: ModelProvider;
|
|
47
|
+
private maxFacts: number;
|
|
48
|
+
private enabled: boolean;
|
|
49
|
+
private initPromise: Promise<void> | null = null;
|
|
50
|
+
|
|
51
|
+
constructor(config?: UserMemoryConfig) {
|
|
52
|
+
this.storage = config?.storage ?? new InMemoryStorage();
|
|
53
|
+
this.model = config?.model;
|
|
54
|
+
this.maxFacts = config?.maxFacts ?? 100;
|
|
55
|
+
this.enabled = config?.enabled ?? true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private ensureInitialized(): Promise<void> {
|
|
59
|
+
if (!this.initPromise) {
|
|
60
|
+
this.initPromise = (async () => {
|
|
61
|
+
if (typeof (this.storage as any).initialize === "function") {
|
|
62
|
+
await (this.storage as any).initialize();
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
}
|
|
66
|
+
return this.initPromise;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getFacts(userId: string): Promise<UserFact[]> {
|
|
70
|
+
await this.ensureInitialized();
|
|
71
|
+
return (await this.storage.get<UserFact[]>(USER_MEMORY_NS, userId)) ?? [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async addFacts(userId: string, facts: string[], source: "auto" | "manual" = "manual"): Promise<void> {
|
|
75
|
+
await this.ensureInitialized();
|
|
76
|
+
const existing = await this.getFacts(userId);
|
|
77
|
+
const existingSet = new Set(existing.map((f) => f.fact.toLowerCase()));
|
|
78
|
+
|
|
79
|
+
const newFacts: UserFact[] = [];
|
|
80
|
+
for (const fact of facts) {
|
|
81
|
+
const normalized = fact.trim();
|
|
82
|
+
if (!normalized || existingSet.has(normalized.toLowerCase())) continue;
|
|
83
|
+
newFacts.push({
|
|
84
|
+
id: uuidv4(),
|
|
85
|
+
fact: normalized,
|
|
86
|
+
createdAt: new Date(),
|
|
87
|
+
source,
|
|
88
|
+
});
|
|
89
|
+
existingSet.add(normalized.toLowerCase());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (newFacts.length === 0) return;
|
|
93
|
+
|
|
94
|
+
let updated = [...existing, ...newFacts];
|
|
95
|
+
if (updated.length > this.maxFacts) {
|
|
96
|
+
updated = updated.slice(updated.length - this.maxFacts);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await this.storage.set(USER_MEMORY_NS, userId, updated);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async removeFact(userId: string, factId: string): Promise<void> {
|
|
103
|
+
await this.ensureInitialized();
|
|
104
|
+
const existing = await this.getFacts(userId);
|
|
105
|
+
const updated = existing.filter((f) => f.id !== factId);
|
|
106
|
+
await this.storage.set(USER_MEMORY_NS, userId, updated);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async clear(userId: string): Promise<void> {
|
|
110
|
+
await this.ensureInitialized();
|
|
111
|
+
await this.storage.delete(USER_MEMORY_NS, userId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getContextString(userId: string): Promise<string> {
|
|
115
|
+
if (!this.enabled) return "";
|
|
116
|
+
const facts = await this.getFacts(userId);
|
|
117
|
+
if (facts.length === 0) return "";
|
|
118
|
+
const factList = facts.map((f) => `- ${f.fact}`).join("\n");
|
|
119
|
+
return `What you know about this user:\n${factList}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async extractAndStore(
|
|
123
|
+
userId: string,
|
|
124
|
+
messages: ChatMessage[],
|
|
125
|
+
fallbackModel?: ModelProvider
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
if (!this.enabled) return;
|
|
128
|
+
|
|
129
|
+
const model = this.model ?? fallbackModel;
|
|
130
|
+
if (!model) return;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const existing = await this.getFacts(userId);
|
|
134
|
+
const existingStr =
|
|
135
|
+
existing.length > 0
|
|
136
|
+
? existing.map((f) => `- ${f.fact}`).join("\n")
|
|
137
|
+
: "(none)";
|
|
138
|
+
|
|
139
|
+
const conversationStr = messages
|
|
140
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
141
|
+
.map((m) => {
|
|
142
|
+
const content = typeof m.content === "string" ? m.content : "(multimodal)";
|
|
143
|
+
return `${m.role}: ${content}`;
|
|
144
|
+
})
|
|
145
|
+
.join("\n");
|
|
146
|
+
|
|
147
|
+
const prompt = EXTRACTION_PROMPT
|
|
148
|
+
.replace("{existingFacts}", existingStr)
|
|
149
|
+
.replace("{conversation}", conversationStr);
|
|
150
|
+
|
|
151
|
+
const response = await model.generate(
|
|
152
|
+
[{ role: "user", content: prompt }],
|
|
153
|
+
{ temperature: 0, maxTokens: 500 }
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const text =
|
|
157
|
+
typeof response.message.content === "string"
|
|
158
|
+
? response.message.content
|
|
159
|
+
: "";
|
|
160
|
+
|
|
161
|
+
if (!text) return;
|
|
162
|
+
|
|
163
|
+
const jsonStr = this.extractJsonArray(text);
|
|
164
|
+
const parsed = JSON.parse(jsonStr);
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
167
|
+
const validFacts = parsed.filter(
|
|
168
|
+
(f: unknown) => typeof f === "string" && f.trim().length > 0
|
|
169
|
+
);
|
|
170
|
+
if (validFacts.length > 0) {
|
|
171
|
+
await this.addFacts(userId, validFacts, "auto");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.warn("[UserMemory] extractAndStore failed:", (err as Error).message ?? err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private extractJsonArray(text: string): string {
|
|
180
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
181
|
+
if (fenceMatch) return fenceMatch[1].trim();
|
|
182
|
+
|
|
183
|
+
const bracketStart = text.indexOf("[");
|
|
184
|
+
const bracketEnd = text.lastIndexOf("]");
|
|
185
|
+
if (bracketStart !== -1 && bracketEnd > bracketStart) {
|
|
186
|
+
return text.slice(bracketStart, bracketEnd + 1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return text.trim();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -81,6 +81,13 @@ export class AnthropicProvider implements ModelProvider {
|
|
|
81
81
|
if (options?.tools?.length) {
|
|
82
82
|
params.tools = this.toAnthropicTools(options.tools);
|
|
83
83
|
}
|
|
84
|
+
if (options?.reasoning?.enabled) {
|
|
85
|
+
params.thinking = {
|
|
86
|
+
type: "enabled",
|
|
87
|
+
budget_tokens: options.reasoning.budgetTokens ?? 10000,
|
|
88
|
+
};
|
|
89
|
+
delete params.temperature;
|
|
90
|
+
}
|
|
84
91
|
|
|
85
92
|
const client = this.getClient(options?.apiKey);
|
|
86
93
|
const response = await client.messages.create(params);
|
|
@@ -109,11 +116,19 @@ export class AnthropicProvider implements ModelProvider {
|
|
|
109
116
|
if (options?.tools?.length) {
|
|
110
117
|
params.tools = this.toAnthropicTools(options.tools);
|
|
111
118
|
}
|
|
119
|
+
if (options?.reasoning?.enabled) {
|
|
120
|
+
params.thinking = {
|
|
121
|
+
type: "enabled",
|
|
122
|
+
budget_tokens: options.reasoning.budgetTokens ?? 10000,
|
|
123
|
+
};
|
|
124
|
+
delete params.temperature;
|
|
125
|
+
}
|
|
112
126
|
|
|
113
127
|
const client = this.getClient(options?.apiKey);
|
|
114
128
|
const stream = await client.messages.create(params);
|
|
115
129
|
|
|
116
130
|
let currentToolId = "";
|
|
131
|
+
let inThinkingBlock = false;
|
|
117
132
|
|
|
118
133
|
for await (const event of stream) {
|
|
119
134
|
switch (event.type) {
|
|
@@ -127,11 +142,15 @@ export class AnthropicProvider implements ModelProvider {
|
|
|
127
142
|
name: event.content_block.name,
|
|
128
143
|
},
|
|
129
144
|
};
|
|
145
|
+
} else if (event.content_block?.type === "thinking") {
|
|
146
|
+
inThinkingBlock = true;
|
|
130
147
|
}
|
|
131
148
|
break;
|
|
132
149
|
}
|
|
133
150
|
case "content_block_delta": {
|
|
134
|
-
if (event.delta?.type === "
|
|
151
|
+
if (event.delta?.type === "thinking_delta") {
|
|
152
|
+
yield { type: "thinking", text: event.delta.thinking };
|
|
153
|
+
} else if (event.delta?.type === "text_delta") {
|
|
135
154
|
yield { type: "text", text: event.delta.text };
|
|
136
155
|
} else if (event.delta?.type === "input_json_delta") {
|
|
137
156
|
yield {
|
|
@@ -143,7 +162,9 @@ export class AnthropicProvider implements ModelProvider {
|
|
|
143
162
|
break;
|
|
144
163
|
}
|
|
145
164
|
case "content_block_stop": {
|
|
146
|
-
if (
|
|
165
|
+
if (inThinkingBlock) {
|
|
166
|
+
inThinkingBlock = false;
|
|
167
|
+
} else if (currentToolId) {
|
|
147
168
|
yield { type: "tool_call_end", toolCallId: currentToolId };
|
|
148
169
|
currentToolId = "";
|
|
149
170
|
}
|
|
@@ -288,13 +309,16 @@ export class AnthropicProvider implements ModelProvider {
|
|
|
288
309
|
}));
|
|
289
310
|
}
|
|
290
311
|
|
|
291
|
-
private normalizeResponse(response: any): ModelResponse {
|
|
312
|
+
private normalizeResponse(response: any): ModelResponse & { thinking?: string } {
|
|
292
313
|
const toolCalls: ToolCall[] = [];
|
|
293
314
|
let textContent = "";
|
|
315
|
+
let thinkingContent = "";
|
|
294
316
|
|
|
295
317
|
for (const block of response.content ?? []) {
|
|
296
318
|
if (block.type === "text") {
|
|
297
319
|
textContent += block.text;
|
|
320
|
+
} else if (block.type === "thinking") {
|
|
321
|
+
thinkingContent += block.thinking;
|
|
298
322
|
} else if (block.type === "tool_use") {
|
|
299
323
|
toolCalls.push({
|
|
300
324
|
id: block.id,
|
|
@@ -316,7 +340,7 @@ export class AnthropicProvider implements ModelProvider {
|
|
|
316
340
|
if (response.stop_reason === "tool_use") finishReason = "tool_calls";
|
|
317
341
|
else if (response.stop_reason === "max_tokens") finishReason = "length";
|
|
318
342
|
|
|
319
|
-
|
|
343
|
+
const result: ModelResponse & { thinking?: string } = {
|
|
320
344
|
message: {
|
|
321
345
|
role: "assistant",
|
|
322
346
|
content: textContent || null,
|
|
@@ -326,5 +350,11 @@ export class AnthropicProvider implements ModelProvider {
|
|
|
326
350
|
finishReason,
|
|
327
351
|
raw: response,
|
|
328
352
|
};
|
|
353
|
+
|
|
354
|
+
if (thinkingContent) {
|
|
355
|
+
result.thinking = thinkingContent;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return result;
|
|
329
359
|
}
|
|
330
360
|
}
|
|
@@ -82,6 +82,12 @@ export class GoogleProvider implements ModelProvider {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (options?.reasoning?.enabled) {
|
|
86
|
+
config.thinkingConfig = {
|
|
87
|
+
thinkingBudget: options.reasoning.budgetTokens ?? 10000,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
const params: Record<string, unknown> = {
|
|
86
92
|
model: this.modelId,
|
|
87
93
|
contents,
|
|
@@ -116,6 +122,12 @@ export class GoogleProvider implements ModelProvider {
|
|
|
116
122
|
if (options?.topP !== undefined) config.topP = options.topP;
|
|
117
123
|
if (options?.stop) config.stopSequences = options.stop;
|
|
118
124
|
|
|
125
|
+
if (options?.reasoning?.enabled) {
|
|
126
|
+
config.thinkingConfig = {
|
|
127
|
+
thinkingBudget: options.reasoning.budgetTokens ?? 10000,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
const params: Record<string, unknown> = {
|
|
120
132
|
model: this.modelId,
|
|
121
133
|
contents,
|
|
@@ -142,7 +154,9 @@ export class GoogleProvider implements ModelProvider {
|
|
|
142
154
|
if (!candidate?.content?.parts) continue;
|
|
143
155
|
|
|
144
156
|
for (const part of candidate.content.parts) {
|
|
145
|
-
if (part.
|
|
157
|
+
if (part.thought) {
|
|
158
|
+
yield { type: "thinking", text: part.text ?? "" };
|
|
159
|
+
} else if (part.text) {
|
|
146
160
|
yield { type: "text", text: part.text };
|
|
147
161
|
}
|
|
148
162
|
|
|
@@ -313,16 +327,19 @@ export class GoogleProvider implements ModelProvider {
|
|
|
313
327
|
return cleaned;
|
|
314
328
|
}
|
|
315
329
|
|
|
316
|
-
private normalizeResponse(response: any): ModelResponse {
|
|
330
|
+
private normalizeResponse(response: any): ModelResponse & { thinking?: string } {
|
|
317
331
|
const candidate = response.candidates?.[0];
|
|
318
332
|
const parts = candidate?.content?.parts ?? [];
|
|
319
333
|
|
|
320
334
|
let textContent = "";
|
|
335
|
+
let thinkingContent = "";
|
|
321
336
|
const toolCalls: ToolCall[] = [];
|
|
322
337
|
let toolCallCounter = 0;
|
|
323
338
|
|
|
324
339
|
for (const part of parts) {
|
|
325
|
-
if (part.text) {
|
|
340
|
+
if (part.thought && part.text) {
|
|
341
|
+
thinkingContent += part.text;
|
|
342
|
+
} else if (part.text) {
|
|
326
343
|
textContent += part.text;
|
|
327
344
|
}
|
|
328
345
|
if (part.functionCall) {
|
|
@@ -334,10 +351,12 @@ export class GoogleProvider implements ModelProvider {
|
|
|
334
351
|
}
|
|
335
352
|
}
|
|
336
353
|
|
|
354
|
+
const thinkingTokens = response.usageMetadata?.thoughtsTokenCount ?? 0;
|
|
337
355
|
const usage: TokenUsage = {
|
|
338
356
|
promptTokens: response.usageMetadata?.promptTokenCount ?? 0,
|
|
339
357
|
completionTokens: response.usageMetadata?.candidatesTokenCount ?? 0,
|
|
340
358
|
totalTokens: response.usageMetadata?.totalTokenCount ?? 0,
|
|
359
|
+
...(thinkingTokens > 0 ? { reasoningTokens: thinkingTokens } : {}),
|
|
341
360
|
};
|
|
342
361
|
|
|
343
362
|
let finishReason: ModelResponse["finishReason"] = "stop";
|
|
@@ -347,7 +366,7 @@ export class GoogleProvider implements ModelProvider {
|
|
|
347
366
|
else if (candidate?.finishReason === "SAFETY")
|
|
348
367
|
finishReason = "content_filter";
|
|
349
368
|
|
|
350
|
-
|
|
369
|
+
const result: ModelResponse & { thinking?: string } = {
|
|
351
370
|
message: {
|
|
352
371
|
role: "assistant",
|
|
353
372
|
content: textContent || null,
|
|
@@ -357,5 +376,11 @@ export class GoogleProvider implements ModelProvider {
|
|
|
357
376
|
finishReason,
|
|
358
377
|
raw: response,
|
|
359
378
|
};
|
|
379
|
+
|
|
380
|
+
if (thinkingContent) {
|
|
381
|
+
result.thinking = thinkingContent;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return result;
|
|
360
385
|
}
|
|
361
386
|
}
|