@falai/agent 1.1.3 → 1.2.1

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.
Files changed (193) hide show
  1. package/README.md +9 -0
  2. package/dist/cjs/core/Agent.d.ts +17 -1
  3. package/dist/cjs/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/core/Agent.js +47 -0
  5. package/dist/cjs/core/Agent.js.map +1 -1
  6. package/dist/cjs/core/BatchPromptBuilder.d.ts +3 -0
  7. package/dist/cjs/core/BatchPromptBuilder.d.ts.map +1 -1
  8. package/dist/cjs/core/BatchPromptBuilder.js +4 -1
  9. package/dist/cjs/core/BatchPromptBuilder.js.map +1 -1
  10. package/dist/cjs/core/CompactionEngine.d.ts +65 -0
  11. package/dist/cjs/core/CompactionEngine.d.ts.map +1 -0
  12. package/dist/cjs/core/CompactionEngine.js +251 -0
  13. package/dist/cjs/core/CompactionEngine.js.map +1 -0
  14. package/dist/cjs/core/PromptComposer.d.ts +8 -1
  15. package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
  16. package/dist/cjs/core/PromptComposer.js +238 -126
  17. package/dist/cjs/core/PromptComposer.js.map +1 -1
  18. package/dist/cjs/core/PromptSectionCache.d.ts +57 -0
  19. package/dist/cjs/core/PromptSectionCache.d.ts.map +1 -0
  20. package/dist/cjs/core/PromptSectionCache.js +108 -0
  21. package/dist/cjs/core/PromptSectionCache.js.map +1 -0
  22. package/dist/cjs/core/ResponseEngine.d.ts +3 -2
  23. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  24. package/dist/cjs/core/ResponseEngine.js +8 -8
  25. package/dist/cjs/core/ResponseEngine.js.map +1 -1
  26. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  27. package/dist/cjs/core/ResponseModal.js +120 -70
  28. package/dist/cjs/core/ResponseModal.js.map +1 -1
  29. package/dist/cjs/core/ResponsePipeline.d.ts +2 -1
  30. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  31. package/dist/cjs/core/ResponsePipeline.js +17 -19
  32. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  33. package/dist/cjs/core/RoutingEngine.d.ts +10 -0
  34. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
  35. package/dist/cjs/core/RoutingEngine.js +5 -4
  36. package/dist/cjs/core/RoutingEngine.js.map +1 -1
  37. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  38. package/dist/cjs/core/SessionManager.js +20 -0
  39. package/dist/cjs/core/SessionManager.js.map +1 -1
  40. package/dist/cjs/core/StreamingToolExecutor.d.ts +142 -0
  41. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -0
  42. package/dist/cjs/core/StreamingToolExecutor.js +455 -0
  43. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -0
  44. package/dist/cjs/core/ToolManager.d.ts +18 -1
  45. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  46. package/dist/cjs/core/ToolManager.js +91 -0
  47. package/dist/cjs/core/ToolManager.js.map +1 -1
  48. package/dist/cjs/index.d.ts +5 -1
  49. package/dist/cjs/index.d.ts.map +1 -1
  50. package/dist/cjs/index.js +8 -2
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/providers/AnthropicProvider.d.ts +7 -0
  53. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  54. package/dist/cjs/providers/AnthropicProvider.js +109 -19
  55. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  56. package/dist/cjs/providers/GeminiProvider.d.ts +32 -0
  57. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  58. package/dist/cjs/providers/GeminiProvider.js +160 -53
  59. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  60. package/dist/cjs/providers/OpenAIProvider.d.ts +5 -0
  61. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  62. package/dist/cjs/providers/OpenAIProvider.js +65 -18
  63. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  64. package/dist/cjs/providers/OpenRouterProvider.d.ts +5 -0
  65. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  66. package/dist/cjs/providers/OpenRouterProvider.js +57 -18
  67. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  68. package/dist/cjs/types/agent.d.ts +44 -0
  69. package/dist/cjs/types/agent.d.ts.map +1 -1
  70. package/dist/cjs/types/agent.js.map +1 -1
  71. package/dist/cjs/types/ai.d.ts +2 -2
  72. package/dist/cjs/types/ai.d.ts.map +1 -1
  73. package/dist/cjs/types/compaction.d.ts +50 -0
  74. package/dist/cjs/types/compaction.d.ts.map +1 -0
  75. package/dist/cjs/types/compaction.js +6 -0
  76. package/dist/cjs/types/compaction.js.map +1 -0
  77. package/dist/cjs/types/index.d.ts +4 -2
  78. package/dist/cjs/types/index.d.ts.map +1 -1
  79. package/dist/cjs/types/index.js.map +1 -1
  80. package/dist/cjs/types/tool.d.ts +84 -0
  81. package/dist/cjs/types/tool.d.ts.map +1 -1
  82. package/dist/core/Agent.d.ts +17 -1
  83. package/dist/core/Agent.d.ts.map +1 -1
  84. package/dist/core/Agent.js +47 -0
  85. package/dist/core/Agent.js.map +1 -1
  86. package/dist/core/BatchPromptBuilder.d.ts +3 -0
  87. package/dist/core/BatchPromptBuilder.d.ts.map +1 -1
  88. package/dist/core/BatchPromptBuilder.js +4 -1
  89. package/dist/core/BatchPromptBuilder.js.map +1 -1
  90. package/dist/core/CompactionEngine.d.ts +65 -0
  91. package/dist/core/CompactionEngine.d.ts.map +1 -0
  92. package/dist/core/CompactionEngine.js +244 -0
  93. package/dist/core/CompactionEngine.js.map +1 -0
  94. package/dist/core/PromptComposer.d.ts +8 -1
  95. package/dist/core/PromptComposer.d.ts.map +1 -1
  96. package/dist/core/PromptComposer.js +238 -126
  97. package/dist/core/PromptComposer.js.map +1 -1
  98. package/dist/core/PromptSectionCache.d.ts +57 -0
  99. package/dist/core/PromptSectionCache.d.ts.map +1 -0
  100. package/dist/core/PromptSectionCache.js +104 -0
  101. package/dist/core/PromptSectionCache.js.map +1 -0
  102. package/dist/core/ResponseEngine.d.ts +3 -2
  103. package/dist/core/ResponseEngine.d.ts.map +1 -1
  104. package/dist/core/ResponseEngine.js +8 -8
  105. package/dist/core/ResponseEngine.js.map +1 -1
  106. package/dist/core/ResponseModal.d.ts.map +1 -1
  107. package/dist/core/ResponseModal.js +121 -71
  108. package/dist/core/ResponseModal.js.map +1 -1
  109. package/dist/core/ResponsePipeline.d.ts +2 -1
  110. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  111. package/dist/core/ResponsePipeline.js +18 -20
  112. package/dist/core/ResponsePipeline.js.map +1 -1
  113. package/dist/core/RoutingEngine.d.ts +10 -0
  114. package/dist/core/RoutingEngine.d.ts.map +1 -1
  115. package/dist/core/RoutingEngine.js +6 -5
  116. package/dist/core/RoutingEngine.js.map +1 -1
  117. package/dist/core/SessionManager.d.ts.map +1 -1
  118. package/dist/core/SessionManager.js +17 -0
  119. package/dist/core/SessionManager.js.map +1 -1
  120. package/dist/core/StreamingToolExecutor.d.ts +142 -0
  121. package/dist/core/StreamingToolExecutor.d.ts.map +1 -0
  122. package/dist/core/StreamingToolExecutor.js +448 -0
  123. package/dist/core/StreamingToolExecutor.js.map +1 -0
  124. package/dist/core/ToolManager.d.ts +18 -1
  125. package/dist/core/ToolManager.d.ts.map +1 -1
  126. package/dist/core/ToolManager.js +91 -0
  127. package/dist/core/ToolManager.js.map +1 -1
  128. package/dist/index.d.ts +5 -1
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +3 -0
  131. package/dist/index.js.map +1 -1
  132. package/dist/providers/AnthropicProvider.d.ts +7 -0
  133. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  134. package/dist/providers/AnthropicProvider.js +109 -19
  135. package/dist/providers/AnthropicProvider.js.map +1 -1
  136. package/dist/providers/GeminiProvider.d.ts +32 -0
  137. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  138. package/dist/providers/GeminiProvider.js +160 -53
  139. package/dist/providers/GeminiProvider.js.map +1 -1
  140. package/dist/providers/OpenAIProvider.d.ts +5 -0
  141. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  142. package/dist/providers/OpenAIProvider.js +65 -18
  143. package/dist/providers/OpenAIProvider.js.map +1 -1
  144. package/dist/providers/OpenRouterProvider.d.ts +5 -0
  145. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  146. package/dist/providers/OpenRouterProvider.js +57 -18
  147. package/dist/providers/OpenRouterProvider.js.map +1 -1
  148. package/dist/types/agent.d.ts +44 -0
  149. package/dist/types/agent.d.ts.map +1 -1
  150. package/dist/types/agent.js.map +1 -1
  151. package/dist/types/ai.d.ts +2 -2
  152. package/dist/types/ai.d.ts.map +1 -1
  153. package/dist/types/compaction.d.ts +50 -0
  154. package/dist/types/compaction.d.ts.map +1 -0
  155. package/dist/types/compaction.js +5 -0
  156. package/dist/types/compaction.js.map +1 -0
  157. package/dist/types/index.d.ts +4 -2
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/types/index.js.map +1 -1
  160. package/dist/types/tool.d.ts +84 -0
  161. package/dist/types/tool.d.ts.map +1 -1
  162. package/docs/api/overview.md +140 -0
  163. package/docs/core/tools/enhanced-tool.md +186 -0
  164. package/docs/core/tools/streaming-execution.md +161 -0
  165. package/docs/guides/context-compaction.md +96 -0
  166. package/docs/guides/prompt-optimization.md +164 -0
  167. package/examples/advanced-patterns/context-compaction.ts +223 -0
  168. package/examples/advanced-patterns/streaming-responses.ts +85 -7
  169. package/examples/tools/enhanced-tool-metadata.ts +268 -0
  170. package/examples/tools/streaming-tool-execution.ts +283 -0
  171. package/package.json +1 -1
  172. package/src/core/Agent.ts +58 -2
  173. package/src/core/BatchPromptBuilder.ts +4 -1
  174. package/src/core/CompactionEngine.ts +318 -0
  175. package/src/core/PromptComposer.ts +259 -156
  176. package/src/core/PromptSectionCache.ts +136 -0
  177. package/src/core/ResponseEngine.ts +7 -11
  178. package/src/core/ResponseModal.ts +133 -83
  179. package/src/core/ResponsePipeline.ts +22 -22
  180. package/src/core/RoutingEngine.ts +16 -5
  181. package/src/core/SessionManager.ts +19 -0
  182. package/src/core/StreamingToolExecutor.ts +572 -0
  183. package/src/core/ToolManager.ts +151 -41
  184. package/src/index.ts +14 -0
  185. package/src/providers/AnthropicProvider.ts +121 -24
  186. package/src/providers/GeminiProvider.ts +174 -54
  187. package/src/providers/OpenAIProvider.ts +77 -25
  188. package/src/providers/OpenRouterProvider.ts +68 -25
  189. package/src/types/agent.ts +45 -0
  190. package/src/types/ai.ts +2 -2
  191. package/src/types/compaction.ts +52 -0
  192. package/src/types/index.ts +35 -14
  193. package/src/types/tool.ts +108 -0
@@ -15,6 +15,7 @@ import type {
15
15
  GenerateMessageStreamChunk,
16
16
  AgentStructuredResponse,
17
17
  } from "../types";
18
+ import type { HistoryItem } from "../types/history";
18
19
  import { withTimeoutAndRetry, logger } from "../utils";
19
20
 
20
21
  const DEFAULT_RETRY_CONFIG = {
@@ -152,6 +153,67 @@ export class AnthropicProvider implements AiProvider {
152
153
  };
153
154
  }
154
155
 
156
+ /**
157
+ * Build Anthropic-formatted messages from HistoryItem[] array.
158
+ * System messages are extracted separately (Anthropic uses a `system` param).
159
+ * Tool results are mapped to Anthropic's tool_result content blocks.
160
+ * Assistant tool_calls are mapped to tool_use content blocks.
161
+ */
162
+ private buildAnthropicMessages(history: HistoryItem[]): {
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ messages: any[];
165
+ systemMessages: string[];
166
+ } {
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ const messages: any[] = [];
169
+ const systemMessages: string[] = [];
170
+
171
+ for (const item of history) {
172
+ switch (item.role) {
173
+ case "system":
174
+ systemMessages.push(item.content);
175
+ break;
176
+ case "user":
177
+ messages.push({ role: "user", content: item.content });
178
+ break;
179
+ case "assistant":
180
+ if (item.tool_calls && item.tool_calls.length > 0) {
181
+ const content: Array<Record<string, unknown>> = [];
182
+ if (item.content) {
183
+ content.push({ type: "text", text: item.content });
184
+ }
185
+ for (const tc of item.tool_calls) {
186
+ content.push({
187
+ type: "tool_use",
188
+ id: tc.id,
189
+ name: tc.name,
190
+ input: tc.arguments,
191
+ });
192
+ }
193
+ messages.push({ role: "assistant", content });
194
+ } else {
195
+ messages.push({ role: "assistant", content: item.content || "" });
196
+ }
197
+ break;
198
+ case "tool":
199
+ // Anthropic tool results are sent as user messages with tool_result content blocks
200
+ messages.push({
201
+ role: "user",
202
+ content: [
203
+ {
204
+ type: "tool_result",
205
+ tool_use_id: item.tool_call_id,
206
+ content: typeof item.content === "string" ? item.content : JSON.stringify(item.content),
207
+ },
208
+ ],
209
+ });
210
+ break;
211
+ }
212
+ }
213
+
214
+ return { messages, systemMessages };
215
+ }
216
+
155
217
  async generateMessage<
156
218
  TContext = unknown,
157
219
  TStructured = AgentStructuredResponse
@@ -199,8 +261,7 @@ export class AnthropicProvider implements AiProvider {
199
261
  for (let i = 0; i < this.backupModels.length; i++) {
200
262
  const backupModel = this.backupModels[i];
201
263
  logger.debug(
202
- `[ANTHROPIC] Trying backup model ${i + 1}/${
203
- this.backupModels.length
264
+ `[ANTHROPIC] Trying backup model ${i + 1}/${this.backupModels.length
204
265
  }: ${backupModel}`
205
266
  );
206
267
 
@@ -249,18 +310,36 @@ export class AnthropicProvider implements AiProvider {
249
310
  // Anthropic requires max_tokens to be specified
250
311
  const maxTokens = input.parameters?.maxOutputTokens || 4096;
251
312
 
313
+ // Build messages from history
314
+ const { messages: historyMessages, systemMessages } = this.buildAnthropicMessages(input.history);
315
+
316
+ // Append the current prompt as the final user message
317
+ historyMessages.push({
318
+ role: "user",
319
+ content: input.prompt,
320
+ });
321
+
252
322
  const params: MessageCreateParamsNonStreaming = {
253
323
  model,
254
324
  max_tokens: maxTokens,
255
- messages: [
256
- {
257
- role: "user",
258
- content: input.prompt,
259
- },
260
- ],
325
+ messages: historyMessages,
261
326
  ...this.config,
262
327
  };
263
328
 
329
+ // Set system messages from history if present
330
+ if (systemMessages.length > 0) {
331
+ if (typeof this.config?.system === "string") {
332
+ params.system = `${this.config.system}\n\n${systemMessages.join("\n\n")}`;
333
+ } else if (Array.isArray(this.config?.system)) {
334
+ params.system = [
335
+ ...this.config.system,
336
+ ...systemMessages.map(s => ({ type: "text" as const, text: s })),
337
+ ];
338
+ } else {
339
+ params.system = systemMessages.join("\n\n");
340
+ }
341
+ }
342
+
264
343
  // Add tools if provided
265
344
  if (input.tools && input.tools.length > 0) {
266
345
  params.tools = input.tools.map((tool) => ({
@@ -299,10 +378,6 @@ export class AnthropicProvider implements AiProvider {
299
378
  );
300
379
  const message = textContent?.type === "text" ? textContent.text : "";
301
380
 
302
- if (!message) {
303
- throw new Error("No response from Anthropic");
304
- }
305
-
306
381
  // Extract tool calls from response
307
382
  const toolCalls: Array<{
308
383
  toolName: string;
@@ -319,6 +394,11 @@ export class AnthropicProvider implements AiProvider {
319
394
  }
320
395
  }
321
396
 
397
+ // Only throw error if we have no text AND no function calls
398
+ if (!message && toolCalls.length === 0) {
399
+ throw new Error("No response from Anthropic");
400
+ }
401
+
322
402
  // Parse JSON response if schema was provided
323
403
  let structured: AgentStructuredResponse | undefined;
324
404
  if (input.parameters?.jsonSchema) {
@@ -333,9 +413,9 @@ export class AnthropicProvider implements AiProvider {
333
413
  // If tools were used, include them in structured response
334
414
  if (toolCalls.length > 0) {
335
415
  structured = {
336
- message,
416
+ ...(structured || {}),
417
+ message: structured?.message || message,
337
418
  toolCalls,
338
- ...structured,
339
419
  } as AgentStructuredResponse;
340
420
  }
341
421
 
@@ -390,8 +470,7 @@ export class AnthropicProvider implements AiProvider {
390
470
  for (let i = 0; i < this.backupModels.length; i++) {
391
471
  const backupModel = this.backupModels[i];
392
472
  logger.debug(
393
- `[ANTHROPIC] Trying backup model ${i + 1}/${
394
- this.backupModels.length
473
+ `[ANTHROPIC] Trying backup model ${i + 1}/${this.backupModels.length
395
474
  }: ${backupModel}`
396
475
  );
397
476
 
@@ -439,19 +518,37 @@ export class AnthropicProvider implements AiProvider {
439
518
  // Anthropic requires max_tokens to be specified
440
519
  const maxTokens = input.parameters?.maxOutputTokens || 4096;
441
520
 
521
+ // Build messages from history
522
+ const { messages: historyMessages, systemMessages } = this.buildAnthropicMessages(input.history);
523
+
524
+ // Append the current prompt as the final user message
525
+ historyMessages.push({
526
+ role: "user" as const,
527
+ content: input.prompt,
528
+ });
529
+
442
530
  const params = {
443
531
  model,
444
532
  max_tokens: maxTokens,
445
- messages: [
446
- {
447
- role: "user" as const,
448
- content: input.prompt,
449
- },
450
- ],
533
+ messages: historyMessages,
451
534
  stream: true,
452
535
  ...this.config,
453
536
  };
454
537
 
538
+ // Set system messages from history if present
539
+ if (systemMessages.length > 0) {
540
+ if (typeof this.config?.system === "string") {
541
+ params.system = `${this.config.system}\n\n${systemMessages.join("\n\n")}`;
542
+ } else if (Array.isArray(this.config?.system)) {
543
+ params.system = [
544
+ ...this.config.system,
545
+ ...systemMessages.map(s => ({ type: "text" as const, text: s })),
546
+ ];
547
+ } else {
548
+ params.system = systemMessages.join("\n\n");
549
+ }
550
+ }
551
+
455
552
  // Add tools if provided
456
553
  if (input.tools && input.tools.length > 0) {
457
554
  params.tools = input.tools.map((tool) => ({
@@ -536,9 +633,9 @@ export class AnthropicProvider implements AiProvider {
536
633
  // If tools were used, include them in structured response
537
634
  if (toolCalls.length > 0) {
538
635
  structured = {
539
- message: accumulated,
636
+ ...(structured || {}),
637
+ message: structured?.message || accumulated,
540
638
  toolCalls,
541
- ...structured,
542
639
  } as AgentStructuredResponse;
543
640
  }
544
641
 
@@ -19,6 +19,7 @@ import type {
19
19
  AgentStructuredResponse,
20
20
  StructuredSchema,
21
21
  } from "../types";
22
+ import type { HistoryItem } from "../types/history";
22
23
  import { withTimeoutAndRetry } from "../utils/retry";
23
24
  import { tryParseJSONResponse } from "../utils/json";
24
25
  import { logger } from "../utils/logger";
@@ -152,6 +153,122 @@ export class GeminiProvider implements AiProvider {
152
153
  };
153
154
  }
154
155
 
156
+ /**
157
+ * Build Gemini-formatted contents from HistoryItem[] array.
158
+ * Gemini uses "user"/"model" roles (not "assistant").
159
+ * System messages are extracted separately for systemInstruction.
160
+ * Tool results map to functionResponse parts, tool calls to functionCall parts.
161
+ */
162
+ private buildGeminiContents(history: HistoryItem[]): {
163
+ contents: Array<{ role: string; parts: Array<Record<string, unknown>> }>;
164
+ systemInstructions: string[];
165
+ } {
166
+ const contents: Array<{ role: string; parts: Array<Record<string, unknown>> }> = [];
167
+ const systemInstructions: string[] = [];
168
+
169
+ for (const item of history) {
170
+ switch (item.role) {
171
+ case "system":
172
+ systemInstructions.push(item.content);
173
+ break;
174
+ case "user":
175
+ contents.push({ role: "user", parts: [{ text: item.content }] });
176
+ break;
177
+ case "assistant":
178
+ if (item.tool_calls && item.tool_calls.length > 0) {
179
+ const parts: Array<Record<string, unknown>> = [];
180
+ if (item.content) {
181
+ parts.push({ text: item.content });
182
+ }
183
+ for (const tc of item.tool_calls) {
184
+ parts.push({
185
+ functionCall: { name: tc.name, args: tc.arguments },
186
+ });
187
+ }
188
+ contents.push({ role: "model", parts });
189
+ } else {
190
+ contents.push({ role: "model", parts: [{ text: item.content || "" }] });
191
+ }
192
+ break;
193
+ case "tool":
194
+ contents.push({
195
+ role: "user",
196
+ parts: [
197
+ {
198
+ functionResponse: {
199
+ name: item.name,
200
+ response: typeof item.content === "object" ? item.content : { result: item.content },
201
+ },
202
+ },
203
+ ],
204
+ });
205
+ break;
206
+ }
207
+ }
208
+
209
+ return { contents, systemInstructions };
210
+ }
211
+
212
+ /**
213
+ * Convert tool parameter schemas (JSON Schema) to Gemini's Schema format.
214
+ * Gemini's FunctionDeclaration.parameters expects its own Schema type,
215
+ * not raw JSON Schema. This method handles the conversion, including
216
+ * edge cases like empty objects that Gemini rejects.
217
+ *
218
+ * @private
219
+ */
220
+ private convertToolParameters(parameters: unknown): Schema | undefined {
221
+ if (!parameters || typeof parameters !== "object") {
222
+ return undefined;
223
+ }
224
+ try {
225
+ return this.adaptSchemaForGemini(parameters as StructuredSchema);
226
+ } catch (error) {
227
+ logger.warn(`[GeminiProvider] Failed to convert tool parameters, passing as-is:`, error);
228
+ return parameters as Schema;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Safely extract text from a Gemini response or chunk.
234
+ * The `.text` getter can throw when the response contains only function calls
235
+ * (observed in some SDK versions). This method falls back to manually
236
+ * extracting text parts from candidates.
237
+ *
238
+ * @private
239
+ */
240
+ private safeExtractText(responseOrChunk: { text?: string; candidates?: Array<{ content?: { parts?: Array<{ text?: string; functionCall?: unknown }> } }> }): string {
241
+ try {
242
+ return responseOrChunk.text || "";
243
+ } catch {
244
+ // .text getter threw — extract text parts manually
245
+ const parts = responseOrChunk.candidates?.[0]?.content?.parts;
246
+ if (parts) {
247
+ return parts
248
+ .filter((p) => p.text != null)
249
+ .map((p) => p.text)
250
+ .join("");
251
+ }
252
+ return "";
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Build Gemini function declarations from framework tool definitions.
258
+ * Converts JSON Schema parameters to Gemini's Schema format.
259
+ *
260
+ * @private
261
+ */
262
+ private buildFunctionDeclarations(
263
+ tools: Array<{ id: string; name?: string; description?: string; parameters?: unknown }>
264
+ ): FunctionDeclaration[] {
265
+ return tools.map((tool) => ({
266
+ name: tool.name || tool.id,
267
+ description: tool.description || "",
268
+ parameters: this.convertToolParameters(tool.parameters),
269
+ }));
270
+ }
271
+
155
272
  /**
156
273
  * Adapt common schema format to Gemini's specific requirements.
157
274
  * Gemini has strict validation:
@@ -356,15 +473,8 @@ export class GeminiProvider implements AiProvider {
356
473
  if (hasTools) {
357
474
  const toolNames = input.tools?.map((tool) => tool.name || tool.id) || [];
358
475
  logger.debug(`[GeminiProvider] Configuring ${toolNames.length} tools for model ${model}:`, toolNames);
359
- configOverride.tools = [
360
- {
361
- functionDeclarations: input.tools?.map((tool) => ({
362
- name: tool.name || tool.id,
363
- description: tool.description || "",
364
- parameters: tool.parameters as FunctionDeclaration["parameters"], // JSON schema
365
- })),
366
- },
367
- ];
476
+ const functionDeclarations = this.buildFunctionDeclarations(input.tools!);
477
+ configOverride.tools = [{ functionDeclarations }];
368
478
 
369
479
  } else if (hasJsonSchema) {
370
480
  // Only set JSON schema if no tools are present
@@ -377,10 +487,29 @@ export class GeminiProvider implements AiProvider {
377
487
 
378
488
  let response: GenerateContentResponse;
379
489
  try {
490
+ // Build contents from history
491
+ const { contents: historyContents, systemInstructions } = this.buildGeminiContents(input.history);
492
+
493
+ // Append the current prompt as the final user content
494
+ historyContents.push({ role: "user", parts: [{ text: input.prompt }] });
495
+
496
+ // Set system instruction from history if present
497
+ if (systemInstructions.length > 0) {
498
+ const existingSystem = configOverride.systemInstruction;
499
+ if (typeof existingSystem === "string") {
500
+ configOverride.systemInstruction = `${existingSystem}\n\n${systemInstructions.join("\n\n")}`;
501
+ } else {
502
+ configOverride.systemInstruction = systemInstructions.join("\n\n");
503
+ }
504
+ }
505
+
380
506
  response = await this.genAI.models.generateContent({
381
507
  model,
382
- contents: input.prompt,
383
- config: configOverride,
508
+ contents: historyContents,
509
+ config: {
510
+ ...configOverride,
511
+ ...(input.signal ? { abortSignal: input.signal } : {}),
512
+ },
384
513
  });
385
514
  } catch (error: unknown) {
386
515
  logger.error(`[GeminiProvider] API call failed:`, error);
@@ -399,7 +528,7 @@ export class GeminiProvider implements AiProvider {
399
528
  if (part.functionCall) {
400
529
  toolCalls.push({
401
530
  toolName: part.functionCall.name || "",
402
- arguments: part.functionCall.args as Record<string, unknown>,
531
+ arguments: (part.functionCall.args as Record<string, unknown>) || {},
403
532
  });
404
533
  }
405
534
  }
@@ -408,30 +537,13 @@ export class GeminiProvider implements AiProvider {
408
537
  // Debug logging for response structure
409
538
  if (!response.text && toolCalls.length === 0) {
410
539
  logger.debug(`[GeminiProvider] Debug - Response structure:`, {
411
- hasText: !!response.text,
412
540
  candidatesCount: response.candidates?.length || 0,
413
- firstCandidateContent: response.candidates?.[0]?.content,
414
541
  firstCandidateParts: response.candidates?.[0]?.content?.parts?.length || 0,
415
542
  });
416
543
  }
417
- // Try to get text from response, handling function calls properly
418
- let message = "";
419
- try {
420
- message = response.text || "";
421
- } catch (textError) {
422
- // Sometimes response.text throws when there are function calls
423
- logger.debug(`[GeminiProvider] Could not get response.text (likely due to function calls):`, textError);
424
-
425
- // Try to extract text parts manually
426
- if (response.candidates && response.candidates[0]?.content?.parts) {
427
- const textParts = response.candidates[0].content.parts
428
- .filter(part => part.text)
429
- .map(part => part.text)
430
- .join('');
431
- message = textParts;
432
- logger.debug(`[GeminiProvider] Extracted text from parts:`, message);
433
- }
434
- }
544
+
545
+ // Safely extract text — .text getter can throw when response has only function calls
546
+ const message = this.safeExtractText(response);
435
547
 
436
548
  // Only throw error if we have no text AND no function calls
437
549
  if (!message && toolCalls.length === 0) {
@@ -443,15 +555,8 @@ export class GeminiProvider implements AiProvider {
443
555
  // Log when we have function calls but no text (this is normal)
444
556
  if (toolCalls.length > 0 && !message) {
445
557
  logger.debug(`[GeminiProvider] Function calls detected without text message:`, toolCalls.map(tc => tc.toolName));
446
- } else if (toolCalls.length > 0 && message) {
447
- logger.debug(`[GeminiProvider] Response has both text and function calls:`, {
448
- messageLength: message.length,
449
- toolCalls: toolCalls.map(tc => tc.toolName),
450
- });
451
558
  }
452
559
 
453
-
454
-
455
560
  // Parse JSON response if schema was provided
456
561
  let structured: AgentStructuredResponse | undefined;
457
562
  if (input.parameters?.jsonSchema) {
@@ -574,15 +679,8 @@ export class GeminiProvider implements AiProvider {
574
679
  if (hasTools) {
575
680
  const toolNames = input.tools?.map((tool) => tool.name || tool.id) || [];
576
681
  logger.debug(`[GeminiProvider] Configuring ${toolNames.length} tools for streaming:`, toolNames);
577
- configOverride.tools = [
578
- {
579
- functionDeclarations: input.tools?.map((tool) => ({
580
- name: tool.name || tool.id,
581
- description: tool.description || "",
582
- parameters: tool.parameters as FunctionDeclaration["parameters"],
583
- })),
584
- },
585
- ];
682
+ const functionDeclarations = this.buildFunctionDeclarations(input.tools!);
683
+ configOverride.tools = [{ functionDeclarations }];
586
684
 
587
685
  } else if (hasJsonSchema) {
588
686
  // Only set JSON schema if no tools are present
@@ -595,10 +693,29 @@ export class GeminiProvider implements AiProvider {
595
693
 
596
694
  let stream;
597
695
  try {
696
+ // Build contents from history
697
+ const { contents: historyContents, systemInstructions } = this.buildGeminiContents(input.history);
698
+
699
+ // Append the current prompt as the final user content
700
+ historyContents.push({ role: "user", parts: [{ text: input.prompt }] });
701
+
702
+ // Set system instruction from history if present
703
+ if (systemInstructions.length > 0) {
704
+ const existingSystem = configOverride.systemInstruction;
705
+ if (typeof existingSystem === "string") {
706
+ configOverride.systemInstruction = `${existingSystem}\n\n${systemInstructions.join("\n\n")}`;
707
+ } else {
708
+ configOverride.systemInstruction = systemInstructions.join("\n\n");
709
+ }
710
+ }
711
+
598
712
  stream = await this.genAI.models.generateContentStream({
599
713
  model,
600
- contents: input.prompt,
601
- config: configOverride,
714
+ contents: historyContents,
715
+ config: {
716
+ ...configOverride,
717
+ ...(input.signal ? { abortSignal: input.signal } : {}),
718
+ },
602
719
  });
603
720
  } catch (error: unknown) {
604
721
  logger.error(`[GeminiProvider] Streaming API call failed:`, error);
@@ -615,7 +732,10 @@ export class GeminiProvider implements AiProvider {
615
732
  }> = [];
616
733
 
617
734
  for await (const chunk of stream) {
618
- const delta = chunk.text || "";
735
+ if (input.signal?.aborted) break;
736
+
737
+ // Safely extract text — chunk.text can throw when chunk has only function calls
738
+ const delta = this.safeExtractText(chunk);
619
739
 
620
740
  // Extract tool calls from chunk
621
741
  if (chunk.candidates && chunk.candidates[0]?.content?.parts) {
@@ -623,7 +743,7 @@ export class GeminiProvider implements AiProvider {
623
743
  if (part.functionCall) {
624
744
  toolCalls.push({
625
745
  toolName: part.functionCall.name || "",
626
- arguments: part.functionCall.args as Record<string, unknown>,
746
+ arguments: (part.functionCall.args as Record<string, unknown>) || {},
627
747
  });
628
748
  }
629
749
  }
@@ -657,12 +777,12 @@ export class GeminiProvider implements AiProvider {
657
777
  }
658
778
  }
659
779
 
660
- // If tools were used, include them in structured response
780
+ // Include tool calls in structured response (even without JSON schema)
661
781
  if (toolCalls.length > 0) {
662
782
  structured = {
663
783
  message: structured?.message || accumulated,
664
- toolCalls,
665
784
  ...structured,
785
+ toolCalls,
666
786
  } as AgentStructuredResponse;
667
787
  }
668
788