@compilr-dev/agents 0.3.0 → 0.3.2

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.
@@ -83,13 +83,20 @@ export declare function normalizeMessages(messages: Message[]): Message[];
83
83
  /**
84
84
  * Repair tool use/result pairing issues in a message array.
85
85
  *
86
- * This function removes orphaned tool_result blocks (those without a matching tool_use)
87
- * which can cause API errors like "function_response.name: Name cannot be empty" in Gemini.
86
+ * This function handles BOTH types of orphans with POSITIONAL requirements:
87
+ * 1. Orphaned tool_result blocks (those without a matching tool_use BEFORE them)
88
+ * - Causes: "function_response.name: Name cannot be empty" in Gemini
89
+ * 2. Orphaned tool_use blocks (those without tool_result in IMMEDIATELY NEXT message)
90
+ * - Causes: "tool_use ids were found without tool_result blocks" in Claude
88
91
  *
89
- * This is particularly useful after context compaction/summarization, which may remove
90
- * tool_use messages while preserving tool_result messages in the "recent" window.
92
+ * CRITICAL: Claude API requires tool_result to be in the IMMEDIATELY NEXT message
93
+ * after the assistant message containing tool_use. This function validates that
94
+ * positional requirement, not just existence anywhere in the array.
95
+ *
96
+ * This is particularly useful after context compaction/summarization, which may
97
+ * remove messages and break tool_use/tool_result pairing.
91
98
  *
92
99
  * @param messages - Array of messages to repair
93
- * @returns New array with orphaned tool_results removed
100
+ * @returns New array with orphaned tool_use and tool_result blocks removed
94
101
  */
95
102
  export declare function repairToolPairing(messages: Message[]): Message[];
@@ -156,39 +156,77 @@ export function normalizeMessages(messages) {
156
156
  /**
157
157
  * Repair tool use/result pairing issues in a message array.
158
158
  *
159
- * This function removes orphaned tool_result blocks (those without a matching tool_use)
160
- * which can cause API errors like "function_response.name: Name cannot be empty" in Gemini.
159
+ * This function handles BOTH types of orphans with POSITIONAL requirements:
160
+ * 1. Orphaned tool_result blocks (those without a matching tool_use BEFORE them)
161
+ * - Causes: "function_response.name: Name cannot be empty" in Gemini
162
+ * 2. Orphaned tool_use blocks (those without tool_result in IMMEDIATELY NEXT message)
163
+ * - Causes: "tool_use ids were found without tool_result blocks" in Claude
161
164
  *
162
- * This is particularly useful after context compaction/summarization, which may remove
163
- * tool_use messages while preserving tool_result messages in the "recent" window.
165
+ * CRITICAL: Claude API requires tool_result to be in the IMMEDIATELY NEXT message
166
+ * after the assistant message containing tool_use. This function validates that
167
+ * positional requirement, not just existence anywhere in the array.
168
+ *
169
+ * This is particularly useful after context compaction/summarization, which may
170
+ * remove messages and break tool_use/tool_result pairing.
164
171
  *
165
172
  * @param messages - Array of messages to repair
166
- * @returns New array with orphaned tool_results removed
173
+ * @returns New array with orphaned tool_use and tool_result blocks removed
167
174
  */
168
175
  export function repairToolPairing(messages) {
169
- // First, collect all tool_use IDs
170
- const toolUseIds = new Set();
171
- for (const message of messages) {
176
+ // First pass: collect all tool_use IDs that appear BEFORE their tool_result
177
+ // Also track which tool_use IDs have their result in the IMMEDIATELY NEXT message
178
+ const validToolUseIds = new Set();
179
+ const allToolUseIds = new Set();
180
+ for (let i = 0; i < messages.length; i++) {
181
+ const message = messages[i];
172
182
  if (typeof message.content === 'string')
173
183
  continue;
174
- for (const block of message.content) {
175
- if (block.type === 'tool_use') {
176
- toolUseIds.add(block.id);
184
+ // Check for tool_use blocks in assistant messages
185
+ if (message.role === 'assistant') {
186
+ const toolUseIdsInThisMessage = [];
187
+ for (const block of message.content) {
188
+ if (block.type === 'tool_use') {
189
+ toolUseIdsInThisMessage.push(block.id);
190
+ allToolUseIds.add(block.id);
191
+ }
192
+ }
193
+ // Check if the NEXT message contains tool_result for ALL these tool_use IDs
194
+ if (toolUseIdsInThisMessage.length > 0) {
195
+ // Next message may not exist if we're at the end of the array
196
+ const nextMessage = messages[i + 1];
197
+ if (nextMessage && typeof nextMessage.content !== 'string') {
198
+ const toolResultIdsInNext = new Set();
199
+ for (const block of nextMessage.content) {
200
+ if (block.type === 'tool_result') {
201
+ toolResultIdsInNext.add(block.toolUseId);
202
+ }
203
+ }
204
+ // Mark tool_use IDs as valid only if their result is in the next message
205
+ for (const id of toolUseIdsInThisMessage) {
206
+ if (toolResultIdsInNext.has(id)) {
207
+ validToolUseIds.add(id);
208
+ }
209
+ }
210
+ }
177
211
  }
178
212
  }
179
213
  }
180
- // Now filter out orphaned tool_results
214
+ // Second pass: filter out orphaned blocks
181
215
  const repairedMessages = [];
182
216
  for (const message of messages) {
183
217
  if (typeof message.content === 'string') {
184
218
  repairedMessages.push(message);
185
219
  continue;
186
220
  }
187
- // Filter content blocks to remove orphaned tool_results
221
+ // Filter content blocks to remove orphaned tool_use and tool_result
188
222
  const filteredBlocks = message.content.filter((block) => {
189
223
  if (block.type === 'tool_result') {
190
- // Only keep if there's a matching tool_use
191
- return toolUseIds.has(block.toolUseId);
224
+ // Only keep if there's a valid matching tool_use (that also has its result properly placed)
225
+ return validToolUseIds.has(block.toolUseId);
226
+ }
227
+ if (block.type === 'tool_use') {
228
+ // Only keep if this tool_use has its result in the immediately next message
229
+ return validToolUseIds.has(block.id);
192
230
  }
193
231
  return true;
194
232
  });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * GeminiNativeProvider - Native Google Gen AI SDK implementation
3
+ *
4
+ * Uses the native @google/genai SDK instead of OpenAI-compatible endpoint.
5
+ * This enables proper support for Gemini 3 models with thought signatures.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const provider = new GeminiNativeProvider({
10
+ * apiKey: process.env.GOOGLE_AI_API_KEY,
11
+ * });
12
+ *
13
+ * const agent = new Agent({ provider });
14
+ * ```
15
+ */
16
+ import type { LLMProvider, Message, ChatOptions, StreamChunk } from './types.js';
17
+ /**
18
+ * Configuration for GeminiNativeProvider
19
+ */
20
+ export interface GeminiNativeProviderConfig {
21
+ /**
22
+ * Google AI API key
23
+ */
24
+ apiKey: string;
25
+ /**
26
+ * Default model to use
27
+ * @default 'gemini-2.5-flash'
28
+ */
29
+ model?: string;
30
+ /**
31
+ * Default max tokens
32
+ * @default 4096
33
+ */
34
+ maxTokens?: number;
35
+ }
36
+ /**
37
+ * GeminiNativeProvider implements LLMProvider using the native Google Gen AI SDK
38
+ *
39
+ * Benefits over OpenAI-compatible endpoint:
40
+ * - Automatic thought signature handling for Gemini 3
41
+ * - Native streaming support
42
+ * - Full access to Gemini-specific features
43
+ */
44
+ export declare class GeminiNativeProvider implements LLMProvider {
45
+ readonly name = "gemini";
46
+ private readonly client;
47
+ private readonly defaultModel;
48
+ private readonly defaultMaxTokens;
49
+ constructor(config: GeminiNativeProviderConfig);
50
+ /**
51
+ * Send messages and stream the response
52
+ */
53
+ chat(messages: Message[], options?: ChatOptions): AsyncIterable<StreamChunk>;
54
+ /**
55
+ * Count tokens in messages (approximation)
56
+ */
57
+ countTokens(messages: Message[]): Promise<number>;
58
+ /**
59
+ * Convert our Message format to Google's format
60
+ */
61
+ private convertMessages;
62
+ /**
63
+ * Convert content to Google's Part format
64
+ */
65
+ private convertContent;
66
+ /**
67
+ * Extract tool name from tool_use block that matches the given toolUseId
68
+ */
69
+ private extractToolName;
70
+ /**
71
+ * Convert our ToolDefinition to Google's FunctionDeclaration format
72
+ */
73
+ private convertTools;
74
+ /**
75
+ * Process a streaming chunk into StreamChunks
76
+ */
77
+ private processChunk;
78
+ /**
79
+ * Map errors to ProviderError
80
+ */
81
+ private mapError;
82
+ }
83
+ /**
84
+ * Create a GeminiNativeProvider with API key from environment
85
+ */
86
+ export declare function createGeminiNativeProvider(config?: Partial<GeminiNativeProviderConfig>): GeminiNativeProvider;
@@ -0,0 +1,339 @@
1
+ /**
2
+ * GeminiNativeProvider - Native Google Gen AI SDK implementation
3
+ *
4
+ * Uses the native @google/genai SDK instead of OpenAI-compatible endpoint.
5
+ * This enables proper support for Gemini 3 models with thought signatures.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const provider = new GeminiNativeProvider({
10
+ * apiKey: process.env.GOOGLE_AI_API_KEY,
11
+ * });
12
+ *
13
+ * const agent = new Agent({ provider });
14
+ * ```
15
+ */
16
+ import { GoogleGenAI } from '@google/genai';
17
+ import { ProviderError } from '../errors.js';
18
+ /**
19
+ * Default model for Gemini API
20
+ */
21
+ const DEFAULT_MODEL = 'gemini-2.5-flash';
22
+ /**
23
+ * Default max tokens
24
+ */
25
+ const DEFAULT_MAX_TOKENS = 4096;
26
+ /**
27
+ * GeminiNativeProvider implements LLMProvider using the native Google Gen AI SDK
28
+ *
29
+ * Benefits over OpenAI-compatible endpoint:
30
+ * - Automatic thought signature handling for Gemini 3
31
+ * - Native streaming support
32
+ * - Full access to Gemini-specific features
33
+ */
34
+ export class GeminiNativeProvider {
35
+ name = 'gemini';
36
+ client;
37
+ defaultModel;
38
+ defaultMaxTokens;
39
+ constructor(config) {
40
+ this.client = new GoogleGenAI({ apiKey: config.apiKey });
41
+ this.defaultModel = config.model ?? DEFAULT_MODEL;
42
+ this.defaultMaxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
43
+ }
44
+ /**
45
+ * Send messages and stream the response
46
+ */
47
+ async *chat(messages, options) {
48
+ const { systemInstruction, contents } = this.convertMessages(messages);
49
+ const tools = this.convertTools(options?.tools);
50
+ const model = options?.model ?? this.defaultModel;
51
+ try {
52
+ // Build config
53
+ const config = {
54
+ systemInstruction,
55
+ maxOutputTokens: options?.maxTokens ?? this.defaultMaxTokens,
56
+ temperature: options?.temperature,
57
+ stopSequences: options?.stopSequences,
58
+ };
59
+ // Add tools if any
60
+ if (tools.length > 0) {
61
+ config.tools = [{ functionDeclarations: tools }];
62
+ }
63
+ // Request streaming response
64
+ const streamResponse = await this.client.models.generateContentStream({
65
+ model,
66
+ contents,
67
+ config,
68
+ });
69
+ // Track state for tool calls
70
+ let currentToolId = '';
71
+ let currentToolName = '';
72
+ let toolInputJson = '';
73
+ let inThinkingBlock = false;
74
+ // Process stream chunks
75
+ for await (const chunk of streamResponse) {
76
+ const streamChunks = this.processChunk(chunk, currentToolId, currentToolName, toolInputJson, inThinkingBlock);
77
+ for (const streamChunk of streamChunks) {
78
+ // Update tracking state
79
+ if (streamChunk.type === 'tool_use_start' && streamChunk.toolUse) {
80
+ currentToolId = streamChunk.toolUse.id;
81
+ currentToolName = streamChunk.toolUse.name;
82
+ toolInputJson = '';
83
+ }
84
+ else if (streamChunk.type === 'tool_use_delta' && streamChunk.text) {
85
+ toolInputJson += streamChunk.text;
86
+ }
87
+ else if (streamChunk.type === 'tool_use_end') {
88
+ currentToolId = '';
89
+ currentToolName = '';
90
+ toolInputJson = '';
91
+ }
92
+ else if (streamChunk.type === 'thinking_start') {
93
+ inThinkingBlock = true;
94
+ }
95
+ else if (streamChunk.type === 'thinking_end') {
96
+ inThinkingBlock = false;
97
+ }
98
+ yield streamChunk;
99
+ }
100
+ // Check for usage in the final chunk
101
+ if (chunk.usageMetadata) {
102
+ yield {
103
+ type: 'done',
104
+ model,
105
+ usage: {
106
+ inputTokens: chunk.usageMetadata.promptTokenCount ?? 0,
107
+ outputTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
108
+ },
109
+ };
110
+ }
111
+ }
112
+ }
113
+ catch (error) {
114
+ throw this.mapError(error);
115
+ }
116
+ }
117
+ /**
118
+ * Count tokens in messages (approximation)
119
+ */
120
+ countTokens(messages) {
121
+ // Approximation: ~4 characters per token
122
+ let charCount = 0;
123
+ for (const msg of messages) {
124
+ if (typeof msg.content === 'string') {
125
+ charCount += msg.content.length;
126
+ }
127
+ else {
128
+ for (const block of msg.content) {
129
+ switch (block.type) {
130
+ case 'text':
131
+ charCount += block.text.length;
132
+ break;
133
+ case 'tool_use':
134
+ charCount += JSON.stringify(block.input).length;
135
+ break;
136
+ case 'tool_result':
137
+ charCount += block.content.length;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return Promise.resolve(Math.ceil(charCount / 4));
144
+ }
145
+ /**
146
+ * Convert our Message format to Google's format
147
+ */
148
+ convertMessages(messages) {
149
+ let systemInstruction;
150
+ const contents = [];
151
+ for (const msg of messages) {
152
+ if (msg.role === 'system') {
153
+ // Google uses systemInstruction in config, not a message
154
+ systemInstruction = typeof msg.content === 'string' ? msg.content : '';
155
+ }
156
+ else {
157
+ // user → user, assistant → model
158
+ const role = msg.role === 'assistant' ? 'model' : 'user';
159
+ const parts = this.convertContent(msg.content);
160
+ contents.push({ role, parts });
161
+ }
162
+ }
163
+ return { systemInstruction, contents };
164
+ }
165
+ /**
166
+ * Convert content to Google's Part format
167
+ */
168
+ convertContent(content) {
169
+ if (typeof content === 'string') {
170
+ return [{ text: content }];
171
+ }
172
+ return content.map((block) => {
173
+ switch (block.type) {
174
+ case 'text':
175
+ return { text: block.text };
176
+ case 'tool_use':
177
+ // Convert to functionCall with thought signature if present
178
+ // Gemini 3 requires thought signatures on function calls
179
+ return {
180
+ functionCall: {
181
+ id: block.id,
182
+ name: block.name,
183
+ args: block.input,
184
+ },
185
+ // Include thought signature if present (required for Gemini 3)
186
+ ...(block.signature ? { thoughtSignature: block.signature } : {}),
187
+ };
188
+ case 'tool_result':
189
+ // Convert to functionResponse
190
+ return {
191
+ functionResponse: {
192
+ id: block.toolUseId,
193
+ name: this.extractToolName(content, block.toolUseId),
194
+ response: { result: block.content },
195
+ },
196
+ };
197
+ case 'thinking':
198
+ // Pass through thinking with signature if present
199
+ return {
200
+ text: block.thinking,
201
+ thought: true,
202
+ thoughtSignature: block.signature,
203
+ };
204
+ default: {
205
+ // Exhaustive check
206
+ const _exhaustive = block;
207
+ throw new Error(`Unknown content block type: ${_exhaustive.type}`);
208
+ }
209
+ }
210
+ });
211
+ }
212
+ /**
213
+ * Extract tool name from tool_use block that matches the given toolUseId
214
+ */
215
+ extractToolName(content, toolUseId) {
216
+ for (const block of content) {
217
+ if (block.type === 'tool_use' && block.id === toolUseId) {
218
+ return block.name;
219
+ }
220
+ }
221
+ // Fallback if not found in same message - this shouldn't happen normally
222
+ return 'unknown_function';
223
+ }
224
+ /**
225
+ * Convert our ToolDefinition to Google's FunctionDeclaration format
226
+ */
227
+ convertTools(tools) {
228
+ if (!tools || tools.length === 0) {
229
+ return [];
230
+ }
231
+ return tools.map((tool) => ({
232
+ name: tool.name,
233
+ description: tool.description,
234
+ // Use parametersJsonSchema for raw JSON schema (more flexible than typed Schema)
235
+ parametersJsonSchema: {
236
+ type: 'object',
237
+ properties: tool.inputSchema.properties,
238
+ required: tool.inputSchema.required,
239
+ },
240
+ }));
241
+ }
242
+ /**
243
+ * Process a streaming chunk into StreamChunks
244
+ */
245
+ processChunk(chunk, _currentToolId, _currentToolName, _toolInputJson, inThinkingBlock) {
246
+ const chunks = [];
247
+ // Get the first candidate's content
248
+ const candidate = chunk.candidates?.[0];
249
+ if (!candidate?.content?.parts) {
250
+ return chunks;
251
+ }
252
+ for (const part of candidate.content.parts) {
253
+ // Text content
254
+ if (part.text !== undefined && !part.thought) {
255
+ chunks.push({
256
+ type: 'text',
257
+ text: part.text,
258
+ });
259
+ }
260
+ // Thinking content (Gemini 3)
261
+ if (part.thought && part.text !== undefined) {
262
+ if (!inThinkingBlock) {
263
+ chunks.push({ type: 'thinking_start' });
264
+ }
265
+ chunks.push({
266
+ type: 'thinking_delta',
267
+ text: part.text,
268
+ thinking: {
269
+ thinking: part.text,
270
+ signature: part.thoughtSignature,
271
+ },
272
+ });
273
+ }
274
+ // Function call
275
+ if (part.functionCall) {
276
+ const funcCall = part.functionCall;
277
+ const toolId = funcCall.id ?? `tool_${String(Date.now())}_${Math.random().toString(36).slice(2, 9)}`;
278
+ // Capture thought signature if present (Gemini 3 attaches it to function calls)
279
+ // Note: thoughtSignature is not in the SDK types yet but exists on the response
280
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
281
+ const signature = part.thoughtSignature;
282
+ chunks.push({
283
+ type: 'tool_use_start',
284
+ toolUse: {
285
+ id: toolId,
286
+ name: funcCall.name ?? 'unknown',
287
+ signature, // Pass through the thought signature
288
+ },
289
+ });
290
+ // Emit the arguments as delta
291
+ if (funcCall.args) {
292
+ chunks.push({
293
+ type: 'tool_use_delta',
294
+ text: JSON.stringify(funcCall.args),
295
+ });
296
+ }
297
+ chunks.push({ type: 'tool_use_end' });
298
+ }
299
+ }
300
+ // Check for finish reason to end thinking block
301
+ if (inThinkingBlock && candidate.finishReason) {
302
+ chunks.push({ type: 'thinking_end' });
303
+ }
304
+ return chunks;
305
+ }
306
+ /**
307
+ * Map errors to ProviderError
308
+ */
309
+ mapError(error) {
310
+ if (error instanceof Error) {
311
+ const message = error.message;
312
+ // Check for common error patterns
313
+ if (message.includes('API key')) {
314
+ return new ProviderError('Invalid Gemini API key. Check your GOOGLE_AI_API_KEY or GEMINI_API_KEY.', 'gemini', 401, error);
315
+ }
316
+ if (message.includes('rate limit') || message.includes('429')) {
317
+ return new ProviderError('Gemini rate limit exceeded. Please wait and try again.', 'gemini', 429, error);
318
+ }
319
+ if (message.includes('not found') || message.includes('404')) {
320
+ return new ProviderError(`Gemini model not found: ${message}`, 'gemini', 404, error);
321
+ }
322
+ return new ProviderError(message, 'gemini', undefined, error);
323
+ }
324
+ return new ProviderError(String(error), 'gemini');
325
+ }
326
+ }
327
+ /**
328
+ * Create a GeminiNativeProvider with API key from environment
329
+ */
330
+ export function createGeminiNativeProvider(config) {
331
+ const apiKey = config?.apiKey ?? process.env.GOOGLE_AI_API_KEY ?? process.env.GEMINI_API_KEY;
332
+ if (!apiKey) {
333
+ throw new ProviderError('Gemini API key not found. Set GOOGLE_AI_API_KEY or GEMINI_API_KEY environment variable or pass apiKey in config.', 'gemini');
334
+ }
335
+ return new GeminiNativeProvider({
336
+ ...config,
337
+ apiKey,
338
+ });
339
+ }
@@ -12,5 +12,10 @@ export { OpenAICompatibleProvider } from './openai-compatible.js';
12
12
  export type { OpenAICompatibleConfig, OpenAIMessage, OpenAIToolCall, OpenAITool, OpenAIStreamChunk, } from './openai-compatible.js';
13
13
  export { OpenAIProvider, createOpenAIProvider } from './openai.js';
14
14
  export type { OpenAIProviderConfig } from './openai.js';
15
- export { GeminiProvider, createGeminiProvider } from './gemini.js';
16
- export type { GeminiProviderConfig } from './gemini.js';
15
+ export { GeminiProvider as GeminiLegacyProvider, createGeminiProvider as createGeminiLegacyProvider, } from './gemini.js';
16
+ export type { GeminiProviderConfig as GeminiLegacyProviderConfig } from './gemini.js';
17
+ export { GeminiNativeProvider, createGeminiNativeProvider } from './gemini-native.js';
18
+ export type { GeminiNativeProviderConfig } from './gemini-native.js';
19
+ export { GeminiNativeProvider as GeminiProvider } from './gemini-native.js';
20
+ export { createGeminiNativeProvider as createGeminiProvider } from './gemini-native.js';
21
+ export type { GeminiNativeProviderConfig as GeminiProviderConfig } from './gemini-native.js';
@@ -11,5 +11,10 @@ export { OllamaProvider, createOllamaProvider } from './ollama.js';
11
11
  export { OpenAICompatibleProvider } from './openai-compatible.js';
12
12
  // OpenAI provider
13
13
  export { OpenAIProvider, createOpenAIProvider } from './openai.js';
14
- // Gemini provider
15
- export { GeminiProvider, createGeminiProvider } from './gemini.js';
14
+ // Gemini provider (legacy OpenAI-compatible endpoint)
15
+ export { GeminiProvider as GeminiLegacyProvider, createGeminiProvider as createGeminiLegacyProvider, } from './gemini.js';
16
+ // Gemini native provider (recommended - supports Gemini 3 thought signatures)
17
+ export { GeminiNativeProvider, createGeminiNativeProvider } from './gemini-native.js';
18
+ // Re-export native as default Gemini provider
19
+ export { GeminiNativeProvider as GeminiProvider } from './gemini-native.js';
20
+ export { createGeminiNativeProvider as createGeminiProvider } from './gemini-native.js';
@@ -7,14 +7,14 @@
7
7
  * @example
8
8
  * ```typescript
9
9
  * const provider = createOllamaProvider({
10
- * model: 'llama3.1',
10
+ * model: 'llama3.2',
11
11
  * baseUrl: 'http://localhost:11434'
12
12
  * });
13
13
  * ```
14
14
  *
15
15
  * @remarks
16
16
  * - Requires Ollama to be running locally
17
- * - Default model is llama3.1 (supports tool calling)
17
+ * - Default model is llama3.2 (supports tool calling)
18
18
  * - Extended thinking is not supported (Claude-specific feature)
19
19
  */
20
20
  import type { ChatOptions } from './types.js';
@@ -7,20 +7,20 @@
7
7
  * @example
8
8
  * ```typescript
9
9
  * const provider = createOllamaProvider({
10
- * model: 'llama3.1',
10
+ * model: 'llama3.2',
11
11
  * baseUrl: 'http://localhost:11434'
12
12
  * });
13
13
  * ```
14
14
  *
15
15
  * @remarks
16
16
  * - Requires Ollama to be running locally
17
- * - Default model is llama3.1 (supports tool calling)
17
+ * - Default model is llama3.2 (supports tool calling)
18
18
  * - Extended thinking is not supported (Claude-specific feature)
19
19
  */
20
20
  import { ProviderError } from '../errors.js';
21
21
  import { OpenAICompatibleProvider } from './openai-compatible.js';
22
22
  // Default configuration
23
- const DEFAULT_MODEL = 'llama3.1';
23
+ const DEFAULT_MODEL = 'llama3.2';
24
24
  const DEFAULT_BASE_URL = 'http://localhost:11434';
25
25
  /**
26
26
  * Ollama LLM Provider
@@ -24,6 +24,12 @@ export interface ToolUseBlock {
24
24
  id: string;
25
25
  name: string;
26
26
  input: Record<string, unknown>;
27
+ /**
28
+ * Thought signature for Gemini 3 function calls.
29
+ * Required for Gemini 3 to maintain reasoning context.
30
+ * @see https://ai.google.dev/gemini-api/docs/thought-signatures
31
+ */
32
+ signature?: string;
27
33
  }
28
34
  /**
29
35
  * Tool result content block (result of a tool call)
@@ -75,6 +81,11 @@ export interface StreamChunk {
75
81
  id: string;
76
82
  name: string;
77
83
  input?: Record<string, unknown>;
84
+ /**
85
+ * Thought signature for Gemini 3 function calls.
86
+ * Only present on first function call in each step.
87
+ */
88
+ signature?: string;
78
89
  };
79
90
  /**
80
91
  * Thinking block data (for thinking_start/thinking_end)