@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radaros/core",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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
  }
@@ -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<{
@@ -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";
@@ -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));
@@ -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 === "text_delta") {
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 (currentToolId) {
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
- return {
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.text) {
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
- return {
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
  }