@falai/agent 1.1.2 → 1.2.0

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 (173) 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 +14 -11
  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 -118
  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 -0
  23. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  24. package/dist/cjs/core/ResponseEngine.js +10 -6
  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 +79 -20
  28. package/dist/cjs/core/ResponseModal.js.map +1 -1
  29. package/dist/cjs/core/RoutingEngine.d.ts +10 -0
  30. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
  31. package/dist/cjs/core/RoutingEngine.js +3 -2
  32. package/dist/cjs/core/RoutingEngine.js.map +1 -1
  33. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  34. package/dist/cjs/core/SessionManager.js +20 -0
  35. package/dist/cjs/core/SessionManager.js.map +1 -1
  36. package/dist/cjs/core/StreamingToolExecutor.d.ts +142 -0
  37. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -0
  38. package/dist/cjs/core/StreamingToolExecutor.js +455 -0
  39. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -0
  40. package/dist/cjs/core/ToolManager.d.ts +18 -1
  41. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  42. package/dist/cjs/core/ToolManager.js +91 -0
  43. package/dist/cjs/core/ToolManager.js.map +1 -1
  44. package/dist/cjs/index.d.ts +5 -1
  45. package/dist/cjs/index.d.ts.map +1 -1
  46. package/dist/cjs/index.js +8 -2
  47. package/dist/cjs/index.js.map +1 -1
  48. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  49. package/dist/cjs/providers/AnthropicProvider.js +8 -7
  50. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  51. package/dist/cjs/providers/GeminiProvider.d.ts +25 -0
  52. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  53. package/dist/cjs/providers/GeminiProvider.js +79 -51
  54. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  55. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  56. package/dist/cjs/providers/OpenAIProvider.js +14 -6
  57. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  58. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  59. package/dist/cjs/providers/OpenRouterProvider.js +7 -6
  60. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  61. package/dist/cjs/types/agent.d.ts +44 -0
  62. package/dist/cjs/types/agent.d.ts.map +1 -1
  63. package/dist/cjs/types/agent.js.map +1 -1
  64. package/dist/cjs/types/compaction.d.ts +50 -0
  65. package/dist/cjs/types/compaction.d.ts.map +1 -0
  66. package/dist/cjs/types/compaction.js +6 -0
  67. package/dist/cjs/types/compaction.js.map +1 -0
  68. package/dist/cjs/types/index.d.ts +4 -2
  69. package/dist/cjs/types/index.d.ts.map +1 -1
  70. package/dist/cjs/types/index.js.map +1 -1
  71. package/dist/cjs/types/tool.d.ts +84 -0
  72. package/dist/cjs/types/tool.d.ts.map +1 -1
  73. package/dist/core/Agent.d.ts +17 -1
  74. package/dist/core/Agent.d.ts.map +1 -1
  75. package/dist/core/Agent.js +47 -0
  76. package/dist/core/Agent.js.map +1 -1
  77. package/dist/core/BatchPromptBuilder.d.ts +3 -0
  78. package/dist/core/BatchPromptBuilder.d.ts.map +1 -1
  79. package/dist/core/BatchPromptBuilder.js +14 -11
  80. package/dist/core/BatchPromptBuilder.js.map +1 -1
  81. package/dist/core/CompactionEngine.d.ts +65 -0
  82. package/dist/core/CompactionEngine.d.ts.map +1 -0
  83. package/dist/core/CompactionEngine.js +244 -0
  84. package/dist/core/CompactionEngine.js.map +1 -0
  85. package/dist/core/PromptComposer.d.ts +8 -1
  86. package/dist/core/PromptComposer.d.ts.map +1 -1
  87. package/dist/core/PromptComposer.js +238 -118
  88. package/dist/core/PromptComposer.js.map +1 -1
  89. package/dist/core/PromptSectionCache.d.ts +57 -0
  90. package/dist/core/PromptSectionCache.d.ts.map +1 -0
  91. package/dist/core/PromptSectionCache.js +104 -0
  92. package/dist/core/PromptSectionCache.js.map +1 -0
  93. package/dist/core/ResponseEngine.d.ts +3 -0
  94. package/dist/core/ResponseEngine.d.ts.map +1 -1
  95. package/dist/core/ResponseEngine.js +10 -6
  96. package/dist/core/ResponseEngine.js.map +1 -1
  97. package/dist/core/ResponseModal.d.ts.map +1 -1
  98. package/dist/core/ResponseModal.js +79 -20
  99. package/dist/core/ResponseModal.js.map +1 -1
  100. package/dist/core/RoutingEngine.d.ts +10 -0
  101. package/dist/core/RoutingEngine.d.ts.map +1 -1
  102. package/dist/core/RoutingEngine.js +3 -2
  103. package/dist/core/RoutingEngine.js.map +1 -1
  104. package/dist/core/SessionManager.d.ts.map +1 -1
  105. package/dist/core/SessionManager.js +17 -0
  106. package/dist/core/SessionManager.js.map +1 -1
  107. package/dist/core/StreamingToolExecutor.d.ts +142 -0
  108. package/dist/core/StreamingToolExecutor.d.ts.map +1 -0
  109. package/dist/core/StreamingToolExecutor.js +448 -0
  110. package/dist/core/StreamingToolExecutor.js.map +1 -0
  111. package/dist/core/ToolManager.d.ts +18 -1
  112. package/dist/core/ToolManager.d.ts.map +1 -1
  113. package/dist/core/ToolManager.js +91 -0
  114. package/dist/core/ToolManager.js.map +1 -1
  115. package/dist/index.d.ts +5 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +3 -0
  118. package/dist/index.js.map +1 -1
  119. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  120. package/dist/providers/AnthropicProvider.js +8 -7
  121. package/dist/providers/AnthropicProvider.js.map +1 -1
  122. package/dist/providers/GeminiProvider.d.ts +25 -0
  123. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  124. package/dist/providers/GeminiProvider.js +79 -51
  125. package/dist/providers/GeminiProvider.js.map +1 -1
  126. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  127. package/dist/providers/OpenAIProvider.js +14 -6
  128. package/dist/providers/OpenAIProvider.js.map +1 -1
  129. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  130. package/dist/providers/OpenRouterProvider.js +7 -6
  131. package/dist/providers/OpenRouterProvider.js.map +1 -1
  132. package/dist/types/agent.d.ts +44 -0
  133. package/dist/types/agent.d.ts.map +1 -1
  134. package/dist/types/agent.js.map +1 -1
  135. package/dist/types/compaction.d.ts +50 -0
  136. package/dist/types/compaction.d.ts.map +1 -0
  137. package/dist/types/compaction.js +5 -0
  138. package/dist/types/compaction.js.map +1 -0
  139. package/dist/types/index.d.ts +4 -2
  140. package/dist/types/index.d.ts.map +1 -1
  141. package/dist/types/index.js.map +1 -1
  142. package/dist/types/tool.d.ts +84 -0
  143. package/dist/types/tool.d.ts.map +1 -1
  144. package/docs/api/overview.md +140 -0
  145. package/docs/core/tools/enhanced-tool.md +186 -0
  146. package/docs/core/tools/streaming-execution.md +161 -0
  147. package/docs/guides/context-compaction.md +96 -0
  148. package/docs/guides/prompt-optimization.md +164 -0
  149. package/examples/advanced-patterns/context-compaction.ts +223 -0
  150. package/examples/advanced-patterns/streaming-responses.ts +85 -7
  151. package/examples/tools/enhanced-tool-metadata.ts +268 -0
  152. package/examples/tools/streaming-tool-execution.ts +283 -0
  153. package/package.json +1 -1
  154. package/src/core/Agent.ts +58 -2
  155. package/src/core/BatchPromptBuilder.ts +14 -11
  156. package/src/core/CompactionEngine.ts +318 -0
  157. package/src/core/PromptComposer.ts +261 -141
  158. package/src/core/PromptSectionCache.ts +136 -0
  159. package/src/core/ResponseEngine.ts +9 -6
  160. package/src/core/ResponseModal.ts +81 -20
  161. package/src/core/RoutingEngine.ts +13 -2
  162. package/src/core/SessionManager.ts +19 -0
  163. package/src/core/StreamingToolExecutor.ts +572 -0
  164. package/src/core/ToolManager.ts +151 -41
  165. package/src/index.ts +14 -0
  166. package/src/providers/AnthropicProvider.ts +11 -12
  167. package/src/providers/GeminiProvider.ts +83 -52
  168. package/src/providers/OpenAIProvider.ts +21 -13
  169. package/src/providers/OpenRouterProvider.ts +13 -13
  170. package/src/types/agent.ts +45 -0
  171. package/src/types/compaction.ts +52 -0
  172. package/src/types/index.ts +35 -14
  173. package/src/types/tool.ts +108 -0
@@ -152,6 +152,66 @@ export class GeminiProvider implements AiProvider {
152
152
  };
153
153
  }
154
154
 
155
+ /**
156
+ * Convert tool parameter schemas (JSON Schema) to Gemini's Schema format.
157
+ * Gemini's FunctionDeclaration.parameters expects its own Schema type,
158
+ * not raw JSON Schema. This method handles the conversion, including
159
+ * edge cases like empty objects that Gemini rejects.
160
+ *
161
+ * @private
162
+ */
163
+ private convertToolParameters(parameters: unknown): Schema | undefined {
164
+ if (!parameters || typeof parameters !== "object") {
165
+ return undefined;
166
+ }
167
+ try {
168
+ return this.adaptSchemaForGemini(parameters as StructuredSchema);
169
+ } catch (error) {
170
+ logger.warn(`[GeminiProvider] Failed to convert tool parameters, passing as-is:`, error);
171
+ return parameters as Schema;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Safely extract text from a Gemini response or chunk.
177
+ * The `.text` getter can throw when the response contains only function calls
178
+ * (observed in some SDK versions). This method falls back to manually
179
+ * extracting text parts from candidates.
180
+ *
181
+ * @private
182
+ */
183
+ private safeExtractText(responseOrChunk: { text?: string; candidates?: Array<{ content?: { parts?: Array<{ text?: string; functionCall?: unknown }> } }> }): string {
184
+ try {
185
+ return responseOrChunk.text || "";
186
+ } catch {
187
+ // .text getter threw — extract text parts manually
188
+ const parts = responseOrChunk.candidates?.[0]?.content?.parts;
189
+ if (parts) {
190
+ return parts
191
+ .filter((p) => p.text != null)
192
+ .map((p) => p.text)
193
+ .join("");
194
+ }
195
+ return "";
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Build Gemini function declarations from framework tool definitions.
201
+ * Converts JSON Schema parameters to Gemini's Schema format.
202
+ *
203
+ * @private
204
+ */
205
+ private buildFunctionDeclarations(
206
+ tools: Array<{ id: string; name?: string; description?: string; parameters?: unknown }>
207
+ ): FunctionDeclaration[] {
208
+ return tools.map((tool) => ({
209
+ name: tool.name || tool.id,
210
+ description: tool.description || "",
211
+ parameters: this.convertToolParameters(tool.parameters),
212
+ }));
213
+ }
214
+
155
215
  /**
156
216
  * Adapt common schema format to Gemini's specific requirements.
157
217
  * Gemini has strict validation:
@@ -356,15 +416,8 @@ export class GeminiProvider implements AiProvider {
356
416
  if (hasTools) {
357
417
  const toolNames = input.tools?.map((tool) => tool.name || tool.id) || [];
358
418
  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
- ];
419
+ const functionDeclarations = this.buildFunctionDeclarations(input.tools!);
420
+ configOverride.tools = [{ functionDeclarations }];
368
421
 
369
422
  } else if (hasJsonSchema) {
370
423
  // Only set JSON schema if no tools are present
@@ -380,7 +433,10 @@ export class GeminiProvider implements AiProvider {
380
433
  response = await this.genAI.models.generateContent({
381
434
  model,
382
435
  contents: input.prompt,
383
- config: configOverride,
436
+ config: {
437
+ ...configOverride,
438
+ ...(input.signal ? { abortSignal: input.signal } : {}),
439
+ },
384
440
  });
385
441
  } catch (error: unknown) {
386
442
  logger.error(`[GeminiProvider] API call failed:`, error);
@@ -399,7 +455,7 @@ export class GeminiProvider implements AiProvider {
399
455
  if (part.functionCall) {
400
456
  toolCalls.push({
401
457
  toolName: part.functionCall.name || "",
402
- arguments: part.functionCall.args as Record<string, unknown>,
458
+ arguments: (part.functionCall.args as Record<string, unknown>) || {},
403
459
  });
404
460
  }
405
461
  }
@@ -408,30 +464,13 @@ export class GeminiProvider implements AiProvider {
408
464
  // Debug logging for response structure
409
465
  if (!response.text && toolCalls.length === 0) {
410
466
  logger.debug(`[GeminiProvider] Debug - Response structure:`, {
411
- hasText: !!response.text,
412
467
  candidatesCount: response.candidates?.length || 0,
413
- firstCandidateContent: response.candidates?.[0]?.content,
414
468
  firstCandidateParts: response.candidates?.[0]?.content?.parts?.length || 0,
415
469
  });
416
470
  }
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
- }
471
+
472
+ // Safely extract text — .text getter can throw when response has only function calls
473
+ const message = this.safeExtractText(response);
435
474
 
436
475
  // Only throw error if we have no text AND no function calls
437
476
  if (!message && toolCalls.length === 0) {
@@ -443,15 +482,8 @@ export class GeminiProvider implements AiProvider {
443
482
  // Log when we have function calls but no text (this is normal)
444
483
  if (toolCalls.length > 0 && !message) {
445
484
  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
485
  }
452
486
 
453
-
454
-
455
487
  // Parse JSON response if schema was provided
456
488
  let structured: AgentStructuredResponse | undefined;
457
489
  if (input.parameters?.jsonSchema) {
@@ -574,15 +606,8 @@ export class GeminiProvider implements AiProvider {
574
606
  if (hasTools) {
575
607
  const toolNames = input.tools?.map((tool) => tool.name || tool.id) || [];
576
608
  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
- ];
609
+ const functionDeclarations = this.buildFunctionDeclarations(input.tools!);
610
+ configOverride.tools = [{ functionDeclarations }];
586
611
 
587
612
  } else if (hasJsonSchema) {
588
613
  // Only set JSON schema if no tools are present
@@ -598,7 +623,10 @@ export class GeminiProvider implements AiProvider {
598
623
  stream = await this.genAI.models.generateContentStream({
599
624
  model,
600
625
  contents: input.prompt,
601
- config: configOverride,
626
+ config: {
627
+ ...configOverride,
628
+ ...(input.signal ? { abortSignal: input.signal } : {}),
629
+ },
602
630
  });
603
631
  } catch (error: unknown) {
604
632
  logger.error(`[GeminiProvider] Streaming API call failed:`, error);
@@ -615,7 +643,10 @@ export class GeminiProvider implements AiProvider {
615
643
  }> = [];
616
644
 
617
645
  for await (const chunk of stream) {
618
- const delta = chunk.text || "";
646
+ if (input.signal?.aborted) break;
647
+
648
+ // Safely extract text — chunk.text can throw when chunk has only function calls
649
+ const delta = this.safeExtractText(chunk);
619
650
 
620
651
  // Extract tool calls from chunk
621
652
  if (chunk.candidates && chunk.candidates[0]?.content?.parts) {
@@ -623,7 +654,7 @@ export class GeminiProvider implements AiProvider {
623
654
  if (part.functionCall) {
624
655
  toolCalls.push({
625
656
  toolName: part.functionCall.name || "",
626
- arguments: part.functionCall.args as Record<string, unknown>,
657
+ arguments: (part.functionCall.args as Record<string, unknown>) || {},
627
658
  });
628
659
  }
629
660
  }
@@ -657,12 +688,12 @@ export class GeminiProvider implements AiProvider {
657
688
  }
658
689
  }
659
690
 
660
- // If tools were used, include them in structured response
691
+ // Include tool calls in structured response (even without JSON schema)
661
692
  if (toolCalls.length > 0) {
662
693
  structured = {
663
694
  message: structured?.message || accumulated,
664
- toolCalls,
665
695
  ...structured,
696
+ toolCalls,
666
697
  } as AgentStructuredResponse;
667
698
  }
668
699
 
@@ -222,8 +222,7 @@ export class OpenAIProvider implements AiProvider {
222
222
  for (let i = 0; i < this.backupModels.length; i++) {
223
223
  const backupModel = this.backupModels[i];
224
224
  logger.debug(
225
- `[OPENAI] Trying backup model ${i + 1}/${
226
- this.backupModels.length
225
+ `[OPENAI] Trying backup model ${i + 1}/${this.backupModels.length
227
226
  }: ${backupModel}`
228
227
  );
229
228
 
@@ -336,10 +335,7 @@ export class OpenAIProvider implements AiProvider {
336
335
  // Fall back to regular chat completions API if no schema provided
337
336
  const response = await this.client.chat.completions.create(params);
338
337
 
339
- const message = response.choices[0]?.message?.content;
340
- if (!message) {
341
- throw new Error("No response from OpenAI");
342
- }
338
+ const message = response.choices[0]?.message?.content || "";
343
339
 
344
340
  let toolCalls: Array<{
345
341
  toolName: string;
@@ -368,7 +364,11 @@ export class OpenAIProvider implements AiProvider {
368
364
  };
369
365
  });
370
366
  }
371
- // Extract tool calls from response
367
+
368
+ // Only throw error if we have no text AND no function calls
369
+ if (!message && toolCalls.length === 0) {
370
+ throw new Error("No response from OpenAI");
371
+ }
372
372
 
373
373
  return {
374
374
  message,
@@ -423,8 +423,7 @@ export class OpenAIProvider implements AiProvider {
423
423
  for (let i = 0; i < this.backupModels.length; i++) {
424
424
  const backupModel = this.backupModels[i];
425
425
  logger.debug(
426
- `[OPENAI] Trying backup model ${i + 1}/${
427
- this.backupModels.length
426
+ `[OPENAI] Trying backup model ${i + 1}/${this.backupModels.length
428
427
  }: ${backupModel}`
429
428
  );
430
429
 
@@ -530,9 +529,9 @@ export class OpenAIProvider implements AiProvider {
530
529
  try {
531
530
  toolCallArguments = toolCall.function.arguments
532
531
  ? (JSON.parse(toolCall.function.arguments) as Record<
533
- string,
534
- unknown
535
- >)
532
+ string,
533
+ unknown
534
+ >)
536
535
  : {};
537
536
  } catch (error) {
538
537
  logger.warn(
@@ -584,6 +583,15 @@ export class OpenAIProvider implements AiProvider {
584
583
  }
585
584
  }
586
585
 
586
+ // Include tool calls in structured response (even without JSON schema)
587
+ if (toolCalls.length > 0) {
588
+ structured = {
589
+ ...(structured || {}),
590
+ message: (structured as AgentStructuredResponse | undefined)?.message || accumulated,
591
+ toolCalls,
592
+ } as TStructured;
593
+ }
594
+
587
595
  // Yield final chunk
588
596
  yield {
589
597
  delta: "",
@@ -596,7 +604,7 @@ export class OpenAIProvider implements AiProvider {
596
604
  promptTokens,
597
605
  completionTokens,
598
606
  },
599
- structured: structured ? { ...structured, toolCalls } : undefined,
607
+ structured,
600
608
  };
601
609
  }
602
610
  }
@@ -229,8 +229,7 @@ export class OpenRouterProvider implements AiProvider {
229
229
  for (let i = 0; i < this.backupModels.length; i++) {
230
230
  const backupModel = this.backupModels[i];
231
231
  logger.debug(
232
- `[OPENROUTER] Trying backup model ${i + 1}/${
233
- this.backupModels.length
232
+ `[OPENROUTER] Trying backup model ${i + 1}/${this.backupModels.length
234
233
  }: ${backupModel}`
235
234
  );
236
235
 
@@ -345,10 +344,7 @@ export class OpenRouterProvider implements AiProvider {
345
344
  // Fall back to regular chat completions API if no schema provided
346
345
  const response = await this.client.chat.completions.create(params);
347
346
 
348
- const message = response.choices[0]?.message?.content;
349
- if (!message) {
350
- throw new Error("No response from OpenRouter");
351
- }
347
+ const message = response.choices[0]?.message?.content || "";
352
348
 
353
349
  let toolCalls: Array<{
354
350
  toolName: string;
@@ -378,6 +374,11 @@ export class OpenRouterProvider implements AiProvider {
378
374
  });
379
375
  }
380
376
 
377
+ // Only throw error if we have no text AND no function calls
378
+ if (!message && toolCalls.length === 0) {
379
+ throw new Error("No response from OpenRouter");
380
+ }
381
+
381
382
  return {
382
383
  message,
383
384
  metadata: {
@@ -428,8 +429,7 @@ export class OpenRouterProvider implements AiProvider {
428
429
  for (let i = 0; i < this.backupModels.length; i++) {
429
430
  const backupModel = this.backupModels[i];
430
431
  logger.debug(
431
- `[OPENROUTER] Trying backup model ${i + 1}/${
432
- this.backupModels.length
432
+ `[OPENROUTER] Trying backup model ${i + 1}/${this.backupModels.length
433
433
  }: ${backupModel}`
434
434
  );
435
435
 
@@ -532,9 +532,9 @@ export class OpenRouterProvider implements AiProvider {
532
532
  try {
533
533
  toolCallArguments = toolCall.function.arguments
534
534
  ? (JSON.parse(toolCall.function.arguments) as Record<
535
- string,
536
- unknown
537
- >)
535
+ string,
536
+ unknown
537
+ >)
538
538
  : {};
539
539
  } catch (error) {
540
540
  logger.warn(
@@ -589,9 +589,9 @@ export class OpenRouterProvider implements AiProvider {
589
589
  // If tools were used, include them in structured response
590
590
  if (toolCalls.length > 0) {
591
591
  structured = {
592
- message: accumulated,
592
+ ...(structured || {}),
593
+ message: (structured as AgentStructuredResponse | undefined)?.message || accumulated,
593
594
  toolCalls,
594
- ...structured,
595
595
  } as TStructured;
596
596
  }
597
597
 
@@ -9,6 +9,39 @@ import type { PersistenceConfig } from "./persistence";
9
9
  import type { SessionState } from "./session";
10
10
  import type { StructuredSchema } from "./schema";
11
11
  import { Template, ConditionTemplate } from "./template";
12
+ import type { PromptCacheConfig } from "../core/PromptSectionCache";
13
+
14
+ /**
15
+ * Agent-level compaction configuration.
16
+ * Unlike CompactionOptions, this does not require a `provider` since the agent already has one.
17
+ */
18
+ export interface AgentCompactionConfig {
19
+ /** Maximum token budget for the conversation */
20
+ maxTokens: number;
21
+ /**
22
+ * Threshold ratio (0–1) at which to trigger compaction.
23
+ * Must be between 0.5 and 0.95.
24
+ * @default 0.8
25
+ */
26
+ compactionThreshold?: number;
27
+ /**
28
+ * Number of recent messages to always preserve unchanged.
29
+ * Must be >= 2.
30
+ * @default 4
31
+ */
32
+ preserveRecentCount?: number;
33
+ /**
34
+ * Maximum characters per tool result before truncation.
35
+ * Must be > 0.
36
+ * @default 5000
37
+ */
38
+ maxToolResultChars?: number;
39
+ /**
40
+ * Whether compaction is enabled.
41
+ * @default true when config is provided
42
+ */
43
+ enabled?: boolean;
44
+ }
12
45
 
13
46
  /**
14
47
  * Composition mode determines how the agent processes and structures responses
@@ -132,6 +165,18 @@ export interface AgentOptions<TContext = unknown, TData = unknown> {
132
165
  * @default 1
133
166
  */
134
167
  maxStepsPerBatch?: number;
168
+ /**
169
+ * Optional compaction configuration for managing conversation history size.
170
+ * When provided, the agent will validate the options and make them available
171
+ * for use by the SessionManager/CompactionEngine.
172
+ */
173
+ compaction?: AgentCompactionConfig;
174
+ /**
175
+ * Optional prompt cache configuration for controlling section memoization behavior.
176
+ * When provided, controls whether prompt sections are cached across turns.
177
+ * @default { enabled: true }
178
+ */
179
+ promptCache?: PromptCacheConfig;
135
180
  }
136
181
 
137
182
  /**
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Context compaction types for managing conversation history size
3
+ */
4
+
5
+ import type { AiProvider } from "./ai";
6
+ import type { HistoryItem } from "./history";
7
+
8
+ /**
9
+ * Configuration for the compaction engine.
10
+ *
11
+ * Validation constraints:
12
+ * - `compactionThreshold` must be between 0.5 and 0.95
13
+ * - `preserveRecentCount` must be >= 2
14
+ * - `maxToolResultChars` must be > 0
15
+ */
16
+ export interface CompactionOptions {
17
+ /** Maximum token budget for the conversation */
18
+ maxTokens: number;
19
+ /**
20
+ * Threshold ratio (0–1) at which to trigger compaction.
21
+ * Must be between 0.5 and 0.95.
22
+ */
23
+ compactionThreshold: number;
24
+ /**
25
+ * Number of recent messages to always preserve unchanged.
26
+ * Must be >= 2.
27
+ */
28
+ preserveRecentCount: number;
29
+ /**
30
+ * Maximum characters per tool result before truncation.
31
+ * Must be > 0.
32
+ */
33
+ maxToolResultChars: number;
34
+ /** Provider to use for LLM summarization during auto-compact */
35
+ provider: AiProvider;
36
+ }
37
+
38
+ /**
39
+ * Result of a compaction operation
40
+ */
41
+ export interface CompactionResult {
42
+ /** The compacted history */
43
+ history: HistoryItem[];
44
+ /** Strategy that was applied */
45
+ strategy: 'none' | 'tool_result_budget' | 'micro_compact' | 'auto_compact';
46
+ /** Estimated tokens after compaction */
47
+ estimatedTokens: number;
48
+ /** Number of messages removed/compacted */
49
+ messagesCompacted: number;
50
+ /** Summary text (if auto-compact was used) */
51
+ summary?: string;
52
+ }
@@ -5,6 +5,7 @@
5
5
  // Agent types
6
6
  export type {
7
7
  AgentOptions,
8
+ AgentCompactionConfig,
8
9
  Term,
9
10
  Guideline,
10
11
  GuidelineMatch,
@@ -58,12 +59,19 @@ export * from "./route";
58
59
  export type { SessionState, PendingTransition } from "./session";
59
60
 
60
61
  // Tool types
61
- export type {
62
- Tool,
63
- ToolContext,
64
- ToolResult,
62
+ export type {
63
+ Tool,
64
+ ToolContext,
65
+ ToolResult,
65
66
  ToolHandler,
66
67
  ToolExecutionResult,
68
+ EnhancedTool,
69
+ ToolValidationResult,
70
+ ToolPermissionResult,
71
+ ToolCallRequest,
72
+ ToolExecutionUpdate,
73
+ TrackedTool,
74
+ ToolStatus,
67
75
  DataEnrichmentConfig,
68
76
  ValidationConfig,
69
77
  ApiCallConfig,
@@ -71,6 +79,19 @@ export type {
71
79
  } from "./tool";
72
80
  export { ToolScope } from "./tool";
73
81
 
82
+ // Compaction types
83
+ export type {
84
+ CompactionOptions,
85
+ CompactionResult,
86
+ } from "./compaction";
87
+
88
+ // Prompt cache types (re-exported from core)
89
+ export type {
90
+ PromptSectionType,
91
+ PromptCacheConfig,
92
+ SectionCompute,
93
+ } from "../core/PromptSectionCache";
94
+
74
95
  // AI provider types
75
96
  export type {
76
97
  AiProvider,
@@ -105,15 +126,15 @@ export type {
105
126
  export * from "./persistence";
106
127
 
107
128
  // Template types
108
- export type {
109
- Template,
110
- TemplateContext,
111
- ConditionTemplate,
112
- ConditionEvaluationResult
129
+ export type {
130
+ Template,
131
+ TemplateContext,
132
+ ConditionTemplate,
133
+ ConditionEvaluationResult
113
134
  } from "./template";
114
- export {
115
- ConditionEvaluator,
116
- createConditionEvaluator,
117
- extractAIContextStrings,
118
- hasProgrammaticConditions
135
+ export {
136
+ ConditionEvaluator,
137
+ createConditionEvaluator,
138
+ extractAIContextStrings,
139
+ hasProgrammaticConditions
119
140
  } from "../utils/condition";
package/src/types/tool.ts CHANGED
@@ -116,6 +116,114 @@ export enum ToolScope {
116
116
 
117
117
 
118
118
 
119
+ // --- EnhancedTool and supporting types ---
120
+
121
+ /**
122
+ * Result of input validation on a tool call
123
+ */
124
+ export interface ToolValidationResult {
125
+ valid: boolean;
126
+ error?: string;
127
+ /** Suggested corrected input */
128
+ correctedInput?: Record<string, unknown>;
129
+ }
130
+
131
+ /**
132
+ * Result of a permission check on a tool call
133
+ */
134
+ export interface ToolPermissionResult {
135
+ allowed: boolean;
136
+ reason?: string;
137
+ /** If not allowed, can the user override? */
138
+ canOverride?: boolean;
139
+ }
140
+
141
+ /**
142
+ * A single tool invocation request from the LLM
143
+ */
144
+ export interface ToolCallRequest {
145
+ /** Unique ID for this tool call instance */
146
+ id: string;
147
+ /** Tool name/ID to execute */
148
+ toolName: string;
149
+ /** Arguments passed to the tool */
150
+ arguments: Record<string, unknown>;
151
+ }
152
+
153
+ /**
154
+ * Result or progress update from a tool execution
155
+ */
156
+ export interface ToolExecutionUpdate<TData = unknown> {
157
+ /** The tool call this update relates to */
158
+ toolCallId: string;
159
+ /** Result message (undefined for progress updates) */
160
+ result?: ToolExecutionResult;
161
+ /** Progress message for long-running tools */
162
+ progress?: string;
163
+ /** Updated context after tool execution */
164
+ contextUpdate?: Record<string, unknown>;
165
+ /** Updated data after tool execution */
166
+ dataUpdate?: Partial<TData>;
167
+ }
168
+
169
+ /**
170
+ * Internal status of a tracked tool in the executor queue
171
+ */
172
+ export type ToolStatus = 'queued' | 'executing' | 'completed' | 'yielded';
173
+
174
+ /**
175
+ * Internal type tracking the state of a queued or executing tool
176
+ */
177
+ export interface TrackedTool<TContext = unknown, TData = unknown> {
178
+ id: string;
179
+ toolCall: ToolCallRequest;
180
+ tool: EnhancedTool<TContext, TData>;
181
+ status: ToolStatus;
182
+ isConcurrencySafe: boolean;
183
+ promise?: Promise<void>;
184
+ results: ToolExecutionResult[];
185
+ pendingProgress: string[];
186
+ }
187
+
188
+ /**
189
+ * Extended tool interface with rich metadata for concurrency control,
190
+ * permission gating, input validation, and result size management.
191
+ *
192
+ * All additional methods/properties are optional — plain `Tool` objects
193
+ * remain fully compatible.
194
+ */
195
+ export interface EnhancedTool<
196
+ TContext = any,
197
+ TData = any,
198
+ TResult = any
199
+ > extends Tool<TContext, TData, TResult> {
200
+ /** Whether this tool is safe to run concurrently with other concurrent-safe tools */
201
+ isConcurrencySafe?(input?: Record<string, unknown>): boolean;
202
+ /** Whether this tool only reads data without side effects */
203
+ isReadOnly?(input?: Record<string, unknown>): boolean;
204
+ /** Whether this tool performs destructive/irreversible operations */
205
+ isDestructive?(input?: Record<string, unknown>): boolean;
206
+
207
+ /** How the tool responds to abort signals: 'cancel' = immediate abort, 'block' = allow completion */
208
+ interruptBehavior?(): 'cancel' | 'block';
209
+ /** Maximum characters for the tool result before truncation */
210
+ maxResultSizeChars?: number;
211
+
212
+ /** Validate input before execution */
213
+ validateInput?(
214
+ input: Record<string, unknown>,
215
+ context: ToolContext<TContext, TData>
216
+ ): Promise<ToolValidationResult> | ToolValidationResult;
217
+
218
+ /** Check permissions before execution */
219
+ checkPermissions?(
220
+ input: Record<string, unknown>,
221
+ context: ToolContext<TContext, TData>
222
+ ): Promise<ToolPermissionResult> | ToolPermissionResult;
223
+ }
224
+
225
+ // --- Existing tool configuration types ---
226
+
119
227
  /**
120
228
  * Configuration for data enrichment tools
121
229
  */