@compilr-dev/agents 0.3.2 → 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/dist/agent.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Agent - The main class for running AI agents with tool use
3
3
  */
4
4
  import type { LLMProvider, Message, ChatOptions, StreamChunk } from './providers/types.js';
5
- import type { Tool, ToolDefinition, ToolRegistry, ToolExecutionResult } from './tools/types.js';
5
+ import type { Tool, ToolDefinition, ToolRegistry, ToolExecutionResult, ToolExecutionContext } from './tools/types.js';
6
6
  import type { ContextStats, VerbosityLevel, SmartCompactionResult } from './context/types.js';
7
7
  import type { AgentState, Checkpointer, SessionMetadata } from './state/types.js';
8
8
  import type { Anchor, AnchorInput, AnchorQueryOptions, AnchorClearOptions, AnchorManagerOptions } from './anchors/types.js';
@@ -589,6 +589,29 @@ export interface RunOptions {
589
589
  * ```
590
590
  */
591
591
  toolFilter?: string[];
592
+ /**
593
+ * Callback to provide additional tool execution context.
594
+ * Called before each tool execution, allowing the caller to inject
595
+ * abort signals or other context-specific options.
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * // Provide abort signal for bash commands (for Ctrl+B backgrounding)
600
+ * const bashAbortController = new AbortController();
601
+ * await agent.stream(message, {
602
+ * getToolContext: (toolName, toolUseId) => {
603
+ * if (toolName === 'bash') {
604
+ * return {
605
+ * abortSignal: bashAbortController.signal,
606
+ * onBackground: (shellId, output) => { ... },
607
+ * };
608
+ * }
609
+ * return {};
610
+ * },
611
+ * });
612
+ * ```
613
+ */
614
+ getToolContext?: (toolName: string, toolUseId: string) => Partial<Omit<ToolExecutionContext, 'toolUseId' | 'onOutput'>>;
592
615
  }
593
616
  /**
594
617
  * Agent run result
@@ -1505,6 +1528,10 @@ export declare class Agent {
1505
1528
  * Get all registered tool definitions
1506
1529
  */
1507
1530
  getToolDefinitions(): ToolDefinition[];
1531
+ /**
1532
+ * Check if a tool is marked as silent (no spinner or result output)
1533
+ */
1534
+ isToolSilent(name: string): boolean;
1508
1535
  /**
1509
1536
  * Run the agent with a user message
1510
1537
  */
package/dist/agent.js CHANGED
@@ -1422,6 +1422,13 @@ export class Agent {
1422
1422
  getToolDefinitions() {
1423
1423
  return this.toolRegistry.getDefinitions();
1424
1424
  }
1425
+ /**
1426
+ * Check if a tool is marked as silent (no spinner or result output)
1427
+ */
1428
+ isToolSilent(name) {
1429
+ const tool = this.toolRegistry.get(name);
1430
+ return tool?.silent === true;
1431
+ }
1425
1432
  /**
1426
1433
  * Run the agent with a user message
1427
1434
  */
@@ -1429,6 +1436,7 @@ export class Agent {
1429
1436
  let maxIterations = options?.maxIterations ?? this.maxIterations;
1430
1437
  const chatOptions = { ...this.chatOptions, ...options?.chatOptions };
1431
1438
  const signal = options?.signal;
1439
+ const getToolContext = options?.getToolContext;
1432
1440
  // Combined event emitter
1433
1441
  const emit = (event) => {
1434
1442
  this.onEvent?.(event);
@@ -1817,6 +1825,8 @@ export class Agent {
1817
1825
  }
1818
1826
  const toolStartTime = Date.now();
1819
1827
  try {
1828
+ // Get additional context from caller (e.g., for bash backgrounding)
1829
+ const additionalContext = getToolContext?.(toolUse.name, toolUse.id) ?? {};
1820
1830
  const toolContext = {
1821
1831
  toolUseId: toolUse.id,
1822
1832
  onOutput: (output, stream) => {
@@ -1828,6 +1838,8 @@ export class Agent {
1828
1838
  stream,
1829
1839
  });
1830
1840
  },
1841
+ // Merge in additional context (abortSignal, onBackground, etc.)
1842
+ ...additionalContext,
1831
1843
  };
1832
1844
  result = await this.toolRegistry.execute(toolUse.name, toolInput, toolContext);
1833
1845
  }
package/dist/index.d.ts CHANGED
@@ -21,6 +21,16 @@ export { GeminiLegacyProvider, createGeminiLegacyProvider } from './providers/in
21
21
  export type { GeminiLegacyProviderConfig } from './providers/index.js';
22
22
  export { OpenAICompatibleProvider } from './providers/index.js';
23
23
  export type { OpenAICompatibleConfig } from './providers/index.js';
24
+ export { TogetherProvider, createTogetherProvider } from './providers/index.js';
25
+ export type { TogetherProviderConfig } from './providers/index.js';
26
+ export { GroqProvider, createGroqProvider } from './providers/index.js';
27
+ export type { GroqProviderConfig } from './providers/index.js';
28
+ export { FireworksProvider, createFireworksProvider } from './providers/index.js';
29
+ export type { FireworksProviderConfig } from './providers/index.js';
30
+ export { PerplexityProvider, createPerplexityProvider } from './providers/index.js';
31
+ export type { PerplexityProviderConfig } from './providers/index.js';
32
+ export { OpenRouterProvider, createOpenRouterProvider } from './providers/index.js';
33
+ export type { OpenRouterProviderConfig } from './providers/index.js';
24
34
  export type { Tool, ToolHandler, ToolRegistry, ToolInputSchema, ToolExecutionResult, ToolRegistryOptions, DefineToolOptions, ReadFileInput, WriteFileInput, BashInput, BashResult, FifoDetectionResult, GrepInput, GlobInput, EditInput, TodoWriteInput, TodoReadInput, TodoItem, TodoStatus, TodoContextCleanupOptions, TaskInput, TaskResult, AgentTypeConfig, TaskToolOptions, ContextMode, ThoroughnessLevel, SubAgentEventInfo, SuggestInput, SuggestToolOptions, } from './tools/index.js';
25
35
  export { defineTool, createSuccessResult, createErrorResult, wrapToolExecute, DefaultToolRegistry, createToolRegistry, } from './tools/index.js';
26
36
  export { readFileTool, createReadFileTool, writeFileTool, createWriteFileTool, bashTool, createBashTool, execStream, detectFifoUsage, bashOutputTool, createBashOutputTool, killShellTool, createKillShellTool, ShellManager, getDefaultShellManager, setDefaultShellManager, grepTool, createGrepTool, globTool, createGlobTool, editTool, createEditTool, todoWriteTool, todoReadTool, createTodoTools, TodoStore, resetDefaultTodoStore, getDefaultTodoStore, createIsolatedTodoStore, cleanupTodoContextMessages, getTodoContextStats, webFetchTool, createWebFetchTool, createTaskTool, defaultAgentTypes, suggestTool, createSuggestTool, builtinTools, allBuiltinTools, TOOL_NAMES, TOOL_SETS, } from './tools/index.js';
package/dist/index.js CHANGED
@@ -16,6 +16,12 @@ export { GeminiNativeProvider, createGeminiNativeProvider } from './providers/in
16
16
  // Legacy Gemini provider (OpenAI-compatible endpoint)
17
17
  export { GeminiLegacyProvider, createGeminiLegacyProvider } from './providers/index.js';
18
18
  export { OpenAICompatibleProvider } from './providers/index.js';
19
+ // "Others" providers - OpenAI-compatible cloud APIs
20
+ export { TogetherProvider, createTogetherProvider } from './providers/index.js';
21
+ export { GroqProvider, createGroqProvider } from './providers/index.js';
22
+ export { FireworksProvider, createFireworksProvider } from './providers/index.js';
23
+ export { PerplexityProvider, createPerplexityProvider } from './providers/index.js';
24
+ export { OpenRouterProvider, createOpenRouterProvider } from './providers/index.js';
19
25
  // Tool utilities
20
26
  export { defineTool, createSuccessResult, createErrorResult, wrapToolExecute, DefaultToolRegistry, createToolRegistry, } from './tools/index.js';
21
27
  // Built-in tools
@@ -33,6 +33,18 @@ export interface ClaudeProviderConfig {
33
33
  * @default 4096
34
34
  */
35
35
  maxTokens?: number;
36
+ /**
37
+ * Enable prompt caching for system prompt and tools.
38
+ *
39
+ * When enabled, the system prompt and tool definitions are cached
40
+ * server-side, reducing token costs by up to 90% on subsequent requests.
41
+ *
42
+ * - Cache write: 1.25x base input cost (first request)
43
+ * - Cache read: 0.1x base input cost (subsequent requests within 5 min)
44
+ *
45
+ * @default true
46
+ */
47
+ enablePromptCaching?: boolean;
36
48
  }
37
49
  /**
38
50
  * ClaudeProvider implements LLMProvider for Anthropic's Claude API
@@ -42,6 +54,7 @@ export declare class ClaudeProvider implements LLMProvider {
42
54
  private readonly client;
43
55
  private readonly defaultModel;
44
56
  private readonly defaultMaxTokens;
57
+ private readonly enablePromptCaching;
45
58
  constructor(config: ClaudeProviderConfig);
46
59
  /**
47
60
  * Send messages and stream the response
@@ -71,6 +84,20 @@ export declare class ClaudeProvider implements LLMProvider {
71
84
  * Convert thinking config to Anthropic API format
72
85
  */
73
86
  private convertThinking;
87
+ /**
88
+ * Wrap system prompt in array format with cache_control for prompt caching.
89
+ *
90
+ * When enabled, the system prompt is cached server-side for 5 minutes,
91
+ * reducing token costs by up to 90% on subsequent requests.
92
+ */
93
+ private wrapSystemPromptWithCache;
94
+ /**
95
+ * Add cache_control to the last tool definition.
96
+ *
97
+ * This caches ALL tool definitions as a single prefix (tools are
98
+ * cached cumulatively up to the cache_control marker).
99
+ */
100
+ private addCacheControlToLastTool;
74
101
  /**
75
102
  * Process a stream event into StreamChunks
76
103
  */
@@ -28,6 +28,7 @@ export class ClaudeProvider {
28
28
  client;
29
29
  defaultModel;
30
30
  defaultMaxTokens;
31
+ enablePromptCaching;
31
32
  constructor(config) {
32
33
  this.client = new Anthropic({
33
34
  apiKey: config.apiKey,
@@ -35,6 +36,7 @@ export class ClaudeProvider {
35
36
  });
36
37
  this.defaultModel = config.model ?? DEFAULT_MODEL;
37
38
  this.defaultMaxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
39
+ this.enablePromptCaching = config.enablePromptCaching ?? true;
38
40
  }
39
41
  /**
40
42
  * Send messages and stream the response
@@ -43,14 +45,26 @@ export class ClaudeProvider {
43
45
  const { systemPrompt, anthropicMessages } = this.convertMessages(messages);
44
46
  const tools = this.convertTools(options?.tools);
45
47
  const thinking = this.convertThinking(options?.thinking);
48
+ // Calculate payload sizes for debugging (same as gemini-native.ts)
49
+ const debugPayload = {
50
+ systemChars: systemPrompt.length,
51
+ contentsChars: JSON.stringify(anthropicMessages).length,
52
+ toolsChars: JSON.stringify(tools).length,
53
+ };
46
54
  try {
55
+ // Determine if prompt caching is enabled
56
+ const shouldCache = options?.enablePromptCaching ?? this.enablePromptCaching;
47
57
  // Build request parameters
48
58
  const params = {
49
59
  model: options?.model ?? this.defaultModel,
50
60
  max_tokens: options?.maxTokens ?? this.defaultMaxTokens,
51
- system: systemPrompt,
61
+ system: shouldCache && systemPrompt
62
+ ? this.wrapSystemPromptWithCache(systemPrompt)
63
+ : systemPrompt,
52
64
  messages: anthropicMessages,
53
- tools: tools.length > 0 ? tools : undefined,
65
+ tools: tools.length > 0
66
+ ? (shouldCache ? this.addCacheControlToLastTool(tools) : tools)
67
+ : undefined,
54
68
  temperature: options?.temperature,
55
69
  stop_sequences: options?.stopSequences,
56
70
  };
@@ -99,6 +113,7 @@ export class ClaudeProvider {
99
113
  outputTokens: usage.output_tokens,
100
114
  cacheReadTokens: usageWithCache.cache_read_input_tokens,
101
115
  cacheCreationTokens: usageWithCache.cache_creation_input_tokens,
116
+ debugPayload,
102
117
  },
103
118
  };
104
119
  }
@@ -229,6 +244,40 @@ export class ClaudeProvider {
229
244
  budget_tokens: thinking.budgetTokens,
230
245
  };
231
246
  }
247
+ /**
248
+ * Wrap system prompt in array format with cache_control for prompt caching.
249
+ *
250
+ * When enabled, the system prompt is cached server-side for 5 minutes,
251
+ * reducing token costs by up to 90% on subsequent requests.
252
+ */
253
+ wrapSystemPromptWithCache(systemPrompt) {
254
+ return [
255
+ {
256
+ type: 'text',
257
+ text: systemPrompt,
258
+ cache_control: { type: 'ephemeral' },
259
+ },
260
+ ];
261
+ }
262
+ /**
263
+ * Add cache_control to the last tool definition.
264
+ *
265
+ * This caches ALL tool definitions as a single prefix (tools are
266
+ * cached cumulatively up to the cache_control marker).
267
+ */
268
+ addCacheControlToLastTool(tools) {
269
+ if (tools.length === 0)
270
+ return tools;
271
+ return tools.map((tool, index) => {
272
+ if (index === tools.length - 1) {
273
+ return {
274
+ ...tool,
275
+ cache_control: { type: 'ephemeral' },
276
+ };
277
+ }
278
+ return tool;
279
+ });
280
+ }
232
281
  /**
233
282
  * Process a stream event into StreamChunks
234
283
  */
@@ -316,7 +365,8 @@ export class ClaudeProvider {
316
365
  */
317
366
  mapError(error) {
318
367
  if (error instanceof Anthropic.APIError) {
319
- return new ProviderError(error.message, 'claude', error.status, error);
368
+ const status = typeof error.status === 'number' ? error.status : undefined;
369
+ return new ProviderError(error.message, 'claude', status, error);
320
370
  }
321
371
  if (error instanceof Anthropic.APIConnectionError) {
322
372
  return new ProviderError(`Connection error: ${error.message}`, 'claude', undefined, error);
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Fireworks AI LLM Provider
3
+ *
4
+ * Implements LLMProvider interface for Fireworks AI models.
5
+ * Extends OpenAICompatibleProvider for shared functionality.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const provider = createFireworksProvider({
10
+ * model: 'accounts/fireworks/models/llama-v3p1-70b-instruct',
11
+ * apiKey: process.env.FIREWORKS_API_KEY
12
+ * });
13
+ * ```
14
+ *
15
+ * @remarks
16
+ * - Requires valid Fireworks AI API key
17
+ * - Default model is accounts/fireworks/models/llama-v3p1-8b-instruct
18
+ * - Supports Llama, Mixtral, and fine-tuned models
19
+ */
20
+ import type { ChatOptions } from './types.js';
21
+ import { ProviderError } from '../errors.js';
22
+ import { OpenAICompatibleProvider } from './openai-compatible.js';
23
+ /**
24
+ * Configuration for FireworksProvider
25
+ */
26
+ export interface FireworksProviderConfig {
27
+ /** Fireworks AI API key (falls back to FIREWORKS_API_KEY env var) */
28
+ apiKey?: string;
29
+ /** Base URL for Fireworks API (default: https://api.fireworks.ai/inference) */
30
+ baseUrl?: string;
31
+ /** Default model to use (default: accounts/fireworks/models/llama-v3p1-8b-instruct) */
32
+ model?: string;
33
+ /** Default max tokens (default: 4096) */
34
+ maxTokens?: number;
35
+ /** Request timeout in milliseconds (default: 120000) */
36
+ timeout?: number;
37
+ }
38
+ /**
39
+ * Fireworks AI LLM Provider
40
+ *
41
+ * Provides streaming chat completion using Fireworks AI.
42
+ * Supports Llama, Mixtral, and custom fine-tuned models.
43
+ */
44
+ export declare class FireworksProvider extends OpenAICompatibleProvider {
45
+ readonly name = "fireworks";
46
+ private readonly apiKey;
47
+ constructor(config?: FireworksProviderConfig);
48
+ /**
49
+ * Fireworks AI authentication with Bearer token
50
+ */
51
+ protected getAuthHeaders(): Record<string, string>;
52
+ /**
53
+ * Fireworks AI chat completions endpoint (OpenAI-compatible)
54
+ */
55
+ protected getEndpointPath(): string;
56
+ /**
57
+ * Fireworks AI uses standard OpenAI body format
58
+ */
59
+ protected buildProviderSpecificBody(_options?: ChatOptions): Record<string, unknown>;
60
+ /**
61
+ * Map HTTP errors with Fireworks AI-specific messages
62
+ */
63
+ protected mapHttpError(status: number, body: string, _model: string): ProviderError;
64
+ /**
65
+ * Map connection errors with Fireworks AI-specific messages
66
+ */
67
+ protected mapConnectionError(_error: Error): ProviderError;
68
+ }
69
+ /**
70
+ * Create a Fireworks AI provider instance
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * // Using environment variable (FIREWORKS_API_KEY)
75
+ * const provider = createFireworksProvider();
76
+ *
77
+ * // With explicit API key
78
+ * const provider = createFireworksProvider({ apiKey: 'fw_...' });
79
+ *
80
+ * // With custom model
81
+ * const provider = createFireworksProvider({
82
+ * model: 'accounts/fireworks/models/llama-v3p1-70b-instruct'
83
+ * });
84
+ * ```
85
+ */
86
+ export declare function createFireworksProvider(config?: FireworksProviderConfig): FireworksProvider;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Fireworks AI LLM Provider
3
+ *
4
+ * Implements LLMProvider interface for Fireworks AI models.
5
+ * Extends OpenAICompatibleProvider for shared functionality.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const provider = createFireworksProvider({
10
+ * model: 'accounts/fireworks/models/llama-v3p1-70b-instruct',
11
+ * apiKey: process.env.FIREWORKS_API_KEY
12
+ * });
13
+ * ```
14
+ *
15
+ * @remarks
16
+ * - Requires valid Fireworks AI API key
17
+ * - Default model is accounts/fireworks/models/llama-v3p1-8b-instruct
18
+ * - Supports Llama, Mixtral, and fine-tuned models
19
+ */
20
+ import { ProviderError } from '../errors.js';
21
+ import { OpenAICompatibleProvider } from './openai-compatible.js';
22
+ // Default configuration
23
+ const DEFAULT_MODEL = 'accounts/fireworks/models/llama-v3p1-8b-instruct';
24
+ const DEFAULT_BASE_URL = 'https://api.fireworks.ai/inference';
25
+ /**
26
+ * Fireworks AI LLM Provider
27
+ *
28
+ * Provides streaming chat completion using Fireworks AI.
29
+ * Supports Llama, Mixtral, and custom fine-tuned models.
30
+ */
31
+ export class FireworksProvider extends OpenAICompatibleProvider {
32
+ name = 'fireworks';
33
+ apiKey;
34
+ constructor(config = {}) {
35
+ const apiKey = config.apiKey ?? process.env.FIREWORKS_API_KEY;
36
+ if (!apiKey) {
37
+ throw new ProviderError('Fireworks AI API key not found. Set FIREWORKS_API_KEY environment variable or pass apiKey in config.', 'fireworks');
38
+ }
39
+ const baseConfig = {
40
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
41
+ model: config.model ?? DEFAULT_MODEL,
42
+ maxTokens: config.maxTokens,
43
+ timeout: config.timeout,
44
+ };
45
+ super(baseConfig);
46
+ this.apiKey = apiKey;
47
+ }
48
+ /**
49
+ * Fireworks AI authentication with Bearer token
50
+ */
51
+ getAuthHeaders() {
52
+ return {
53
+ Authorization: `Bearer ${this.apiKey}`,
54
+ };
55
+ }
56
+ /**
57
+ * Fireworks AI chat completions endpoint (OpenAI-compatible)
58
+ */
59
+ getEndpointPath() {
60
+ return '/v1/chat/completions';
61
+ }
62
+ /**
63
+ * Fireworks AI uses standard OpenAI body format
64
+ */
65
+ buildProviderSpecificBody(_options) {
66
+ return {};
67
+ }
68
+ /**
69
+ * Map HTTP errors with Fireworks AI-specific messages
70
+ */
71
+ mapHttpError(status, body, _model) {
72
+ let message = `Fireworks AI error (${String(status)})`;
73
+ try {
74
+ const parsed = JSON.parse(body);
75
+ if (parsed.error?.message) {
76
+ message = parsed.error.message;
77
+ }
78
+ }
79
+ catch {
80
+ message = body || message;
81
+ }
82
+ switch (status) {
83
+ case 401:
84
+ return new ProviderError('Invalid Fireworks AI API key. Check your FIREWORKS_API_KEY.', 'fireworks', 401);
85
+ case 403:
86
+ return new ProviderError('Access denied. Check your Fireworks AI API key permissions.', 'fireworks', 403);
87
+ case 429:
88
+ return new ProviderError('Fireworks AI rate limit exceeded. Please wait and try again.', 'fireworks', 429);
89
+ case 500:
90
+ case 502:
91
+ case 503:
92
+ return new ProviderError('Fireworks AI service temporarily unavailable. Please try again later.', 'fireworks', status);
93
+ default:
94
+ return new ProviderError(message, 'fireworks', status);
95
+ }
96
+ }
97
+ /**
98
+ * Map connection errors with Fireworks AI-specific messages
99
+ */
100
+ mapConnectionError(_error) {
101
+ return new ProviderError('Failed to connect to Fireworks AI API. Check your internet connection.', 'fireworks');
102
+ }
103
+ }
104
+ /**
105
+ * Create a Fireworks AI provider instance
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * // Using environment variable (FIREWORKS_API_KEY)
110
+ * const provider = createFireworksProvider();
111
+ *
112
+ * // With explicit API key
113
+ * const provider = createFireworksProvider({ apiKey: 'fw_...' });
114
+ *
115
+ * // With custom model
116
+ * const provider = createFireworksProvider({
117
+ * model: 'accounts/fireworks/models/llama-v3p1-70b-instruct'
118
+ * });
119
+ * ```
120
+ */
121
+ export function createFireworksProvider(config = {}) {
122
+ return new FireworksProvider(config);
123
+ }
@@ -48,6 +48,12 @@ export class GeminiNativeProvider {
48
48
  const { systemInstruction, contents } = this.convertMessages(messages);
49
49
  const tools = this.convertTools(options?.tools);
50
50
  const model = options?.model ?? this.defaultModel;
51
+ // Calculate payload sizes for debugging
52
+ const debugPayload = {
53
+ systemChars: systemInstruction?.length ?? 0,
54
+ contentsChars: JSON.stringify(contents).length,
55
+ toolsChars: JSON.stringify(tools).length,
56
+ };
51
57
  try {
52
58
  // Build config
53
59
  const config = {
@@ -71,6 +77,8 @@ export class GeminiNativeProvider {
71
77
  let currentToolName = '';
72
78
  let toolInputJson = '';
73
79
  let inThinkingBlock = false;
80
+ // Track the last usage metadata (Gemini sends it in EVERY chunk, not just the final one)
81
+ let lastUsageMetadata;
74
82
  // Process stream chunks
75
83
  for await (const chunk of streamResponse) {
76
84
  const streamChunks = this.processChunk(chunk, currentToolId, currentToolName, toolInputJson, inThinkingBlock);
@@ -97,18 +105,32 @@ export class GeminiNativeProvider {
97
105
  }
98
106
  yield streamChunk;
99
107
  }
100
- // Check for usage in the final chunk
108
+ // Track usage metadata (update on every chunk, yield only once at end)
101
109
  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
+ lastUsageMetadata = chunk.usageMetadata;
110
111
  }
111
112
  }
113
+ // Yield 'done' only ONCE after stream completes with the final usage
114
+ if (lastUsageMetadata) {
115
+ yield {
116
+ type: 'done',
117
+ model,
118
+ usage: {
119
+ inputTokens: lastUsageMetadata.promptTokenCount ?? 0,
120
+ outputTokens: lastUsageMetadata.candidatesTokenCount ?? 0,
121
+ // Gemini 2.5+ models report thinking tokens separately
122
+ ...(lastUsageMetadata.thoughtsTokenCount
123
+ ? { thinkingTokens: lastUsageMetadata.thoughtsTokenCount }
124
+ : {}),
125
+ // Gemini reports cached content tokens
126
+ ...(lastUsageMetadata.cachedContentTokenCount
127
+ ? { cacheReadTokens: lastUsageMetadata.cachedContentTokenCount }
128
+ : {}),
129
+ // Debug payload info
130
+ debugPayload,
131
+ },
132
+ };
133
+ }
112
134
  }
113
135
  catch (error) {
114
136
  throw this.mapError(error);
@@ -169,45 +191,58 @@ export class GeminiNativeProvider {
169
191
  if (typeof content === 'string') {
170
192
  return [{ text: content }];
171
193
  }
172
- return content.map((block) => {
194
+ const parts = [];
195
+ for (const block of content) {
173
196
  switch (block.type) {
174
197
  case 'text':
175
- return { text: block.text };
198
+ parts.push({ text: block.text });
199
+ break;
176
200
  case 'tool_use':
177
201
  // Convert to functionCall with thought signature if present
178
202
  // Gemini 3 requires thought signatures on function calls
179
- return {
203
+ // Note: No 'id' field - Gemini uses 'name' for matching
204
+ parts.push({
180
205
  functionCall: {
181
- id: block.id,
182
206
  name: block.name,
183
207
  args: block.input,
184
208
  },
185
209
  // Include thought signature if present (required for Gemini 3)
186
210
  ...(block.signature ? { thoughtSignature: block.signature } : {}),
187
- };
188
- case 'tool_result':
211
+ });
212
+ break;
213
+ case 'tool_result': {
189
214
  // Convert to functionResponse
190
- return {
215
+ // block.content is a JSON string from the agent - parse it for the API
216
+ let responseData;
217
+ try {
218
+ responseData = JSON.parse(block.content);
219
+ }
220
+ catch {
221
+ // If not valid JSON, wrap the string in a result object
222
+ responseData = { result: block.content };
223
+ }
224
+ parts.push({
191
225
  functionResponse: {
192
- id: block.toolUseId,
226
+ // Note: No 'id' field - Gemini matches by 'name' only
193
227
  name: this.extractToolName(content, block.toolUseId),
194
- response: { result: block.content },
228
+ response: responseData,
195
229
  },
196
- };
230
+ });
231
+ break;
232
+ }
197
233
  case 'thinking':
198
- // Pass through thinking with signature if present
199
- return {
200
- text: block.thinking,
201
- thought: true,
202
- thoughtSignature: block.signature,
203
- };
234
+ // Thinking blocks should NOT be sent back to Gemini.
235
+ // They are internal model reasoning. Only the signature on function calls matters.
236
+ // Skip - do not add to parts.
237
+ break;
204
238
  default: {
205
239
  // Exhaustive check
206
240
  const _exhaustive = block;
207
241
  throw new Error(`Unknown content block type: ${_exhaustive.type}`);
208
242
  }
209
243
  }
210
- });
244
+ }
245
+ return parts;
211
246
  }
212
247
  /**
213
248
  * Extract tool name from tool_use block that matches the given toolUseId