@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radaros/core",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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, sessionId, ctx);
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
- try {
150
- await this.config.userMemory.extractAndStore(
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
- } catch (e: unknown) {
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, sessionId, ctx);
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
- try {
280
- await this.config.userMemory.extractAndStore(
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
- } catch (e: unknown) {
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: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
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
- sessionId: string,
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 userContext = await this.config.userMemory.getContextString(ctx.userId);
333
- if (userContext) {
334
- systemContent = systemContent
335
- ? `${systemContent}\n\n${userContext}`
336
- : userContext;
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
- const history = await this.sessionManager.getHistory(sessionId, limit);
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: ${messages.map(m => `[${m.role}: ${typeof m.content === 'string' ? m.content.slice(0, 40) : '(multimodal)'}]`).join(', ')}`);
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
  }
@@ -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 this.provider.generate(
79
- currentMessages,
80
- modelConfig
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
- return zodToJsonSchema(schema, { target: "openApi3" }) as Record<string, unknown>;
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
  }
@@ -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: "openApi3",
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 as Record<string, unknown>,
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
+ }