@radaros/core 0.3.2 → 0.3.4

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.2",
3
+ "version": "0.3.4",
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,
@@ -22,6 +23,7 @@ export class Agent {
22
23
  private sessionManager: SessionManager;
23
24
  private llmLoop: LLMLoop;
24
25
  private logger: Logger;
26
+ private storageInitPromise: Promise<void> | null = null;
25
27
 
26
28
  get tools() {
27
29
  return this.config.tools ?? [];
@@ -50,6 +52,9 @@ export class Agent {
50
52
  this.eventBus = config.eventBus ?? new EventBus();
51
53
 
52
54
  const storage = config.storage ?? new InMemoryStorage();
55
+ if (typeof (storage as any).initialize === "function") {
56
+ this.storageInitPromise = (storage as any).initialize();
57
+ }
53
58
  this.sessionManager = new SessionManager(storage);
54
59
 
55
60
  this.logger = new Logger({
@@ -67,6 +72,8 @@ export class Agent {
67
72
  temperature: config.temperature,
68
73
  structuredOutput: config.structuredOutput,
69
74
  logger: this.logger,
75
+ reasoning: config.reasoning,
76
+ retry: config.retry,
70
77
  });
71
78
  }
72
79
 
@@ -76,6 +83,7 @@ export class Agent {
76
83
  const userId = opts?.userId ?? this.config.userId;
77
84
  const inputText = typeof input === "string" ? input : getTextContent(input);
78
85
 
86
+ if (this.storageInitPromise) await this.storageInitPromise;
79
87
  const session = await this.sessionManager.getOrCreate(sessionId, userId);
80
88
 
81
89
  const ctx = new RunContext({
@@ -110,7 +118,7 @@ export class Agent {
110
118
  }
111
119
  }
112
120
 
113
- const messages = await this.buildMessages(input, sessionId, ctx);
121
+ const messages = await this.buildMessages(input, session, ctx);
114
122
  const output = await this.llmLoop.run(messages, ctx, opts?.apiKey);
115
123
 
116
124
  output.durationMs = Date.now() - startTime;
@@ -139,10 +147,26 @@ export class Agent {
139
147
  ]);
140
148
  }
141
149
 
150
+ if (this.config.userMemory && userId) {
151
+ this.config.userMemory
152
+ .extractAndStore(
153
+ userId,
154
+ [
155
+ { role: "user", content: inputText },
156
+ { role: "assistant", content: output.text },
157
+ ],
158
+ this.config.model
159
+ )
160
+ .catch((e: unknown) => this.logger.warn(`UserMemory extraction failed: ${e}`));
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({
@@ -195,6 +220,11 @@ export class Agent {
195
220
 
196
221
  let fullText = "";
197
222
  let streamOk = false;
223
+ let streamUsage: import("../models/types.js").TokenUsage = {
224
+ promptTokens: 0,
225
+ completionTokens: 0,
226
+ totalTokens: 0,
227
+ };
198
228
 
199
229
  try {
200
230
  if (this.config.hooks?.beforeRun) {
@@ -212,11 +242,20 @@ export class Agent {
212
242
  }
213
243
  }
214
244
 
215
- const messages = await this.buildMessages(input, sessionId, ctx);
245
+ const messages = await this.buildMessages(input, session, ctx);
216
246
 
217
247
  for await (const chunk of this.llmLoop.stream(messages, ctx, opts?.apiKey)) {
218
248
  if (chunk.type === "text") {
219
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
+ };
220
259
  }
221
260
  yield chunk;
222
261
  }
@@ -250,12 +289,25 @@ export class Agent {
250
289
  ]);
251
290
  }
252
291
 
292
+ if (this.config.userMemory && userId) {
293
+ this.config.userMemory
294
+ .extractAndStore(
295
+ userId,
296
+ [
297
+ { role: "user", content: inputText },
298
+ { role: "assistant", content: fullText },
299
+ ],
300
+ this.config.model
301
+ )
302
+ .catch((e: unknown) => this.logger.warn(`UserMemory extraction failed: ${e}`));
303
+ }
304
+
253
305
  this.eventBus.emit("run.complete", {
254
306
  runId: ctx.runId,
255
307
  output: {
256
308
  text: fullText,
257
309
  toolCalls: [],
258
- usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
310
+ usage: streamUsage,
259
311
  },
260
312
  });
261
313
  }
@@ -264,7 +316,7 @@ export class Agent {
264
316
 
265
317
  private async buildMessages(
266
318
  input: MessageContent,
267
- sessionId: string,
319
+ session: Session,
268
320
  ctx: RunContext
269
321
  ): Promise<ChatMessage[]> {
270
322
  const messages: ChatMessage[] = [];
@@ -279,7 +331,7 @@ export class Agent {
279
331
 
280
332
  if (this.config.memory) {
281
333
  const memoryContext = await this.config.memory.getContextString(
282
- sessionId
334
+ session.sessionId
283
335
  );
284
336
  if (memoryContext) {
285
337
  systemContent = systemContent
@@ -288,6 +340,20 @@ export class Agent {
288
340
  }
289
341
  }
290
342
 
343
+ if (this.config.userMemory && ctx.userId) {
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
+ }
354
+ }
355
+ }
356
+
291
357
  if (systemContent) {
292
358
  messages.push({ role: "system", content: systemContent });
293
359
  }
@@ -296,23 +362,56 @@ export class Agent {
296
362
  const limit = this.config.numHistoryRuns
297
363
  ? this.config.numHistoryRuns * 2
298
364
  : 20;
299
- 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
+
300
374
  if (history.length > 0) {
301
- this.logger.info(`Loaded ${history.length} history messages for session ${sessionId}`);
302
- if (messages.length > 0 && messages[0].role === "system") {
303
- messages[0] = {
304
- ...messages[0],
305
- 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.`,
306
- };
307
- }
375
+ this.logger.info(`Loaded ${history.length} history messages for session ${session.sessionId}`);
308
376
  }
309
377
  messages.push(...history);
310
378
  }
311
379
 
312
380
  messages.push({ role: "user", content: input });
313
381
 
314
- 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`);
315
383
 
316
384
  return messages;
317
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
+ }
318
417
  }
@@ -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";
@@ -13,6 +14,7 @@ import type { RunContext } from "./run-context.js";
13
14
  import type { RunOutput } from "./types.js";
14
15
  import type { ToolCallResult } from "../tools/types.js";
15
16
  import type { Logger } from "../logger/logger.js";
17
+ import { withRetry, type RetryConfig } from "../utils/retry.js";
16
18
 
17
19
  const _require = createRequire(import.meta.url);
18
20
 
@@ -24,6 +26,8 @@ export class LLMLoop {
24
26
  private maxTokens?: number;
25
27
  private structuredOutput?: z.ZodSchema;
26
28
  private logger?: Logger;
29
+ private reasoning?: ReasoningConfig;
30
+ private retry?: Partial<RetryConfig>;
27
31
 
28
32
  constructor(
29
33
  provider: ModelProvider,
@@ -34,6 +38,8 @@ export class LLMLoop {
34
38
  maxTokens?: number;
35
39
  structuredOutput?: z.ZodSchema;
36
40
  logger?: Logger;
41
+ reasoning?: ReasoningConfig;
42
+ retry?: Partial<RetryConfig>;
37
43
  }
38
44
  ) {
39
45
  this.provider = provider;
@@ -43,12 +49,16 @@ export class LLMLoop {
43
49
  this.maxTokens = options.maxTokens;
44
50
  this.structuredOutput = options.structuredOutput;
45
51
  this.logger = options.logger;
52
+ this.reasoning = options.reasoning;
53
+ this.retry = options.retry;
46
54
  }
47
55
 
48
56
  async run(messages: ChatMessage[], ctx: RunContext, apiKey?: string): Promise<RunOutput> {
49
57
  const allToolCalls: ToolCallResult[] = [];
50
58
  let totalPromptTokens = 0;
51
59
  let totalCompletionTokens = 0;
60
+ let totalReasoningTokens = 0;
61
+ let thinkingContent = "";
52
62
  const currentMessages = [...messages];
53
63
  const toolDefs = this.toolExecutor?.getToolDefinitions() ?? [];
54
64
 
@@ -59,6 +69,7 @@ export class LLMLoop {
59
69
  modelConfig.temperature = this.temperature;
60
70
  if (this.maxTokens !== undefined) modelConfig.maxTokens = this.maxTokens;
61
71
  if (toolDefs.length > 0) modelConfig.tools = toolDefs;
72
+ if (this.reasoning) modelConfig.reasoning = this.reasoning;
62
73
 
63
74
  if (this.structuredOutput) {
64
75
  modelConfig.responseFormat = {
@@ -68,13 +79,18 @@ export class LLMLoop {
68
79
  };
69
80
  }
70
81
 
71
- const response = await this.provider.generate(
72
- currentMessages,
73
- modelConfig
82
+ const response = await withRetry(
83
+ () => this.provider.generate(currentMessages, modelConfig),
84
+ this.retry
74
85
  );
75
86
 
76
87
  totalPromptTokens += response.usage.promptTokens;
77
88
  totalCompletionTokens += response.usage.completionTokens;
89
+ if (response.usage.reasoningTokens) totalReasoningTokens += response.usage.reasoningTokens;
90
+
91
+ if ((response as any).thinking) {
92
+ thinkingContent += (thinkingContent ? "\n" : "") + (response as any).thinking;
93
+ }
78
94
 
79
95
  currentMessages.push(response.message);
80
96
 
@@ -92,9 +108,12 @@ export class LLMLoop {
92
108
  promptTokens: totalPromptTokens,
93
109
  completionTokens: totalCompletionTokens,
94
110
  totalTokens: totalPromptTokens + totalCompletionTokens,
111
+ ...(totalReasoningTokens > 0 ? { reasoningTokens: totalReasoningTokens } : {}),
95
112
  },
96
113
  };
97
114
 
115
+ if (thinkingContent) output.thinking = thinkingContent;
116
+
98
117
  if (this.structuredOutput && text) {
99
118
  try {
100
119
  const jsonStr = this.extractJson(text);
@@ -146,7 +165,9 @@ export class LLMLoop {
146
165
  promptTokens: totalPromptTokens,
147
166
  completionTokens: totalCompletionTokens,
148
167
  totalTokens: totalPromptTokens + totalCompletionTokens,
168
+ ...(totalReasoningTokens > 0 ? { reasoningTokens: totalReasoningTokens } : {}),
149
169
  },
170
+ ...(thinkingContent ? { thinking: thinkingContent } : {}),
150
171
  };
151
172
  }
152
173
 
@@ -165,6 +186,7 @@ export class LLMLoop {
165
186
  modelConfig.temperature = this.temperature;
166
187
  if (this.maxTokens !== undefined) modelConfig.maxTokens = this.maxTokens;
167
188
  if (toolDefs.length > 0) modelConfig.tools = toolDefs;
189
+ if (this.reasoning) modelConfig.reasoning = this.reasoning;
168
190
 
169
191
  let fullText = "";
170
192
  const pendingToolCalls: Array<{
@@ -255,7 +277,12 @@ export class LLMLoop {
255
277
  private zodToJsonSchema(schema: z.ZodSchema): Record<string, unknown> {
256
278
  try {
257
279
  const { zodToJsonSchema } = _require("zod-to-json-schema");
258
- 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;
259
286
  } catch {
260
287
  return {};
261
288
  }
@@ -4,9 +4,11 @@ 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";
11
+ import type { RetryConfig } from "../utils/retry.js";
10
12
 
11
13
  export interface AgentConfig {
12
14
  name: string;
@@ -30,6 +32,14 @@ export interface AgentConfig {
30
32
  eventBus?: EventBus;
31
33
  /** Logging level. Set to "debug" for tool call details, "info" for summaries, "silent" to disable. Default: "silent". */
32
34
  logLevel?: LogLevel;
35
+ /** Enable extended thinking / reasoning for the model. */
36
+ reasoning?: ReasoningConfig;
37
+ /** User-scoped memory for cross-session personalization. */
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;
33
43
  }
34
44
 
35
45
  export interface RunOpts {
@@ -46,6 +56,8 @@ export interface RunOutput {
46
56
  usage: TokenUsage;
47
57
  /** Parsed structured output if structuredOutput schema is set. */
48
58
  structured?: unknown;
59
+ /** Model's internal reasoning / thinking content (when reasoning is enabled). */
60
+ thinking?: string;
49
61
  durationMs?: number;
50
62
  }
51
63
 
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";
@@ -112,6 +115,10 @@ export type { AgentEventMap } from "./events/types.js";
112
115
  export { Logger } from "./logger/logger.js";
113
116
  export type { LogLevel, LoggerConfig } from "./logger/logger.js";
114
117
 
118
+ // Utils
119
+ export { withRetry } from "./utils/retry.js";
120
+ export type { RetryConfig } from "./utils/retry.js";
121
+
115
122
  // MCP
116
123
  export { MCPToolProvider } from "./mcp/mcp-client.js";
117
124
  export type { MCPToolProviderConfig } from "./mcp/mcp-client.js";
@@ -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
- const tokensLine =
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));