@radaros/core 0.3.3 → 0.3.5
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 +81 -31
- package/src/agent/llm-loop.ts +13 -4
- package/src/agent/types.ts +5 -0
- package/src/index.ts +4 -0
- package/src/memory/user-memory.ts +20 -0
- package/src/tools/tool-executor.ts +23 -3
- package/src/utils/retry.ts +56 -0
package/package.json
CHANGED
package/src/agent/agent.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Logger } from "../logger/logger.js";
|
|
|
7
7
|
import { LLMLoop } from "./llm-loop.js";
|
|
8
8
|
import { RunContext } from "./run-context.js";
|
|
9
9
|
import { getTextContent, type ChatMessage, type MessageContent, type StreamChunk } from "../models/types.js";
|
|
10
|
+
import type { Session } from "../session/types.js";
|
|
10
11
|
import type {
|
|
11
12
|
AgentConfig,
|
|
12
13
|
RunOpts,
|
|
@@ -72,6 +73,7 @@ export class Agent {
|
|
|
72
73
|
structuredOutput: config.structuredOutput,
|
|
73
74
|
logger: this.logger,
|
|
74
75
|
reasoning: config.reasoning,
|
|
76
|
+
retry: config.retry,
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -116,7 +118,7 @@ export class Agent {
|
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
const messages = await this.buildMessages(input,
|
|
121
|
+
const messages = await this.buildMessages(input, session, ctx);
|
|
120
122
|
const output = await this.llmLoop.run(messages, ctx, opts?.apiKey);
|
|
121
123
|
|
|
122
124
|
output.durationMs = Date.now() - startTime;
|
|
@@ -146,18 +148,16 @@ export class Agent {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
if (this.config.userMemory && userId) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
this.config.userMemory
|
|
152
|
+
.extractAndStore(
|
|
151
153
|
userId,
|
|
152
154
|
[
|
|
153
155
|
{ role: "user", content: inputText },
|
|
154
156
|
{ role: "assistant", content: output.text },
|
|
155
157
|
],
|
|
156
158
|
this.config.model
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
this.logger.warn(`UserMemory extraction failed: ${e}`);
|
|
160
|
-
}
|
|
159
|
+
)
|
|
160
|
+
.catch((e: unknown) => this.logger.warn(`UserMemory extraction failed: ${e}`));
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
if (this.config.hooks?.afterRun) {
|
|
@@ -220,6 +220,11 @@ export class Agent {
|
|
|
220
220
|
|
|
221
221
|
let fullText = "";
|
|
222
222
|
let streamOk = false;
|
|
223
|
+
let streamUsage: import("../models/types.js").TokenUsage = {
|
|
224
|
+
promptTokens: 0,
|
|
225
|
+
completionTokens: 0,
|
|
226
|
+
totalTokens: 0,
|
|
227
|
+
};
|
|
223
228
|
|
|
224
229
|
try {
|
|
225
230
|
if (this.config.hooks?.beforeRun) {
|
|
@@ -237,11 +242,20 @@ export class Agent {
|
|
|
237
242
|
}
|
|
238
243
|
}
|
|
239
244
|
|
|
240
|
-
const messages = await this.buildMessages(input,
|
|
245
|
+
const messages = await this.buildMessages(input, session, ctx);
|
|
241
246
|
|
|
242
247
|
for await (const chunk of this.llmLoop.stream(messages, ctx, opts?.apiKey)) {
|
|
243
248
|
if (chunk.type === "text") {
|
|
244
249
|
fullText += chunk.text;
|
|
250
|
+
} else if (chunk.type === "finish" && chunk.usage) {
|
|
251
|
+
streamUsage = {
|
|
252
|
+
promptTokens: streamUsage.promptTokens + chunk.usage.promptTokens,
|
|
253
|
+
completionTokens: streamUsage.completionTokens + chunk.usage.completionTokens,
|
|
254
|
+
totalTokens: streamUsage.totalTokens + chunk.usage.totalTokens,
|
|
255
|
+
...(chunk.usage.reasoningTokens
|
|
256
|
+
? { reasoningTokens: (streamUsage.reasoningTokens ?? 0) + chunk.usage.reasoningTokens }
|
|
257
|
+
: {}),
|
|
258
|
+
};
|
|
245
259
|
}
|
|
246
260
|
yield chunk;
|
|
247
261
|
}
|
|
@@ -276,18 +290,16 @@ export class Agent {
|
|
|
276
290
|
}
|
|
277
291
|
|
|
278
292
|
if (this.config.userMemory && userId) {
|
|
279
|
-
|
|
280
|
-
|
|
293
|
+
this.config.userMemory
|
|
294
|
+
.extractAndStore(
|
|
281
295
|
userId,
|
|
282
296
|
[
|
|
283
297
|
{ role: "user", content: inputText },
|
|
284
298
|
{ role: "assistant", content: fullText },
|
|
285
299
|
],
|
|
286
300
|
this.config.model
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
this.logger.warn(`UserMemory extraction failed: ${e}`);
|
|
290
|
-
}
|
|
301
|
+
)
|
|
302
|
+
.catch((e: unknown) => this.logger.warn(`UserMemory extraction failed: ${e}`));
|
|
291
303
|
}
|
|
292
304
|
|
|
293
305
|
this.eventBus.emit("run.complete", {
|
|
@@ -295,7 +307,7 @@ export class Agent {
|
|
|
295
307
|
output: {
|
|
296
308
|
text: fullText,
|
|
297
309
|
toolCalls: [],
|
|
298
|
-
usage:
|
|
310
|
+
usage: streamUsage,
|
|
299
311
|
},
|
|
300
312
|
});
|
|
301
313
|
}
|
|
@@ -304,7 +316,7 @@ export class Agent {
|
|
|
304
316
|
|
|
305
317
|
private async buildMessages(
|
|
306
318
|
input: MessageContent,
|
|
307
|
-
|
|
319
|
+
session: Session,
|
|
308
320
|
ctx: RunContext
|
|
309
321
|
): Promise<ChatMessage[]> {
|
|
310
322
|
const messages: ChatMessage[] = [];
|
|
@@ -319,7 +331,7 @@ export class Agent {
|
|
|
319
331
|
|
|
320
332
|
if (this.config.memory) {
|
|
321
333
|
const memoryContext = await this.config.memory.getContextString(
|
|
322
|
-
sessionId
|
|
334
|
+
session.sessionId
|
|
323
335
|
);
|
|
324
336
|
if (memoryContext) {
|
|
325
337
|
systemContent = systemContent
|
|
@@ -329,11 +341,16 @@ export class Agent {
|
|
|
329
341
|
}
|
|
330
342
|
|
|
331
343
|
if (this.config.userMemory && ctx.userId) {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
344
|
+
const hasRecallTool = (this.config.tools ?? []).some(
|
|
345
|
+
(t) => t.name === "recall_user_facts"
|
|
346
|
+
);
|
|
347
|
+
if (!hasRecallTool) {
|
|
348
|
+
const userContext = await this.config.userMemory.getContextString(ctx.userId);
|
|
349
|
+
if (userContext) {
|
|
350
|
+
systemContent = systemContent
|
|
351
|
+
? `${systemContent}\n\n${userContext}`
|
|
352
|
+
: userContext;
|
|
353
|
+
}
|
|
337
354
|
}
|
|
338
355
|
}
|
|
339
356
|
|
|
@@ -345,23 +362,56 @@ export class Agent {
|
|
|
345
362
|
const limit = this.config.numHistoryRuns
|
|
346
363
|
? this.config.numHistoryRuns * 2
|
|
347
364
|
: 20;
|
|
348
|
-
|
|
365
|
+
let history = session.messages ?? [];
|
|
366
|
+
if (limit > 0 && history.length > limit) {
|
|
367
|
+
history = history.slice(-limit);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (this.config.maxContextTokens) {
|
|
371
|
+
history = this.trimHistoryByTokens(history, systemContent, input);
|
|
372
|
+
}
|
|
373
|
+
|
|
349
374
|
if (history.length > 0) {
|
|
350
|
-
this.logger.info(`Loaded ${history.length} history messages for session ${sessionId}`);
|
|
351
|
-
if (messages.length > 0 && messages[0].role === "system") {
|
|
352
|
-
messages[0] = {
|
|
353
|
-
...messages[0],
|
|
354
|
-
content: `${getTextContent(messages[0].content)}\n\nThis is a multi-turn conversation. The previous messages in this session are included below. Use them to maintain context and answer questions about prior exchanges.`,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
375
|
+
this.logger.info(`Loaded ${history.length} history messages for session ${session.sessionId}`);
|
|
357
376
|
}
|
|
358
377
|
messages.push(...history);
|
|
359
378
|
}
|
|
360
379
|
|
|
361
380
|
messages.push({ role: "user", content: input });
|
|
362
381
|
|
|
363
|
-
this.logger.info(`Sending ${messages.length} messages to LLM
|
|
382
|
+
this.logger.info(`Sending ${messages.length} messages to LLM`);
|
|
364
383
|
|
|
365
384
|
return messages;
|
|
366
385
|
}
|
|
386
|
+
|
|
387
|
+
private estimateTokens(text: string): number {
|
|
388
|
+
return Math.ceil(text.length / 3.5);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private trimHistoryByTokens(
|
|
392
|
+
history: ChatMessage[],
|
|
393
|
+
systemContent: string,
|
|
394
|
+
currentInput: MessageContent
|
|
395
|
+
): ChatMessage[] {
|
|
396
|
+
const maxTokens = this.config.maxContextTokens!;
|
|
397
|
+
const inputText = typeof currentInput === "string" ? currentInput : "(multimodal)";
|
|
398
|
+
let reservedTokens = this.estimateTokens(systemContent) + this.estimateTokens(inputText) + 100;
|
|
399
|
+
|
|
400
|
+
const available = maxTokens - reservedTokens;
|
|
401
|
+
if (available <= 0) return [];
|
|
402
|
+
|
|
403
|
+
const result: ChatMessage[] = [];
|
|
404
|
+
let used = 0;
|
|
405
|
+
|
|
406
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
407
|
+
const msg = history[i];
|
|
408
|
+
const text = typeof msg.content === "string" ? msg.content : "";
|
|
409
|
+
const tokens = this.estimateTokens(text);
|
|
410
|
+
if (used + tokens > available) break;
|
|
411
|
+
used += tokens;
|
|
412
|
+
result.unshift(msg);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
367
417
|
}
|
package/src/agent/llm-loop.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { RunContext } from "./run-context.js";
|
|
|
14
14
|
import type { RunOutput } from "./types.js";
|
|
15
15
|
import type { ToolCallResult } from "../tools/types.js";
|
|
16
16
|
import type { Logger } from "../logger/logger.js";
|
|
17
|
+
import { withRetry, type RetryConfig } from "../utils/retry.js";
|
|
17
18
|
|
|
18
19
|
const _require = createRequire(import.meta.url);
|
|
19
20
|
|
|
@@ -26,6 +27,7 @@ export class LLMLoop {
|
|
|
26
27
|
private structuredOutput?: z.ZodSchema;
|
|
27
28
|
private logger?: Logger;
|
|
28
29
|
private reasoning?: ReasoningConfig;
|
|
30
|
+
private retry?: Partial<RetryConfig>;
|
|
29
31
|
|
|
30
32
|
constructor(
|
|
31
33
|
provider: ModelProvider,
|
|
@@ -37,6 +39,7 @@ export class LLMLoop {
|
|
|
37
39
|
structuredOutput?: z.ZodSchema;
|
|
38
40
|
logger?: Logger;
|
|
39
41
|
reasoning?: ReasoningConfig;
|
|
42
|
+
retry?: Partial<RetryConfig>;
|
|
40
43
|
}
|
|
41
44
|
) {
|
|
42
45
|
this.provider = provider;
|
|
@@ -47,6 +50,7 @@ export class LLMLoop {
|
|
|
47
50
|
this.structuredOutput = options.structuredOutput;
|
|
48
51
|
this.logger = options.logger;
|
|
49
52
|
this.reasoning = options.reasoning;
|
|
53
|
+
this.retry = options.retry;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
async run(messages: ChatMessage[], ctx: RunContext, apiKey?: string): Promise<RunOutput> {
|
|
@@ -75,9 +79,9 @@ export class LLMLoop {
|
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
const response = await
|
|
79
|
-
currentMessages,
|
|
80
|
-
|
|
82
|
+
const response = await withRetry(
|
|
83
|
+
() => this.provider.generate(currentMessages, modelConfig),
|
|
84
|
+
this.retry
|
|
81
85
|
);
|
|
82
86
|
|
|
83
87
|
totalPromptTokens += response.usage.promptTokens;
|
|
@@ -273,7 +277,12 @@ export class LLMLoop {
|
|
|
273
277
|
private zodToJsonSchema(schema: z.ZodSchema): Record<string, unknown> {
|
|
274
278
|
try {
|
|
275
279
|
const { zodToJsonSchema } = _require("zod-to-json-schema");
|
|
276
|
-
|
|
280
|
+
const result = zodToJsonSchema(schema, {
|
|
281
|
+
target: "jsonSchema7",
|
|
282
|
+
$refStrategy: "none",
|
|
283
|
+
}) as Record<string, unknown>;
|
|
284
|
+
delete result["$schema"];
|
|
285
|
+
return result;
|
|
277
286
|
} catch {
|
|
278
287
|
return {};
|
|
279
288
|
}
|
package/src/agent/types.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { TokenUsage, StreamChunk, MessageContent, ReasoningConfig } from ".
|
|
|
8
8
|
import type { RunContext } from "./run-context.js";
|
|
9
9
|
import type { LogLevel } from "../logger/logger.js";
|
|
10
10
|
import type { UserMemory } from "../memory/user-memory.js";
|
|
11
|
+
import type { RetryConfig } from "../utils/retry.js";
|
|
11
12
|
|
|
12
13
|
export interface AgentConfig {
|
|
13
14
|
name: string;
|
|
@@ -35,6 +36,10 @@ export interface AgentConfig {
|
|
|
35
36
|
reasoning?: ReasoningConfig;
|
|
36
37
|
/** User-scoped memory for cross-session personalization. */
|
|
37
38
|
userMemory?: UserMemory;
|
|
39
|
+
/** Retry configuration for transient LLM API failures (429, 5xx, network errors). */
|
|
40
|
+
retry?: Partial<RetryConfig>;
|
|
41
|
+
/** Maximum context window tokens. History is auto-trimmed to fit. */
|
|
42
|
+
maxContextTokens?: number;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export interface RunOpts {
|
package/src/index.ts
CHANGED
|
@@ -115,6 +115,10 @@ export type { AgentEventMap } from "./events/types.js";
|
|
|
115
115
|
export { Logger } from "./logger/logger.js";
|
|
116
116
|
export type { LogLevel, LoggerConfig } from "./logger/logger.js";
|
|
117
117
|
|
|
118
|
+
// Utils
|
|
119
|
+
export { withRetry } from "./utils/retry.js";
|
|
120
|
+
export type { RetryConfig } from "./utils/retry.js";
|
|
121
|
+
|
|
118
122
|
// MCP
|
|
119
123
|
export { MCPToolProvider } from "./mcp/mcp-client.js";
|
|
120
124
|
export type { MCPToolProviderConfig } from "./mcp/mcp-client.js";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { InMemoryStorage } from "../storage/in-memory.js";
|
|
3
4
|
import type { StorageDriver } from "../storage/driver.js";
|
|
4
5
|
import type { ModelProvider } from "../models/provider.js";
|
|
5
6
|
import type { ChatMessage } from "../models/types.js";
|
|
7
|
+
import type { ToolDef } from "../tools/types.js";
|
|
6
8
|
|
|
7
9
|
const USER_MEMORY_NS = "memory:user";
|
|
8
10
|
|
|
@@ -119,6 +121,24 @@ export class UserMemory {
|
|
|
119
121
|
return `What you know about this user:\n${factList}`;
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
asTool(config?: { name?: string; description?: string }): ToolDef {
|
|
125
|
+
const mem = this;
|
|
126
|
+
return {
|
|
127
|
+
name: config?.name ?? "recall_user_facts",
|
|
128
|
+
description:
|
|
129
|
+
config?.description ??
|
|
130
|
+
"Retrieve stored facts about the current user — preferences, background, interests, and other personal details from past conversations. Call this when the user asks what you know or remember about them.",
|
|
131
|
+
parameters: z.object({}),
|
|
132
|
+
execute: async (_args, ctx) => {
|
|
133
|
+
const uid = ctx.userId;
|
|
134
|
+
if (!uid) return "No user identified for this session.";
|
|
135
|
+
const facts = await mem.getFacts(uid);
|
|
136
|
+
if (facts.length === 0) return "No stored facts about this user yet.";
|
|
137
|
+
return facts.map((f) => `- ${f.fact}`).join("\n");
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
122
142
|
async extractAndStore(
|
|
123
143
|
userId: string,
|
|
124
144
|
messages: ChatMessage[],
|
|
@@ -14,10 +14,16 @@ export class ToolExecutor {
|
|
|
14
14
|
private tools: Map<string, ToolDef>;
|
|
15
15
|
private concurrency: number;
|
|
16
16
|
private cache = new Map<string, CacheEntry>();
|
|
17
|
+
private cachedDefs: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
parameters: Record<string, unknown>;
|
|
21
|
+
}> | null = null;
|
|
17
22
|
|
|
18
23
|
constructor(tools: ToolDef[], concurrency: number = 5) {
|
|
19
24
|
this.tools = new Map(tools.map((t) => [t.name, t]));
|
|
20
25
|
this.concurrency = concurrency;
|
|
26
|
+
this.cachedDefs = this.buildToolDefinitions();
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
clearCache(): void {
|
|
@@ -168,6 +174,16 @@ export class ToolExecutor {
|
|
|
168
174
|
name: string;
|
|
169
175
|
description: string;
|
|
170
176
|
parameters: Record<string, unknown>;
|
|
177
|
+
}> {
|
|
178
|
+
if (this.cachedDefs) return this.cachedDefs;
|
|
179
|
+
this.cachedDefs = this.buildToolDefinitions();
|
|
180
|
+
return this.cachedDefs;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private buildToolDefinitions(): Array<{
|
|
184
|
+
name: string;
|
|
185
|
+
description: string;
|
|
186
|
+
parameters: Record<string, unknown>;
|
|
171
187
|
}> {
|
|
172
188
|
const { zodToJsonSchema } = _require("zod-to-json-schema");
|
|
173
189
|
const defs: Array<{
|
|
@@ -185,13 +201,17 @@ export class ToolExecutor {
|
|
|
185
201
|
});
|
|
186
202
|
} else {
|
|
187
203
|
const jsonSchema = zodToJsonSchema(tool.parameters, {
|
|
188
|
-
target: "
|
|
189
|
-
|
|
204
|
+
target: "jsonSchema7",
|
|
205
|
+
$refStrategy: "none",
|
|
206
|
+
}) as Record<string, unknown>;
|
|
207
|
+
|
|
208
|
+
delete jsonSchema["$schema"];
|
|
209
|
+
delete jsonSchema["additionalProperties"];
|
|
190
210
|
|
|
191
211
|
defs.push({
|
|
192
212
|
name: tool.name,
|
|
193
213
|
description: tool.description,
|
|
194
|
-
parameters: jsonSchema
|
|
214
|
+
parameters: jsonSchema,
|
|
195
215
|
});
|
|
196
216
|
}
|
|
197
217
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface RetryConfig {
|
|
2
|
+
maxRetries: number;
|
|
3
|
+
initialDelayMs: number;
|
|
4
|
+
maxDelayMs: number;
|
|
5
|
+
retryableErrors?: (error: unknown) => boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG: RetryConfig = {
|
|
9
|
+
maxRetries: 3,
|
|
10
|
+
initialDelayMs: 500,
|
|
11
|
+
maxDelayMs: 10_000,
|
|
12
|
+
retryableErrors: isRetryableError,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function isRetryableError(error: unknown): boolean {
|
|
16
|
+
if (error && typeof error === "object") {
|
|
17
|
+
const status = (error as any).status ?? (error as any).statusCode;
|
|
18
|
+
if (status === 429 || (status >= 500 && status < 600)) return true;
|
|
19
|
+
|
|
20
|
+
const code = (error as any).code;
|
|
21
|
+
if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND") return true;
|
|
22
|
+
|
|
23
|
+
const msg = (error as any).message;
|
|
24
|
+
if (typeof msg === "string" && /rate.limit|too.many.requests|overloaded/i.test(msg)) return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sleep(ms: number): Promise<void> {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function withRetry<T>(
|
|
34
|
+
fn: () => Promise<T>,
|
|
35
|
+
config?: Partial<RetryConfig>
|
|
36
|
+
): Promise<T> {
|
|
37
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
38
|
+
let lastError: unknown;
|
|
39
|
+
|
|
40
|
+
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
return await fn();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
lastError = error;
|
|
45
|
+
if (attempt >= cfg.maxRetries || !cfg.retryableErrors!(error)) throw error;
|
|
46
|
+
|
|
47
|
+
const delay = Math.min(
|
|
48
|
+
cfg.initialDelayMs * Math.pow(2, attempt) + Math.random() * 200,
|
|
49
|
+
cfg.maxDelayMs
|
|
50
|
+
);
|
|
51
|
+
await sleep(delay);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw lastError;
|
|
56
|
+
}
|