@falai/agent 1.1.3 → 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 +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 -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 +75 -16
  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 +4 -1
  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 -126
  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 +75 -16
  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 +4 -1
  156. package/src/core/CompactionEngine.ts +318 -0
  157. package/src/core/PromptComposer.ts +259 -156
  158. package/src/core/PromptSectionCache.ts +136 -0
  159. package/src/core/ResponseEngine.ts +9 -6
  160. package/src/core/ResponseModal.ts +77 -16
  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
@@ -0,0 +1,572 @@
1
+ /**
2
+ * StreamingToolExecutor - Executes tools as they arrive from the LLM stream
3
+ * with concurrency control, abort handling, and ordered result yielding.
4
+ *
5
+ * Concurrency invariant: at any point during execution, either all executing
6
+ * tools have `isConcurrencySafe === true`, or exactly one tool is executing
7
+ * with `isConcurrencySafe === false`.
8
+ *
9
+ * Results are yielded in the original request order (the order addTool was called).
10
+ * Progress messages bypass result ordering and are yielded immediately.
11
+ */
12
+
13
+ import log from "loglevel";
14
+ import type {
15
+ ToolCallRequest,
16
+ ToolExecutionUpdate,
17
+ ToolExecutionResult,
18
+ ToolContext,
19
+ EnhancedTool,
20
+ TrackedTool,
21
+ } from "../types/tool";
22
+
23
+ /** Options for the StreamingToolExecutor */
24
+ interface StreamingToolExecutorOptions {
25
+ /** Maximum number of tools executing in parallel (default: 10) */
26
+ maxParallel?: number;
27
+ /** Parent abort signal — cancels 'cancel' tools, lets 'block' tools finish, stops queue */
28
+ signal?: AbortSignal;
29
+ }
30
+
31
+ /**
32
+ * A deferred promise that can be resolved/rejected externally.
33
+ * Used to notify getRemainingResults when new results are available.
34
+ */
35
+ interface Deferred<T> {
36
+ promise: Promise<T>;
37
+ resolve: (value: T) => void;
38
+ reject: (reason?: unknown) => void;
39
+ }
40
+
41
+ function createDeferred<T>(): Deferred<T> {
42
+ let resolve!: (value: T) => void;
43
+ let reject!: (reason?: unknown) => void;
44
+ const promise = new Promise<T>((res, rej) => {
45
+ resolve = res;
46
+ reject = rej;
47
+ });
48
+ return { promise, resolve, reject };
49
+ }
50
+
51
+
52
+ /**
53
+ * StreamingToolExecutor manages queuing, concurrent execution, abort propagation,
54
+ * and ordered result yielding for tool calls arriving from an LLM stream.
55
+ */
56
+ export class StreamingToolExecutor<TContext = unknown, TData = unknown> {
57
+ /** Ordered list of tracked tools (insertion order = request order) */
58
+ private tools: TrackedTool<TContext, TData>[] = [];
59
+
60
+ /** Tool context passed to each tool handler */
61
+ private toolContext: ToolContext<TContext, TData>;
62
+
63
+ /** Maximum concurrent tool executions */
64
+ private maxParallel: number;
65
+
66
+ /** Parent abort signal */
67
+ private parentSignal?: AbortSignal;
68
+
69
+ /** Whether the executor has been discarded (no new tools will be processed) */
70
+ private discarded = false;
71
+
72
+ /** Accumulated context updates from completed tools */
73
+ private contextUpdates: Partial<TContext> = {};
74
+
75
+ /**
76
+ * Sibling abort controller for the current concurrent batch.
77
+ * When one tool in a batch fails, this aborts all siblings.
78
+ */
79
+ private siblingAbortController: AbortController | null = null;
80
+
81
+ /**
82
+ * Set of tool IDs in the current concurrent batch.
83
+ * Reset when a new batch starts.
84
+ */
85
+ private currentBatchIds: Set<string> = new Set();
86
+
87
+ /**
88
+ * Deferred used to wake up getRemainingResults when state changes
89
+ * (tool completes, progress arrives, or executor finishes).
90
+ */
91
+ private waiter: Deferred<void> | null = null;
92
+
93
+ /**
94
+ * @param toolContext - Context provided to each tool handler during execution
95
+ * @param options - Optional configuration for max parallelism and abort signal
96
+ */
97
+ constructor(
98
+ toolContext: ToolContext<TContext, TData>,
99
+ options?: StreamingToolExecutorOptions
100
+ ) {
101
+ this.toolContext = toolContext;
102
+ this.maxParallel = options?.maxParallel ?? 10;
103
+ this.parentSignal = options?.signal;
104
+
105
+ // Listen to parent abort signal
106
+ if (this.parentSignal) {
107
+ this.parentSignal.addEventListener("abort", () => this.handleParentAbort(), { once: true });
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Queue a tool for execution. The tool's `isConcurrencySafe` is evaluated
113
+ * once at queue time and cached on the TrackedTool.
114
+ *
115
+ * If the executor has been discarded, the tool is ignored.
116
+ */
117
+ addTool(
118
+ toolCall: ToolCallRequest,
119
+ tool: EnhancedTool<TContext, TData>
120
+ ): void {
121
+ if (this.discarded) {
122
+ log.warn(`[StreamingToolExecutor] Executor discarded, ignoring tool: ${toolCall.toolName}`);
123
+ return;
124
+ }
125
+
126
+ // Evaluate concurrency safety once at queue time (Req 2.3)
127
+ const isConcurrencySafe = tool.isConcurrencySafe
128
+ ? tool.isConcurrencySafe(toolCall.arguments)
129
+ : false; // Default: not concurrency-safe (Req 2.2, 10.2)
130
+
131
+ const tracked: TrackedTool<TContext, TData> = {
132
+ id: toolCall.id,
133
+ toolCall,
134
+ tool,
135
+ status: "queued",
136
+ isConcurrencySafe,
137
+ results: [],
138
+ pendingProgress: [],
139
+ };
140
+
141
+ this.tools.push(tracked);
142
+ log.debug(
143
+ `[StreamingToolExecutor] Queued tool ${toolCall.toolName} (id=${toolCall.id}, concurrencySafe=${isConcurrencySafe})`
144
+ );
145
+
146
+ // Kick the queue
147
+ this.processQueue();
148
+ }
149
+
150
+ /**
151
+ * Process the tool queue, starting eligible tools while maintaining
152
+ * the concurrency invariant.
153
+ *
154
+ * Invariant: all executing tools are concurrency-safe OR exactly one
155
+ * non-safe tool is executing. Additionally, the number of concurrently
156
+ * executing tools never exceeds `maxParallel`.
157
+ */
158
+ private processQueue(): void {
159
+ if (this.discarded) return;
160
+ if (this.parentSignal?.aborted) return;
161
+
162
+ for (const tool of this.tools) {
163
+ if (tool.status !== "queued") continue;
164
+
165
+ const executing = this.tools.filter((t) => t.status === "executing");
166
+
167
+ // Enforce max parallel limit (Req 14.1, 14.2)
168
+ if (executing.length >= this.maxParallel) {
169
+ break;
170
+ }
171
+
172
+ const canExecute =
173
+ executing.length === 0 ||
174
+ (tool.isConcurrencySafe && executing.every((t) => t.isConcurrencySafe));
175
+
176
+ if (canExecute) {
177
+ // Start a new concurrent batch if needed
178
+ if (executing.length === 0 || !this.currentBatchIds.has(executing[0]?.id)) {
179
+ this.startNewBatch();
180
+ }
181
+
182
+ tool.status = "executing";
183
+ this.currentBatchIds.add(tool.id);
184
+
185
+ log.debug(
186
+ `[StreamingToolExecutor] Starting tool ${tool.toolCall.toolName} (id=${tool.id})`
187
+ );
188
+
189
+ tool.promise = this.executeTool(tool).finally(() => {
190
+ // Re-evaluate queue after each tool completes
191
+ this.processQueue();
192
+ // Wake up any waiting async generator
193
+ this.notifyWaiter();
194
+ });
195
+ } else if (!tool.isConcurrencySafe) {
196
+ // Non-concurrent tool must wait — stop scanning to maintain order (Req 1.3, 1.4)
197
+ break;
198
+ }
199
+ // If tool is concurrency-safe but can't run (non-safe tool executing),
200
+ // skip it and continue scanning — but actually per the design,
201
+ // we should also break here since we need to maintain request order
202
+ // for result yielding. A concurrent-safe tool behind a non-safe tool
203
+ // should wait too.
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Start a new sibling abort controller for a concurrent batch.
209
+ */
210
+ private startNewBatch(): void {
211
+ this.siblingAbortController = new AbortController();
212
+ this.currentBatchIds = new Set();
213
+ }
214
+
215
+ /**
216
+ * Execute a single tool, handling result capture, progress, truncation,
217
+ * and error/abort propagation.
218
+ */
219
+ private async executeTool(tracked: TrackedTool<TContext, TData>): Promise<void> {
220
+ const { tool, toolCall } = tracked;
221
+ const batchAbortController = this.siblingAbortController;
222
+
223
+ try {
224
+ // Create a combined abort signal from parent + sibling
225
+ const toolAbortController = new AbortController();
226
+ const abortTool = () => {
227
+ const behavior = tool.interruptBehavior
228
+ ? tool.interruptBehavior()
229
+ : "block"; // Default: block (Req 3.4, 10.5)
230
+
231
+ if (behavior === "cancel") {
232
+ toolAbortController.abort();
233
+ }
234
+ // 'block' tools are allowed to complete
235
+ };
236
+
237
+ // Listen to sibling abort
238
+ if (batchAbortController) {
239
+ batchAbortController.signal.addEventListener("abort", abortTool, { once: true });
240
+ }
241
+
242
+ // Listen to parent abort for this specific tool
243
+ const parentAbortHandler = () => abortTool();
244
+ if (this.parentSignal) {
245
+ this.parentSignal.addEventListener("abort", parentAbortHandler, { once: true });
246
+ }
247
+
248
+ // Execute the tool handler
249
+ const result = await tool.handler(this.toolContext, toolCall.arguments);
250
+
251
+ // Clean up abort listeners
252
+ if (batchAbortController) {
253
+ batchAbortController.signal.removeEventListener("abort", abortTool);
254
+ }
255
+ if (this.parentSignal) {
256
+ this.parentSignal.removeEventListener("abort", parentAbortHandler);
257
+ }
258
+
259
+ // Check if we were aborted during execution
260
+ if (toolAbortController.signal.aborted) {
261
+ tracked.results.push({
262
+ success: false,
263
+ error: "Tool execution was cancelled",
264
+ metadata: { toolId: tool.id, cancelled: true },
265
+ });
266
+ tracked.status = "completed";
267
+ return;
268
+ }
269
+
270
+ // Normalize the result
271
+ const executionResult = this.normalizeResult(result, tool);
272
+
273
+ // Apply per-tool maxResultSizeChars truncation (Req 9.4)
274
+ const truncatedResult = this.applyResultTruncation(executionResult, tool);
275
+
276
+ tracked.results.push(truncatedResult);
277
+
278
+ // Accumulate context updates
279
+ if (truncatedResult.contextUpdate) {
280
+ this.contextUpdates = {
281
+ ...this.contextUpdates,
282
+ ...truncatedResult.contextUpdate,
283
+ } as Partial<TContext>;
284
+ }
285
+
286
+ tracked.status = "completed";
287
+
288
+ // If the tool failed, abort siblings in the batch (Req 3.1)
289
+ if (!truncatedResult.success) {
290
+ this.abortSiblings(tracked.id, batchAbortController);
291
+ }
292
+ } catch (error) {
293
+ const errorMessage = error instanceof Error ? error.message : String(error);
294
+ log.error(
295
+ `[StreamingToolExecutor] Tool ${toolCall.toolName} (id=${toolCall.id}) failed: ${errorMessage}`
296
+ );
297
+
298
+ tracked.results.push({
299
+ success: false,
300
+ error: errorMessage,
301
+ metadata: { toolId: tool.id },
302
+ });
303
+ tracked.status = "completed";
304
+
305
+ // Abort siblings on failure (Req 3.1)
306
+ this.abortSiblings(tracked.id, batchAbortController);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Normalize a tool handler return value into a ToolExecutionResult.
312
+ */
313
+ private normalizeResult(
314
+ result: unknown,
315
+ tool: EnhancedTool<TContext, TData>
316
+ ): ToolExecutionResult {
317
+ if (
318
+ result &&
319
+ typeof result === "object" &&
320
+ ("data" in result || "success" in result || "error" in result)
321
+ ) {
322
+ const r = result as Record<string, unknown>;
323
+ return {
324
+ success: r.success !== false,
325
+ data: r.data,
326
+ error: r.error as string | undefined,
327
+ contextUpdate: r.contextUpdate as Record<string, unknown> | undefined,
328
+ dataUpdate: r.dataUpdate as Record<string, unknown> | undefined,
329
+ metadata: {
330
+ toolId: tool.id,
331
+ ...(r.meta as Record<string, unknown> | undefined),
332
+ },
333
+ };
334
+ }
335
+
336
+ return {
337
+ success: true,
338
+ data: result,
339
+ metadata: { toolId: tool.id },
340
+ };
341
+ }
342
+
343
+ /**
344
+ * Apply per-tool maxResultSizeChars truncation to a result.
345
+ * If the stringified result data exceeds the limit, truncate with a notice.
346
+ */
347
+ private applyResultTruncation(
348
+ result: ToolExecutionResult,
349
+ tool: EnhancedTool<TContext, TData>
350
+ ): ToolExecutionResult {
351
+ const maxChars = tool.maxResultSizeChars;
352
+ if (maxChars == null || maxChars <= 0 || result.data == null) {
353
+ return result;
354
+ }
355
+
356
+ const serialized =
357
+ typeof result.data === "string"
358
+ ? result.data
359
+ : JSON.stringify(result.data);
360
+
361
+ if (serialized.length <= maxChars) {
362
+ return result;
363
+ }
364
+
365
+ const truncated = serialized.slice(0, maxChars);
366
+ const notice = `\n\n[Truncated: ${serialized.length} chars total, showing first ${maxChars}]`;
367
+
368
+ return {
369
+ ...result,
370
+ data: truncated + notice,
371
+ metadata: {
372
+ ...result.metadata,
373
+ truncated: true,
374
+ originalLength: serialized.length,
375
+ },
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Abort sibling tools in the current concurrent batch when one tool fails.
381
+ * Tools with interruptBehavior 'cancel' are immediately aborted;
382
+ * tools with 'block' are allowed to complete.
383
+ */
384
+ private abortSiblings(
385
+ failedToolId: string,
386
+ batchAbortController: AbortController | null
387
+ ): void {
388
+ if (!batchAbortController) return;
389
+
390
+ log.warn(
391
+ `[StreamingToolExecutor] Tool ${failedToolId} failed, aborting sibling tools in batch`
392
+ );
393
+
394
+ // Fire the sibling abort signal — individual tool handlers
395
+ // check their interruptBehavior to decide whether to cancel or block
396
+ batchAbortController.abort();
397
+ }
398
+
399
+ /**
400
+ * Handle parent AbortSignal firing.
401
+ * Cancel 'cancel' tools immediately, let 'block' tools finish,
402
+ * and stop processing new queued tools.
403
+ */
404
+ private handleParentAbort(): void {
405
+ log.warn("[StreamingToolExecutor] Parent abort signal received");
406
+
407
+ // Stop processing new tools
408
+ this.discarded = true;
409
+
410
+ // Abort the current batch — individual tools respect their interruptBehavior
411
+ if (this.siblingAbortController) {
412
+ this.siblingAbortController.abort();
413
+ }
414
+
415
+ // Mark remaining queued tools as completed with cancellation error
416
+ for (const tool of this.tools) {
417
+ if (tool.status === "queued") {
418
+ tool.results.push({
419
+ success: false,
420
+ error: "Execution cancelled by parent abort signal",
421
+ metadata: { toolId: tool.tool.id, cancelled: true },
422
+ });
423
+ tool.status = "completed";
424
+ }
425
+ }
426
+
427
+ // Wake up any waiting async generator
428
+ this.notifyWaiter();
429
+ }
430
+
431
+ /**
432
+ * Yield completed results in original request order.
433
+ * Progress messages are yielded immediately regardless of order.
434
+ *
435
+ * This is a synchronous generator — it yields what's available now
436
+ * without waiting for pending tools.
437
+ */
438
+ *getCompletedResults(): Generator<ToolExecutionUpdate<TData>> {
439
+ for (const tool of this.tools) {
440
+ // Always yield pending progress immediately (Req 4.1)
441
+ while (tool.pendingProgress.length > 0) {
442
+ yield {
443
+ toolCallId: tool.id,
444
+ progress: tool.pendingProgress.shift()!,
445
+ };
446
+ }
447
+
448
+ if (tool.status === "yielded") continue;
449
+
450
+ if (tool.status === "completed") {
451
+ tool.status = "yielded";
452
+ for (const result of tool.results) {
453
+ yield {
454
+ toolCallId: tool.id,
455
+ result,
456
+ contextUpdate: result.contextUpdate,
457
+ dataUpdate: result.dataUpdate as Partial<TData> | undefined,
458
+ };
459
+ }
460
+ } else {
461
+ // Tool is still queued or executing — stop yielding results
462
+ // to maintain request order, but continue yielding progress
463
+ // for subsequent tools
464
+ break;
465
+ }
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Async generator that yields all results (completed and pending) in
471
+ * original request order. Waits for pending tools to complete.
472
+ *
473
+ * Progress messages are yielded immediately as they arrive.
474
+ */
475
+ async *getRemainingResults(): AsyncGenerator<ToolExecutionUpdate<TData>> {
476
+ let yieldIndex = 0;
477
+
478
+ while (yieldIndex < this.tools.length || this.hasUnfinishedTools()) {
479
+ // Yield progress from all tools (bypass ordering)
480
+ for (const tool of this.tools) {
481
+ while (tool.pendingProgress.length > 0) {
482
+ yield {
483
+ toolCallId: tool.id,
484
+ progress: tool.pendingProgress.shift()!,
485
+ };
486
+ }
487
+ }
488
+
489
+ // Yield completed results in order
490
+ while (yieldIndex < this.tools.length) {
491
+ const tool = this.tools[yieldIndex];
492
+
493
+ if (tool.status === "yielded") {
494
+ yieldIndex++;
495
+ continue;
496
+ }
497
+
498
+ if (tool.status === "completed") {
499
+ tool.status = "yielded";
500
+ for (const result of tool.results) {
501
+ yield {
502
+ toolCallId: tool.id,
503
+ result,
504
+ contextUpdate: result.contextUpdate,
505
+ dataUpdate: result.dataUpdate as Partial<TData> | undefined,
506
+ };
507
+ }
508
+ yieldIndex++;
509
+ } else {
510
+ // Tool is still queued or executing — wait for it
511
+ break;
512
+ }
513
+ }
514
+
515
+ // If there are still unfinished tools, wait for a state change
516
+ if (yieldIndex < this.tools.length && this.hasUnfinishedTools()) {
517
+ await this.waitForStateChange();
518
+ } else {
519
+ break;
520
+ }
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Stop processing new queued tools. Already-executing tools continue
526
+ * to completion based on their interruptBehavior.
527
+ */
528
+ discard(): void {
529
+ log.info("[StreamingToolExecutor] Executor discarded, stopping queue processing");
530
+ this.discarded = true;
531
+ this.notifyWaiter();
532
+ }
533
+
534
+ /**
535
+ * Return the accumulated context updates from all completed tools.
536
+ */
537
+ getUpdatedContext(): TContext {
538
+ return {
539
+ ...this.toolContext.context,
540
+ ...this.contextUpdates,
541
+ } as TContext;
542
+ }
543
+
544
+ /**
545
+ * Check if there are any tools that haven't completed yet.
546
+ */
547
+ hasUnfinishedTools(): boolean {
548
+ return this.tools.some(
549
+ (t) => t.status === "queued" || t.status === "executing"
550
+ );
551
+ }
552
+
553
+ /**
554
+ * Wait for a state change (tool completion, progress, or discard).
555
+ * Uses a deferred promise pattern.
556
+ */
557
+ private waitForStateChange(): Promise<void> {
558
+ this.waiter = createDeferred<void>();
559
+ return this.waiter.promise;
560
+ }
561
+
562
+ /**
563
+ * Notify the waiting async generator that state has changed.
564
+ */
565
+ private notifyWaiter(): void {
566
+ if (this.waiter) {
567
+ const w = this.waiter;
568
+ this.waiter = null;
569
+ w.resolve();
570
+ }
571
+ }
572
+ }