zeitlich 0.1.1 → 0.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 (58) hide show
  1. package/README.md +165 -180
  2. package/dist/index.cjs +1314 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +128 -0
  5. package/dist/index.d.ts +51 -75
  6. package/dist/index.js +741 -1091
  7. package/dist/index.js.map +1 -1
  8. package/dist/workflow-uVNF7zoe.d.cts +941 -0
  9. package/dist/workflow-uVNF7zoe.d.ts +941 -0
  10. package/dist/workflow.cjs +914 -0
  11. package/dist/workflow.cjs.map +1 -0
  12. package/dist/workflow.d.cts +5 -0
  13. package/dist/workflow.d.ts +2 -1
  14. package/dist/workflow.js +543 -423
  15. package/dist/workflow.js.map +1 -1
  16. package/package.json +19 -17
  17. package/src/activities.ts +112 -0
  18. package/src/index.ts +49 -0
  19. package/src/lib/fs.ts +80 -0
  20. package/src/lib/model-invoker.ts +75 -0
  21. package/src/lib/session.ts +216 -0
  22. package/src/lib/state-manager.ts +268 -0
  23. package/src/lib/thread-manager.ts +169 -0
  24. package/src/lib/tool-router.ts +717 -0
  25. package/src/lib/types.ts +354 -0
  26. package/src/plugin.ts +28 -0
  27. package/src/tools/ask-user-question/handler.ts +25 -0
  28. package/src/tools/ask-user-question/tool.ts +46 -0
  29. package/src/tools/bash/bash.test.ts +104 -0
  30. package/src/tools/bash/handler.ts +36 -0
  31. package/src/tools/bash/tool.ts +20 -0
  32. package/src/tools/edit/handler.ts +156 -0
  33. package/src/tools/edit/tool.ts +39 -0
  34. package/src/tools/glob/handler.ts +62 -0
  35. package/src/tools/glob/tool.ts +27 -0
  36. package/src/tools/grep/tool.ts +45 -0
  37. package/src/tools/read/tool.ts +33 -0
  38. package/src/tools/task/handler.ts +75 -0
  39. package/src/tools/task/tool.ts +96 -0
  40. package/src/tools/task-create/handler.ts +49 -0
  41. package/src/tools/task-create/tool.ts +66 -0
  42. package/src/tools/task-get/handler.ts +38 -0
  43. package/src/tools/task-get/tool.ts +11 -0
  44. package/src/tools/task-list/handler.ts +33 -0
  45. package/src/tools/task-list/tool.ts +9 -0
  46. package/src/tools/task-update/handler.ts +79 -0
  47. package/src/tools/task-update/tool.ts +20 -0
  48. package/src/tools/write/tool.ts +26 -0
  49. package/src/workflow.ts +138 -0
  50. package/tsup.config.ts +20 -0
  51. package/dist/index.d.mts +0 -152
  52. package/dist/index.mjs +0 -1587
  53. package/dist/index.mjs.map +0 -1
  54. package/dist/workflow-7_MT-5-w.d.mts +0 -1203
  55. package/dist/workflow-7_MT-5-w.d.ts +0 -1203
  56. package/dist/workflow.d.mts +0 -4
  57. package/dist/workflow.mjs +0 -739
  58. package/dist/workflow.mjs.map +0 -1
@@ -0,0 +1,717 @@
1
+ import type { MessageToolDefinition } from "@langchain/core/messages";
2
+ import type { ToolMessageContent } from "./thread-manager";
3
+ import type { Hooks, SubagentConfig, ToolResultConfig } from "./types";
4
+
5
+ import type { z } from "zod";
6
+ import { proxyActivities } from "@temporalio/workflow";
7
+ import type { ZeitlichSharedActivities } from "../activities";
8
+ import { createTaskTool } from "../tools/task/tool";
9
+ import { createTaskHandler } from "../tools/task/handler";
10
+ import type { createTaskCreateHandler } from "../tools/task-create/handler";
11
+ import { bashTool, createBashToolDescription } from "../tools/bash/tool";
12
+ import { taskCreateTool } from "../tools/task-create/tool";
13
+ import type { handleBashTool } from "../tools/bash/handler";
14
+
15
+ export type { ToolMessageContent };
16
+
17
+ // ============================================================================
18
+ // Tool Definition Types (merged from tool-registry.ts)
19
+ // ============================================================================
20
+
21
+ /**
22
+ * A tool definition with a name, description, and Zod schema for arguments.
23
+ * Does not include a handler - use ToolWithHandler for tools with handlers.
24
+ */
25
+ export interface ToolDefinition<
26
+ TName extends string = string,
27
+ TSchema extends z.ZodType = z.ZodType,
28
+ > {
29
+ name: TName;
30
+ description: string;
31
+ schema: TSchema;
32
+ strict?: boolean;
33
+ max_uses?: number;
34
+ }
35
+
36
+ /**
37
+ * A tool definition with an integrated handler function.
38
+ * This is the primary type for defining tools in the router.
39
+ */
40
+ export interface ToolWithHandler<
41
+ TName extends string = string,
42
+ TSchema extends z.ZodType = z.ZodType,
43
+ TResult = unknown,
44
+ TContext = ToolHandlerContext,
45
+ > {
46
+ name: TName;
47
+ description: string;
48
+ schema: TSchema;
49
+ handler: ToolHandler<z.infer<TSchema>, TResult, TContext>;
50
+ strict?: boolean;
51
+ max_uses?: number;
52
+ }
53
+
54
+ /**
55
+ * A map of tool keys to tool definitions with handlers.
56
+ */
57
+ export type ToolMap = Record<
58
+ string,
59
+ {
60
+ name: string;
61
+ description: string;
62
+ schema: z.ZodType;
63
+ /* eslint-disable @typescript-eslint/no-explicit-any */
64
+ handler: (
65
+ args: any,
66
+ context: any
67
+ ) => ToolHandlerResponse<any> | Promise<ToolHandlerResponse<any>>;
68
+ /* eslint-enable @typescript-eslint/no-explicit-any */
69
+ strict?: boolean;
70
+ max_uses?: number;
71
+ }
72
+ >;
73
+
74
+ /**
75
+ * Converts a ToolMap to MessageStructure-compatible tools type.
76
+ * Maps each tool's name to a MessageToolDefinition with inferred input type from the schema.
77
+ */
78
+ export type ToolMapToMessageTools<T extends ToolMap> = {
79
+ [K in keyof T as T[K]["name"]]: MessageToolDefinition<
80
+ z.infer<T[K]["schema"]>
81
+ >;
82
+ };
83
+
84
+ /**
85
+ * Extract the tool names from a tool map (uses the tool's name property, not the key).
86
+ */
87
+ export type ToolNames<T extends ToolMap> = T[keyof T]["name"];
88
+
89
+ /**
90
+ * A raw tool call as received from the LLM before parsing.
91
+ */
92
+ export interface RawToolCall {
93
+ id?: string;
94
+ name: string;
95
+ args: unknown;
96
+ }
97
+
98
+ /**
99
+ * A parsed tool call with validated arguments for a specific tool.
100
+ */
101
+ export interface ParsedToolCall<
102
+ TName extends string = string,
103
+ TArgs = unknown,
104
+ > {
105
+ id: string;
106
+ name: TName;
107
+ args: TArgs;
108
+ }
109
+
110
+ /**
111
+ * Union type of all possible parsed tool calls from a tool map.
112
+ */
113
+ export type ParsedToolCallUnion<T extends ToolMap> = {
114
+ [K in keyof T]: ParsedToolCall<T[K]["name"], z.infer<T[K]["schema"]>>;
115
+ }[keyof T];
116
+
117
+ // ============================================================================
118
+ // Handler Types
119
+ // ============================================================================
120
+
121
+ /**
122
+ * Function signature for appending tool results to a thread.
123
+ */
124
+ export type AppendToolResultFn = (config: ToolResultConfig) => Promise<void>;
125
+
126
+ /**
127
+ * The response from a tool handler.
128
+ * Contains the content for the tool message and the result to return from processToolCalls.
129
+ */
130
+ export interface ToolHandlerResponse<TResult> {
131
+ /** Content for the tool message added to the thread */
132
+ content: ToolMessageContent;
133
+ /** Result returned from processToolCalls */
134
+ result: TResult;
135
+ }
136
+
137
+ /**
138
+ * Context passed to tool handlers for additional data beyond tool args.
139
+ * Use this to pass workflow state like file trees, user context, etc.
140
+ */
141
+ export interface ToolHandlerContext {
142
+ /** Additional context data - define your own shape */
143
+ [key: string]: unknown;
144
+ }
145
+
146
+ export interface BuildInToolDefinitions {
147
+ [bashTool.name]: typeof bashTool & {
148
+ handler: ReturnType<typeof handleBashTool>;
149
+ };
150
+ [taskCreateTool.name]: typeof taskCreateTool & {
151
+ handler: ReturnType<typeof createTaskCreateHandler>;
152
+ };
153
+ }
154
+
155
+ export const buildIntoolDefinitions: Record<
156
+ keyof BuildInToolDefinitions,
157
+ ToolDefinition
158
+ > = {
159
+ [bashTool.name]: bashTool,
160
+ [taskCreateTool.name]: taskCreateTool,
161
+ };
162
+
163
+ /**
164
+ * A handler function for a specific tool.
165
+ * Receives the parsed args and context, returns a response with content and result.
166
+ * Context always has a value (defaults to empty object if not provided).
167
+ */
168
+ export type ToolHandler<TArgs, TResult, TContext = ToolHandlerContext> = (
169
+ args: TArgs,
170
+ context: TContext
171
+ ) => ToolHandlerResponse<TResult> | Promise<ToolHandlerResponse<TResult>>;
172
+
173
+ /**
174
+ * Activity-compatible tool handler that always returns a Promise.
175
+ * Use this for tool handlers registered as Temporal activities.
176
+ * Context always has a value (defaults to empty object if not provided).
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * // Filesystem handler with context
181
+ * const readHandler: ActivityToolHandler<
182
+ * ReadToolSchemaType,
183
+ * ReadResult,
184
+ * { scopedNodes: FileNode[]; provider: FileSystemProvider }
185
+ * > = async (args, context) => {
186
+ * return readHandler(args, context.scopedNodes, context.provider);
187
+ * };
188
+ * ```
189
+ */
190
+ export type ActivityToolHandler<
191
+ TArgs,
192
+ TResult,
193
+ TContext = ToolHandlerContext,
194
+ > = (args: TArgs, context: TContext) => Promise<ToolHandlerResponse<TResult>>;
195
+
196
+ /**
197
+ * Extract the args type for a specific tool name from a tool map.
198
+ */
199
+ export type ToolArgs<T extends ToolMap, TName extends ToolNames<T>> = z.infer<
200
+ Extract<T[keyof T], { name: TName }>["schema"]
201
+ >;
202
+
203
+ /**
204
+ * Extract the result type for a specific tool name from a tool map.
205
+ */
206
+ export type ToolResult<T extends ToolMap, TName extends ToolNames<T>> =
207
+ Extract<T[keyof T], { name: TName }>["handler"] extends ToolHandler<
208
+ unknown,
209
+ infer R,
210
+ unknown
211
+ >
212
+ ? Awaited<R>
213
+ : never;
214
+
215
+ // ============================================================================
216
+ // Tool Call Result Types
217
+ // ============================================================================
218
+
219
+ /**
220
+ * The result of processing a tool call.
221
+ */
222
+ export interface ToolCallResult<
223
+ TName extends string = string,
224
+ TResult = unknown,
225
+ > {
226
+ toolCallId: string;
227
+ name: TName;
228
+ result: TResult;
229
+ }
230
+
231
+ /**
232
+ * Options for creating a tool router.
233
+ */
234
+ export interface ToolRouterOptions<T extends ToolMap> {
235
+ /** File tree for the agent */
236
+ fileTree: string;
237
+ /** Map of tools with their handlers */
238
+ tools: T;
239
+ /** Thread ID for appending tool results */
240
+ threadId: string;
241
+ /** Function to append tool results to the thread (called automatically after each handler) */
242
+ appendToolResult: AppendToolResultFn;
243
+ /** Whether to process tools in parallel (default: true) */
244
+ parallel?: boolean;
245
+ /** Lifecycle hooks for tool execution */
246
+ hooks?: Hooks<T, ToolCallResultUnion<InferToolResults<T>>>;
247
+ /** Subagent configurations */
248
+ subagents?: SubagentConfig[];
249
+ /** Build in tools - accepts raw handlers or proxied activities */
250
+ buildInTools?: {
251
+ [K in keyof BuildInToolDefinitions]?: BuildInToolDefinitions[K]["handler"];
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Infer result types from a tool map based on handler return types.
257
+ */
258
+ export type InferToolResults<T extends ToolMap> = {
259
+ /* eslint-disable @typescript-eslint/no-explicit-any */
260
+ [K in keyof T as T[K]["name"]]: T[K]["handler"] extends ToolHandler<
261
+ any,
262
+ infer R,
263
+ any
264
+ >
265
+ ? /* eslint-enable @typescript-eslint/no-explicit-any */
266
+ Awaited<R>
267
+ : never;
268
+ };
269
+
270
+ /**
271
+ * Union of all possible tool call results based on handler return types.
272
+ */
273
+ export type ToolCallResultUnion<TResults extends Record<string, unknown>> = {
274
+ [TName in keyof TResults & string]: ToolCallResult<TName, TResults[TName]>;
275
+ }[keyof TResults & string];
276
+
277
+ /**
278
+ * Context passed to processToolCalls for hook execution and handler invocation
279
+ */
280
+ export interface ProcessToolCallsContext<THandlerContext = ToolHandlerContext> {
281
+ /** Current turn number (for hooks) */
282
+ turn?: number;
283
+ /** Context passed to each tool handler (scopedNodes, provider, etc.) */
284
+ handlerContext?: THandlerContext;
285
+ }
286
+
287
+ // ============================================================================
288
+ // Router Interface
289
+ // ============================================================================
290
+
291
+ /**
292
+ * The tool router interface with full type inference for both args and results.
293
+ */
294
+ export interface ToolRouter<T extends ToolMap> {
295
+ /** Check if the router has any tools */
296
+ hasTools(): boolean;
297
+ // --- Methods from registry ---
298
+
299
+ /**
300
+ * Parse and validate a raw tool call against the router's tools.
301
+ * Returns a typed tool call with validated arguments.
302
+ */
303
+ parseToolCall(toolCall: RawToolCall): ParsedToolCallUnion<T>;
304
+
305
+ /**
306
+ * Check if a tool with the given name exists in the router.
307
+ */
308
+ hasTool(name: string): boolean;
309
+
310
+ /**
311
+ * Get all tool names in the router.
312
+ */
313
+ getToolNames(): ToolNames<T>[];
314
+
315
+ /**
316
+ * Get all tool definitions (without handlers) for passing to LLM.
317
+ */
318
+ getToolDefinitions(): ToolDefinition[];
319
+
320
+ // --- Methods for processing tool calls ---
321
+
322
+ /**
323
+ * Process all tool calls using the registered handlers.
324
+ * Returns typed results based on handler return types.
325
+ * @param toolCalls - Array of parsed tool calls to process
326
+ * @param context - Optional context including turn number for hooks
327
+ */
328
+ processToolCalls(
329
+ toolCalls: ParsedToolCallUnion<T>[],
330
+ context?: ProcessToolCallsContext
331
+ ): Promise<ToolCallResultUnion<InferToolResults<T>>[]>;
332
+
333
+ /**
334
+ * Process tool calls matching a specific name with a custom handler.
335
+ * Useful for overriding the default handler for specific cases.
336
+ */
337
+ processToolCallsByName<
338
+ TName extends ToolNames<T>,
339
+ TResult,
340
+ TContext = ToolHandlerContext,
341
+ >(
342
+ toolCalls: ParsedToolCallUnion<T>[],
343
+ toolName: TName,
344
+ handler: ToolHandler<ToolArgs<T, TName>, TResult, TContext>,
345
+ context?: ProcessToolCallsContext<TContext>
346
+ ): Promise<ToolCallResult<TName, TResult>[]>;
347
+
348
+ // --- Utility methods ---
349
+
350
+ /**
351
+ * Filter tool calls by name.
352
+ */
353
+ filterByName<TName extends ToolNames<T>>(
354
+ toolCalls: ParsedToolCallUnion<T>[],
355
+ name: TName
356
+ ): ParsedToolCall<TName, ToolArgs<T, TName>>[];
357
+
358
+ /**
359
+ * Check if any tool call matches the given name.
360
+ */
361
+ hasToolCall(toolCalls: ParsedToolCallUnion<T>[], name: ToolNames<T>): boolean;
362
+
363
+ /**
364
+ * Filter results by tool name.
365
+ */
366
+ getResultsByName<TName extends ToolNames<T>>(
367
+ results: ToolCallResultUnion<InferToolResults<T>>[],
368
+ name: TName
369
+ ): ToolCallResult<TName, ToolResult<T, TName>>[];
370
+ }
371
+
372
+ // ============================================================================
373
+ // Router Factory
374
+ // ============================================================================
375
+
376
+ /**
377
+ * Creates a tool router for declarative tool call processing.
378
+ * Combines tool definitions with handlers in a single API.
379
+ *
380
+ * @example
381
+ * ```typescript
382
+ * const router = createToolRouter({
383
+ * threadId,
384
+ * tools: {
385
+ * Read: {
386
+ * name: "FileRead",
387
+ * description: "Read file contents",
388
+ * schema: z.object({ path: z.string() }),
389
+ * handler: async (args, ctx) => ({
390
+ * content: `Read ${args.path}`,
391
+ * result: { path: args.path, content: "..." },
392
+ * }),
393
+ * },
394
+ * },
395
+ * hooks: { onPreToolUse, onPostToolUse },
396
+ * });
397
+ *
398
+ * // Parse raw tool calls from LLM
399
+ * const parsed = router.parseToolCall(rawToolCall);
400
+ *
401
+ * // Process tool calls
402
+ * const results = await router.processToolCalls([parsed]);
403
+ * ```
404
+ */
405
+ export function createToolRouter<T extends ToolMap>(
406
+ options: ToolRouterOptions<T>
407
+ ): ToolRouter<T> {
408
+ const { appendToolResult } = proxyActivities<ZeitlichSharedActivities>({
409
+ startToCloseTimeout: "2m",
410
+ retry: {
411
+ maximumAttempts: 3,
412
+ initialInterval: "5s",
413
+ maximumInterval: "15m",
414
+ backoffCoefficient: 4,
415
+ },
416
+ });
417
+ type TResults = InferToolResults<T>;
418
+
419
+ // Build internal lookup map by tool name
420
+ // Use ToolMap's value type to allow both user tools and the dynamic Task tool
421
+ const toolMap = new Map<string, ToolMap[string]>();
422
+ for (const [_key, tool] of Object.entries(options.tools)) {
423
+ toolMap.set(tool.name, tool as T[keyof T]);
424
+ }
425
+
426
+ if (options.subagents) {
427
+ toolMap.set("Task", {
428
+ ...createTaskTool(options.subagents),
429
+ handler: createTaskHandler(options.subagents),
430
+ });
431
+ }
432
+
433
+ if (options.buildInTools) {
434
+ for (const [key, value] of Object.entries(options.buildInTools)) {
435
+ if (key === bashTool.name) {
436
+ toolMap.set(key, {
437
+ ...buildIntoolDefinitions[key as keyof BuildInToolDefinitions],
438
+ description: createBashToolDescription({
439
+ fileTree: options.fileTree,
440
+ }),
441
+ handler: value,
442
+ });
443
+ } else {
444
+ toolMap.set(key, {
445
+ ...buildIntoolDefinitions[key as keyof BuildInToolDefinitions],
446
+ handler: value,
447
+ });
448
+ }
449
+ }
450
+ }
451
+
452
+ async function processToolCall(
453
+ toolCall: ParsedToolCallUnion<T>,
454
+ turn: number,
455
+ handlerContext?: ToolHandlerContext
456
+ ): Promise<ToolCallResultUnion<TResults> | null> {
457
+ const startTime = Date.now();
458
+
459
+ // PreToolUse hook - can skip or modify args
460
+ let effectiveArgs: unknown = toolCall.args;
461
+ if (options.hooks?.onPreToolUse) {
462
+ const preResult = await options.hooks.onPreToolUse({
463
+ toolCall,
464
+ threadId: options.threadId,
465
+ turn,
466
+ });
467
+ if (preResult?.skip) {
468
+ // Skip this tool call - append a skip message and return null
469
+ await appendToolResult({
470
+ threadId: options.threadId,
471
+ toolCallId: toolCall.id,
472
+ content: JSON.stringify({
473
+ skipped: true,
474
+ reason: "Skipped by PreToolUse hook",
475
+ }),
476
+ });
477
+ return null;
478
+ }
479
+ if (preResult?.modifiedArgs !== undefined) {
480
+ effectiveArgs = preResult.modifiedArgs;
481
+ }
482
+ }
483
+
484
+ const tool = toolMap.get(toolCall.name);
485
+ let result: unknown;
486
+ let content: ToolMessageContent;
487
+
488
+ try {
489
+ if (tool) {
490
+ const response = await tool.handler(
491
+ effectiveArgs as Parameters<typeof tool.handler>[0],
492
+ (handlerContext ?? {}) as Parameters<typeof tool.handler>[1]
493
+ );
494
+ result = response.result;
495
+ content = response.content;
496
+ } else {
497
+ result = { error: `Unknown tool: ${toolCall.name}` };
498
+ content = JSON.stringify(result, null, 2);
499
+ }
500
+ } catch (error) {
501
+ // PostToolUseFailure hook - can recover from errors
502
+ if (options.hooks?.onPostToolUseFailure) {
503
+ const failureResult = await options.hooks.onPostToolUseFailure({
504
+ toolCall,
505
+ error: error instanceof Error ? error : new Error(String(error)),
506
+ threadId: options.threadId,
507
+ turn,
508
+ });
509
+ if (failureResult?.fallbackContent !== undefined) {
510
+ content = failureResult.fallbackContent;
511
+ result = { error: String(error), recovered: true };
512
+ } else if (failureResult?.suppress) {
513
+ content = JSON.stringify({ error: String(error), suppressed: true });
514
+ result = { error: String(error), suppressed: true };
515
+ } else {
516
+ throw error;
517
+ }
518
+ } else {
519
+ throw error;
520
+ }
521
+ }
522
+
523
+ // Automatically append tool result to thread
524
+ await appendToolResult({
525
+ threadId: options.threadId,
526
+ toolCallId: toolCall.id,
527
+ content,
528
+ });
529
+
530
+ const toolResult = {
531
+ toolCallId: toolCall.id,
532
+ name: toolCall.name,
533
+ result,
534
+ } as ToolCallResultUnion<TResults>;
535
+
536
+ // PostToolUse hook - called after successful execution
537
+ if (options.hooks?.onPostToolUse) {
538
+ const durationMs = Date.now() - startTime;
539
+ await options.hooks.onPostToolUse({
540
+ toolCall,
541
+ result: toolResult,
542
+ threadId: options.threadId,
543
+ turn,
544
+ durationMs,
545
+ });
546
+ }
547
+
548
+ return toolResult;
549
+ }
550
+
551
+ return {
552
+ // --- Methods from registry ---
553
+
554
+ hasTools(): boolean {
555
+ return toolMap.size > 0;
556
+ },
557
+
558
+ parseToolCall(toolCall: RawToolCall): ParsedToolCallUnion<T> {
559
+ const tool = toolMap.get(toolCall.name);
560
+
561
+ if (!tool) {
562
+ throw new Error(`Tool ${toolCall.name} not found`);
563
+ }
564
+
565
+ // Parse and validate args using the tool's schema
566
+ const parsedArgs = tool.schema.parse(toolCall.args);
567
+
568
+ return {
569
+ id: toolCall.id ?? "",
570
+ name: toolCall.name,
571
+ args: parsedArgs,
572
+ } as ParsedToolCallUnion<T>;
573
+ },
574
+
575
+ hasTool(name: string): boolean {
576
+ return toolMap.has(name);
577
+ },
578
+
579
+ getToolNames(): ToolNames<T>[] {
580
+ return Array.from(toolMap.keys()) as ToolNames<T>[];
581
+ },
582
+
583
+ getToolDefinitions(): ToolDefinition[] {
584
+ return Array.from(toolMap).map(([name, tool]) => ({
585
+ name,
586
+ description: tool.description,
587
+ schema: tool.schema,
588
+ strict: tool.strict,
589
+ max_uses: tool.max_uses,
590
+ }));
591
+ },
592
+
593
+ // --- Methods for processing tool calls ---
594
+
595
+ async processToolCalls(
596
+ toolCalls: ParsedToolCallUnion<T>[],
597
+ context?: ProcessToolCallsContext
598
+ ): Promise<ToolCallResultUnion<TResults>[]> {
599
+ if (toolCalls.length === 0) {
600
+ return [];
601
+ }
602
+
603
+ const turn = context?.turn ?? 0;
604
+ const handlerContext = context?.handlerContext;
605
+
606
+ if (options.parallel) {
607
+ const results = await Promise.all(
608
+ toolCalls.map((tc) => processToolCall(tc, turn, handlerContext))
609
+ );
610
+ // Filter out null results (skipped tool calls)
611
+ return results.filter(
612
+ (r): r is NonNullable<typeof r> => r !== null
613
+ ) as ToolCallResultUnion<TResults>[];
614
+ }
615
+
616
+ // Sequential processing
617
+ const results: ToolCallResultUnion<TResults>[] = [];
618
+ for (const toolCall of toolCalls) {
619
+ const result = await processToolCall(toolCall, turn, handlerContext);
620
+ if (result !== null) {
621
+ results.push(result);
622
+ }
623
+ }
624
+ return results;
625
+ },
626
+
627
+ async processToolCallsByName<
628
+ TName extends ToolNames<T>,
629
+ TResult,
630
+ TContext = ToolHandlerContext,
631
+ >(
632
+ toolCalls: ParsedToolCallUnion<T>[],
633
+ toolName: TName,
634
+ handler: ToolHandler<ToolArgs<T, TName>, TResult, TContext>,
635
+ context?: ProcessToolCallsContext<TContext>
636
+ ): Promise<ToolCallResult<TName, TResult>[]> {
637
+ const matchingCalls = toolCalls.filter((tc) => tc.name === toolName);
638
+
639
+ if (matchingCalls.length === 0) {
640
+ return [];
641
+ }
642
+
643
+ const handlerContext = (context?.handlerContext ?? {}) as TContext;
644
+
645
+ const processOne = async (
646
+ toolCall: ParsedToolCallUnion<T>
647
+ ): Promise<ToolCallResult<TName, TResult>> => {
648
+ const response = await handler(
649
+ toolCall.args as ToolArgs<T, TName>,
650
+ handlerContext
651
+ );
652
+
653
+ // Automatically append tool result to thread
654
+ await appendToolResult({
655
+ threadId: options.threadId,
656
+ toolCallId: toolCall.id,
657
+ content: response.content,
658
+ });
659
+
660
+ return {
661
+ toolCallId: toolCall.id,
662
+ name: toolCall.name as TName,
663
+ result: response.result,
664
+ };
665
+ };
666
+
667
+ if (options.parallel) {
668
+ return Promise.all(matchingCalls.map(processOne));
669
+ }
670
+
671
+ const results: ToolCallResult<TName, TResult>[] = [];
672
+ for (const toolCall of matchingCalls) {
673
+ results.push(await processOne(toolCall));
674
+ }
675
+ return results;
676
+ },
677
+
678
+ // --- Utility methods ---
679
+
680
+ filterByName<TName extends ToolNames<T>>(
681
+ toolCalls: ParsedToolCallUnion<T>[],
682
+ name: TName
683
+ ): ParsedToolCall<TName, ToolArgs<T, TName>>[] {
684
+ return toolCalls.filter(
685
+ (tc): tc is ParsedToolCall<TName, ToolArgs<T, TName>> =>
686
+ tc.name === name
687
+ );
688
+ },
689
+
690
+ hasToolCall(
691
+ toolCalls: ParsedToolCallUnion<T>[],
692
+ name: ToolNames<T>
693
+ ): boolean {
694
+ return toolCalls.some((tc) => tc.name === name);
695
+ },
696
+
697
+ getResultsByName<TName extends ToolNames<T>>(
698
+ results: ToolCallResultUnion<TResults>[],
699
+ name: TName
700
+ ): ToolCallResult<TName, ToolResult<T, TName>>[] {
701
+ return results.filter((r) => r.name === name) as ToolCallResult<
702
+ TName,
703
+ ToolResult<T, TName>
704
+ >[];
705
+ },
706
+ };
707
+ }
708
+
709
+ /**
710
+ * Utility to check if there were no tool calls besides a specific one
711
+ */
712
+ export function hasNoOtherToolCalls<T extends ToolMap>(
713
+ toolCalls: ParsedToolCallUnion<T>[],
714
+ excludeName: ToolNames<T>
715
+ ): boolean {
716
+ return toolCalls.filter((tc) => tc.name !== excludeName).length === 0;
717
+ }