zeitlich 0.2.13 → 0.2.14

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 (135) hide show
  1. package/README.md +49 -38
  2. package/dist/adapters/sandbox/daytona/index.cjs +205 -0
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -0
  4. package/dist/adapters/sandbox/daytona/index.d.cts +86 -0
  5. package/dist/adapters/sandbox/daytona/index.d.ts +86 -0
  6. package/dist/adapters/sandbox/daytona/index.js +202 -0
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -0
  8. package/dist/adapters/sandbox/inmemory/index.cjs +174 -0
  9. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -0
  10. package/dist/adapters/sandbox/inmemory/index.d.cts +28 -0
  11. package/dist/adapters/sandbox/inmemory/index.d.ts +28 -0
  12. package/dist/adapters/sandbox/inmemory/index.js +172 -0
  13. package/dist/adapters/sandbox/inmemory/index.js.map +1 -0
  14. package/dist/adapters/sandbox/virtual/index.cjs +405 -0
  15. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -0
  16. package/dist/adapters/sandbox/virtual/index.d.cts +85 -0
  17. package/dist/adapters/sandbox/virtual/index.d.ts +85 -0
  18. package/dist/adapters/sandbox/virtual/index.js +400 -0
  19. package/dist/adapters/sandbox/virtual/index.js.map +1 -0
  20. package/dist/adapters/thread/google-genai/index.cjs +284 -0
  21. package/dist/adapters/thread/google-genai/index.cjs.map +1 -0
  22. package/dist/adapters/thread/google-genai/index.d.cts +145 -0
  23. package/dist/adapters/thread/google-genai/index.d.ts +145 -0
  24. package/dist/adapters/thread/google-genai/index.js +278 -0
  25. package/dist/adapters/thread/google-genai/index.js.map +1 -0
  26. package/dist/adapters/{langchain → thread/langchain}/index.cjs +7 -9
  27. package/dist/adapters/thread/langchain/index.cjs.map +1 -0
  28. package/dist/adapters/{langchain → thread/langchain}/index.d.cts +17 -21
  29. package/dist/adapters/{langchain → thread/langchain}/index.d.ts +17 -21
  30. package/dist/adapters/{langchain → thread/langchain}/index.js +7 -9
  31. package/dist/adapters/thread/langchain/index.js.map +1 -0
  32. package/dist/index.cjs +816 -545
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +235 -74
  35. package/dist/index.d.ts +235 -74
  36. package/dist/index.js +804 -540
  37. package/dist/index.js.map +1 -1
  38. package/dist/types-B4C9txdq.d.ts +389 -0
  39. package/dist/{thread-manager-qc0g5Rvd.d.cts → types-B9ljZewB.d.cts} +1 -6
  40. package/dist/{thread-manager-qc0g5Rvd.d.ts → types-B9ljZewB.d.ts} +1 -6
  41. package/dist/types-BMXzv7TN.d.cts +476 -0
  42. package/dist/types-BMXzv7TN.d.ts +476 -0
  43. package/dist/types-BVP87m_W.d.cts +121 -0
  44. package/dist/types-CDubRtad.d.cts +115 -0
  45. package/dist/types-CDubRtad.d.ts +115 -0
  46. package/dist/types-CwwgQ_9H.d.ts +121 -0
  47. package/dist/types-GpMU4b0w.d.cts +389 -0
  48. package/dist/workflow.cjs +444 -318
  49. package/dist/workflow.cjs.map +1 -1
  50. package/dist/workflow.d.cts +271 -222
  51. package/dist/workflow.d.ts +271 -222
  52. package/dist/workflow.js +440 -316
  53. package/dist/workflow.js.map +1 -1
  54. package/package.json +59 -6
  55. package/src/adapters/sandbox/daytona/filesystem.ts +136 -0
  56. package/src/adapters/sandbox/daytona/index.ts +149 -0
  57. package/src/adapters/sandbox/daytona/types.ts +34 -0
  58. package/src/adapters/sandbox/inmemory/index.ts +213 -0
  59. package/src/adapters/sandbox/virtual/filesystem.ts +345 -0
  60. package/src/adapters/sandbox/virtual/index.ts +88 -0
  61. package/src/adapters/sandbox/virtual/mutations.ts +38 -0
  62. package/src/adapters/sandbox/virtual/provider.ts +101 -0
  63. package/src/adapters/sandbox/virtual/tree.ts +82 -0
  64. package/src/adapters/sandbox/virtual/types.ts +127 -0
  65. package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +523 -0
  66. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +91 -0
  67. package/src/adapters/thread/google-genai/activities.ts +121 -0
  68. package/src/adapters/thread/google-genai/index.ts +41 -0
  69. package/src/adapters/thread/google-genai/model-invoker.ts +154 -0
  70. package/src/adapters/thread/google-genai/thread-manager.ts +169 -0
  71. package/src/adapters/{langchain → thread/langchain}/activities.ts +11 -15
  72. package/src/adapters/{langchain → thread/langchain}/index.ts +1 -1
  73. package/src/adapters/{langchain → thread/langchain}/model-invoker.ts +15 -18
  74. package/src/adapters/{langchain → thread/langchain}/thread-manager.ts +1 -1
  75. package/src/index.ts +32 -24
  76. package/src/lib/activity.ts +87 -0
  77. package/src/lib/hooks/index.ts +11 -0
  78. package/src/lib/hooks/types.ts +98 -0
  79. package/src/lib/model/helpers.ts +6 -0
  80. package/src/lib/model/index.ts +13 -0
  81. package/src/lib/{model-invoker.ts → model/types.ts} +18 -1
  82. package/src/lib/sandbox/index.ts +19 -0
  83. package/src/lib/sandbox/manager.ts +76 -0
  84. package/src/lib/sandbox/sandbox.test.ts +158 -0
  85. package/src/lib/{fs.ts → sandbox/tree.ts} +6 -6
  86. package/src/lib/sandbox/types.ts +164 -0
  87. package/src/lib/session/index.ts +11 -0
  88. package/src/lib/{session.ts → session/session.ts} +76 -48
  89. package/src/lib/session/types.ts +93 -0
  90. package/src/lib/skills/fs-provider.ts +16 -15
  91. package/src/lib/skills/handler.ts +31 -0
  92. package/src/lib/skills/index.ts +5 -1
  93. package/src/lib/skills/register.ts +20 -0
  94. package/src/lib/skills/tool.ts +47 -0
  95. package/src/lib/state/index.ts +9 -0
  96. package/src/lib/{state-manager.ts → state/manager.ts} +10 -147
  97. package/src/lib/state/types.ts +134 -0
  98. package/src/lib/subagent/define.ts +71 -0
  99. package/src/lib/subagent/handler.ts +99 -0
  100. package/src/lib/subagent/index.ts +13 -0
  101. package/src/lib/subagent/register.ts +53 -0
  102. package/src/lib/subagent/tool.ts +80 -0
  103. package/src/lib/subagent/types.ts +92 -0
  104. package/src/lib/thread/index.ts +7 -0
  105. package/src/lib/{thread-manager.ts → thread/manager.ts} +1 -33
  106. package/src/lib/thread/types.ts +33 -0
  107. package/src/lib/tool-router/auto-append.ts +55 -0
  108. package/src/lib/tool-router/index.ts +41 -0
  109. package/src/lib/tool-router/router.ts +462 -0
  110. package/src/lib/tool-router/types.ts +478 -0
  111. package/src/lib/tool-router/with-sandbox.ts +70 -0
  112. package/src/lib/types.ts +5 -382
  113. package/src/tools/bash/bash.test.ts +53 -55
  114. package/src/tools/bash/handler.ts +23 -51
  115. package/src/tools/edit/handler.ts +67 -81
  116. package/src/tools/glob/handler.ts +60 -17
  117. package/src/tools/read-file/handler.ts +67 -0
  118. package/src/tools/read-skill/handler.ts +1 -31
  119. package/src/tools/read-skill/tool.ts +5 -47
  120. package/src/tools/subagent/handler.ts +1 -100
  121. package/src/tools/subagent/tool.ts +5 -93
  122. package/src/tools/task-create/handler.ts +1 -1
  123. package/src/tools/task-get/handler.ts +1 -1
  124. package/src/tools/task-list/handler.ts +1 -1
  125. package/src/tools/task-update/handler.ts +1 -1
  126. package/src/tools/write-file/handler.ts +47 -0
  127. package/src/workflow.ts +88 -47
  128. package/tsup.config.ts +8 -1
  129. package/dist/adapters/langchain/index.cjs.map +0 -1
  130. package/dist/adapters/langchain/index.js.map +0 -1
  131. package/dist/model-invoker-y_zlyMqu.d.cts +0 -892
  132. package/dist/model-invoker-y_zlyMqu.d.ts +0 -892
  133. package/src/lib/tool-router.ts +0 -977
  134. package/src/lib/workflow-helpers.ts +0 -50
  135. /package/src/lib/{thread-id.ts → thread/id.ts} +0 -0
@@ -1,977 +0,0 @@
1
- import type {
2
- ToolMessageContent,
3
- Hooks,
4
- PostToolUseFailureHookResult,
5
- PreToolUseHookResult,
6
- SubagentConfig,
7
- SubagentHooks,
8
- TokenUsage,
9
- ToolHooks,
10
- ToolResultConfig,
11
- } from "./types";
12
- import type { Skill } from "./skills/types";
13
- import type { SubagentArgs } from "../tools/subagent/tool";
14
-
15
- import type { z } from "zod";
16
- import { createSubagentTool, SUBAGENT_TOOL_NAME } from "../tools/subagent/tool";
17
- import { createSubagentHandler } from "../tools/subagent/handler";
18
- import {
19
- createReadSkillTool,
20
- READ_SKILL_TOOL_NAME,
21
- } from "../tools/read-skill/tool";
22
- import { createReadSkillHandler } from "../tools/read-skill/handler";
23
- import { ApplicationFailure } from "@temporalio/workflow";
24
-
25
- // ============================================================================
26
- // Tool Definition Types (merged from tool-registry.ts)
27
- // ============================================================================
28
-
29
- /**
30
- * A tool definition with a name, description, and Zod schema for arguments.
31
- * Does not include a handler - use ToolWithHandler for tools with handlers.
32
- */
33
- export interface ToolDefinition<
34
- TName extends string = string,
35
- TSchema extends z.ZodType = z.ZodType,
36
- > {
37
- name: TName;
38
- description: string;
39
- schema: TSchema;
40
- strict?: boolean;
41
- max_uses?: number;
42
- }
43
-
44
- /**
45
- * A tool definition with an integrated handler function.
46
- * This is the primary type for defining tools in the router.
47
- */
48
- export interface ToolWithHandler<
49
- TName extends string = string,
50
- TSchema extends z.ZodType = z.ZodType,
51
- TResult = unknown,
52
- TContext = ToolHandlerContext,
53
- > {
54
- name: TName;
55
- description: string;
56
- schema: TSchema;
57
- handler: ToolHandler<z.infer<TSchema>, TResult, TContext>;
58
- strict?: boolean;
59
- max_uses?: number;
60
- /** Whether this tool is available to the agent (default: true). Disabled tools are excluded from definitions and rejected at parse time. */
61
- enabled?: boolean;
62
- /** Per-tool lifecycle hooks (run in addition to global hooks) */
63
- hooks?: ToolHooks<z.infer<TSchema>, TResult>;
64
- }
65
-
66
- /**
67
- * A map of tool keys to tool definitions with handlers.
68
- *
69
- * Handler uses `any` intentionally — this is a type-system boundary where heterogeneous
70
- * tool types are stored together. Type safety for individual tools is enforced by
71
- * `defineTool()` at the definition site and generic inference utilities like
72
- * `InferToolResults<T>` at the consumption site.
73
- */
74
- export type ToolMap = Record<
75
- string,
76
- {
77
- name: string;
78
- description: string;
79
- schema: z.ZodType;
80
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
- handler: ToolHandler<any, any, any>;
82
- strict?: boolean;
83
- max_uses?: number;
84
- enabled?: boolean;
85
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
- hooks?: ToolHooks<any, any>;
87
- }
88
- >;
89
-
90
- /**
91
- * Extract the tool names from a tool map (uses the tool's name property, not the key).
92
- */
93
- export type ToolNames<T extends ToolMap> = T[keyof T]["name"];
94
-
95
- /**
96
- * A raw tool call as received from the LLM before parsing.
97
- */
98
- export interface RawToolCall {
99
- id?: string;
100
- name: string;
101
- args: unknown;
102
- }
103
-
104
- /**
105
- * A parsed tool call with validated arguments for a specific tool.
106
- */
107
- export interface ParsedToolCall<
108
- TName extends string = string,
109
- TArgs = unknown,
110
- > {
111
- id: string;
112
- name: TName;
113
- args: TArgs;
114
- }
115
-
116
- /**
117
- * Union type of all possible parsed tool calls from a tool map.
118
- */
119
- export type ParsedToolCallUnion<T extends ToolMap> = {
120
- [K in keyof T]: ParsedToolCall<T[K]["name"], z.infer<T[K]["schema"]>>;
121
- }[keyof T];
122
-
123
- // ============================================================================
124
- // Handler Types
125
- // ============================================================================
126
-
127
- /**
128
- * Function signature for appending tool results to a thread.
129
- */
130
- export type AppendToolResultFn = (config: ToolResultConfig) => Promise<void>;
131
-
132
- /**
133
- * The response from a tool handler.
134
- * Contains the content for the tool message and the result to return from processToolCalls.
135
- *
136
- * Tools that don't return additional data should use `data: null` (TResult defaults to null).
137
- * Tools that may fail to produce data should type TResult as `SomeType | null`.
138
- */
139
- export interface ToolHandlerResponse<TResult = null> {
140
- /** Content sent back to the LLM as the tool call response */
141
- toolResponse: ToolMessageContent;
142
- /** Data returned to the workflow and hooks for further processing */
143
- data: TResult;
144
- /**
145
- * When true, the tool result has already been appended to the thread
146
- * by the handler itself (e.g. via `withAutoAppend`), so the router
147
- * will skip the `appendToolResult` call. This avoids sending large
148
- * payloads through Temporal's activity payload limit.
149
- */
150
- resultAppended?: boolean;
151
- /** Token usage from the tool execution (e.g. child agent invocations) */
152
- usage?: TokenUsage;
153
- /** Thread ID used by the handler (surfaced to the LLM for subagent thread continuation) */
154
- threadId?: string;
155
- }
156
-
157
- /**
158
- * Context passed to tool handlers for additional data beyond tool args.
159
- * Use this to pass workflow state like file trees, user context, etc.
160
- * Generic so callers can type the context shape, e.g. ToolHandlerContext<ControlTestFsParams>.
161
- */
162
- export type ToolHandlerContext<T = Record<string, unknown>> = T;
163
-
164
- /**
165
- * A handler function for a specific tool.
166
- * Receives the parsed args and context, returns a response with content and result.
167
- * Context always has a value (defaults to empty object if not provided).
168
- */
169
- export type ToolHandler<TArgs, TResult, TContext = ToolHandlerContext> = (
170
- args: TArgs,
171
- context: TContext
172
- ) => ToolHandlerResponse<TResult> | Promise<ToolHandlerResponse<TResult>>;
173
-
174
- /**
175
- * Activity-compatible tool handler that always returns a Promise.
176
- * Use this for tool handlers registered as Temporal activities.
177
- * Context always has a value (defaults to empty object if not provided).
178
- *
179
- * @example
180
- * ```typescript
181
- * // Filesystem handler with context
182
- * const readHandler: ActivityToolHandler<
183
- * FileReadArgs,
184
- * ReadResult,
185
- * { scopedNodes: FileNode[]; provider: FileSystemProvider }
186
- * > = async (args, context) => {
187
- * return readHandler(args, context.scopedNodes, context.provider);
188
- * };
189
- * ```
190
- */
191
- export type ActivityToolHandler<
192
- TArgs,
193
- TResult,
194
- TContext = ToolHandlerContext,
195
- > = (args: TArgs, context: TContext) => Promise<ToolHandlerResponse<TResult>>;
196
-
197
- /**
198
- * Extract the args type for a specific tool name from a tool map.
199
- */
200
- export type ToolArgs<T extends ToolMap, TName extends ToolNames<T>> = z.infer<
201
- Extract<T[keyof T], { name: TName }>["schema"]
202
- >;
203
-
204
- /**
205
- * Extract the result type for a specific tool name from a tool map.
206
- */
207
- export type ToolResult<T extends ToolMap, TName extends ToolNames<T>> =
208
- Extract<T[keyof T], { name: TName }>["handler"] extends ToolHandler<
209
- unknown,
210
- infer R,
211
- unknown
212
- >
213
- ? Awaited<R>
214
- : never;
215
-
216
- // ============================================================================
217
- // Tool Call Result Types
218
- // ============================================================================
219
-
220
- /**
221
- * The result of processing a tool call.
222
- */
223
- export interface ToolCallResult<
224
- TName extends string = string,
225
- TResult = unknown,
226
- > {
227
- toolCallId: string;
228
- name: TName;
229
- data: TResult;
230
- usage?: TokenUsage;
231
- }
232
-
233
- /**
234
- * Options for creating a tool router.
235
- */
236
- export interface ToolRouterOptions<T extends ToolMap> {
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
- /** Skills available to the agent (auto-adds ReadSkill tool when non-empty) */
250
- skills?: Skill[];
251
- }
252
-
253
- /**
254
- * Infer result types from a tool map based on handler return types.
255
- */
256
- export type InferToolResults<T extends ToolMap> = {
257
- /* eslint-disable @typescript-eslint/no-explicit-any */
258
- [K in keyof T as T[K]["name"]]: T[K]["handler"] extends ToolHandler<
259
- any,
260
- infer R,
261
- any
262
- >
263
- ? /* eslint-enable @typescript-eslint/no-explicit-any */
264
- Awaited<R>
265
- : never;
266
- };
267
-
268
- /**
269
- * Union of all possible tool call results based on handler return types.
270
- */
271
- export type ToolCallResultUnion<TResults extends Record<string, unknown>> = {
272
- [TName in keyof TResults & string]: ToolCallResult<TName, TResults[TName]>;
273
- }[keyof TResults & string];
274
-
275
- /**
276
- * Context passed to processToolCalls for hook execution and handler invocation
277
- */
278
- export interface ProcessToolCallsContext<THandlerContext = ToolHandlerContext> {
279
- /** Current turn number (for hooks) */
280
- turn?: number;
281
- /** Context passed to each tool handler (scopedNodes, provider, etc.) */
282
- handlerContext?: THandlerContext;
283
- }
284
-
285
- // ============================================================================
286
- // Router Interface
287
- // ============================================================================
288
-
289
- /**
290
- * The tool router interface with full type inference for both args and results.
291
- */
292
- export interface ToolRouter<T extends ToolMap> {
293
- /** Check if the router has any tools */
294
- hasTools(): boolean;
295
- // --- Methods from registry ---
296
-
297
- /**
298
- * Parse and validate a raw tool call against the router's tools.
299
- * Returns a typed tool call with validated arguments.
300
- */
301
- parseToolCall(toolCall: RawToolCall): ParsedToolCallUnion<T>;
302
-
303
- /**
304
- * Check if a tool with the given name exists in the router.
305
- */
306
- hasTool(name: string): boolean;
307
-
308
- /**
309
- * Get all tool names in the router.
310
- */
311
- getToolNames(): ToolNames<T>[];
312
-
313
- /**
314
- * Get all tool definitions (without handlers) for passing to LLM.
315
- */
316
- getToolDefinitions(): ToolDefinition[];
317
-
318
- // --- Methods for processing tool calls ---
319
-
320
- /**
321
- * Process all tool calls using the registered handlers.
322
- * Returns typed results based on handler return types.
323
- * @param toolCalls - Array of parsed tool calls to process
324
- * @param context - Optional context including turn number for hooks
325
- */
326
- processToolCalls(
327
- toolCalls: ParsedToolCallUnion<T>[],
328
- context?: ProcessToolCallsContext
329
- ): Promise<ToolCallResultUnion<InferToolResults<T>>[]>;
330
-
331
- /**
332
- * Process tool calls matching a specific name with a custom handler.
333
- * Useful for overriding the default handler for specific cases.
334
- */
335
- processToolCallsByName<
336
- TName extends ToolNames<T>,
337
- TResult,
338
- TContext = ToolHandlerContext,
339
- >(
340
- toolCalls: ParsedToolCallUnion<T>[],
341
- toolName: TName,
342
- handler: ToolHandler<ToolArgs<T, TName>, TResult, TContext>,
343
- context?: ProcessToolCallsContext<TContext>
344
- ): Promise<ToolCallResult<TName, TResult>[]>;
345
-
346
- // --- Utility methods ---
347
-
348
- /**
349
- * Filter tool calls by name.
350
- */
351
- filterByName<TName extends ToolNames<T>>(
352
- toolCalls: ParsedToolCallUnion<T>[],
353
- name: TName
354
- ): ParsedToolCall<TName, ToolArgs<T, TName>>[];
355
-
356
- /**
357
- * Check if any tool call matches the given name.
358
- */
359
- hasToolCall(toolCalls: ParsedToolCallUnion<T>[], name: ToolNames<T>): boolean;
360
-
361
- /**
362
- * Filter results by tool name.
363
- */
364
- getResultsByName<TName extends ToolNames<T>>(
365
- results: ToolCallResultUnion<InferToolResults<T>>[],
366
- name: TName
367
- ): ToolCallResult<TName, ToolResult<T, TName>>[];
368
- }
369
-
370
- // ============================================================================
371
- // Router Factory
372
- // ============================================================================
373
-
374
- /**
375
- * Creates a tool router for declarative tool call processing.
376
- * Combines tool definitions with handlers in a single API.
377
- *
378
- * @example
379
- * ```typescript
380
- * const router = createToolRouter({
381
- * threadId,
382
- * tools: {
383
- * Read: {
384
- * name: "FileRead",
385
- * description: "Read file contents",
386
- * schema: z.object({ path: z.string() }),
387
- * handler: async (args, ctx) => ({
388
- * content: `Read ${args.path}`,
389
- * result: { path: args.path, content: "..." },
390
- * }),
391
- * },
392
- * },
393
- * hooks: { onPreToolUse, onPostToolUse },
394
- * });
395
- *
396
- * // Parse raw tool calls from LLM
397
- * const parsed = router.parseToolCall(rawToolCall);
398
- *
399
- * // Process tool calls
400
- * const results = await router.processToolCalls([parsed]);
401
- * ```
402
- */
403
- export function createToolRouter<T extends ToolMap>(
404
- options: ToolRouterOptions<T>
405
- ): ToolRouter<T> {
406
- const { appendToolResult } = options;
407
- type TResults = InferToolResults<T>;
408
-
409
- // Build internal lookup map by tool name
410
- // Use ToolMap's value type to allow both user tools and the dynamic Task tool
411
- const toolMap = new Map<string, ToolMap[string]>();
412
- for (const [_key, tool] of Object.entries(options.tools)) {
413
- toolMap.set(tool.name, tool as T[keyof T]);
414
- }
415
-
416
- /** Check if a tool is enabled (defaults to true when not specified) */
417
- const isEnabled = (tool: ToolMap[string] | SubagentConfig): boolean =>
418
- tool.enabled ?? true;
419
-
420
- if (options.subagents) {
421
- if (options.subagents.length > 0) {
422
- // Build per-subagent hook dispatcher keyed by subagent name
423
- const subagentHooksMap = new Map<string, SubagentHooks>();
424
- for (const s of options.subagents) {
425
- if (s.hooks) subagentHooksMap.set(s.agentName, s.hooks);
426
- }
427
-
428
- const resolveSubagentName = (args: unknown): string =>
429
- (args as SubagentArgs).subagent;
430
-
431
- toolMap.set(SUBAGENT_TOOL_NAME, {
432
- ...createSubagentTool(options.subagents),
433
- handler: createSubagentHandler(options.subagents),
434
- ...(subagentHooksMap.size > 0 && {
435
- hooks: {
436
- onPreToolUse: async (ctx): Promise<PreToolUseHookResult> => {
437
- const hooks = subagentHooksMap.get(resolveSubagentName(ctx.args));
438
- return hooks?.onPreExecution?.(ctx) ?? {};
439
- },
440
- onPostToolUse: async (ctx): Promise<void> => {
441
- const hooks = subagentHooksMap.get(resolveSubagentName(ctx.args));
442
- await hooks?.onPostExecution?.(ctx);
443
- },
444
- onPostToolUseFailure: async (
445
- ctx
446
- ): Promise<PostToolUseFailureHookResult> => {
447
- const hooks = subagentHooksMap.get(resolveSubagentName(ctx.args));
448
- return hooks?.onExecutionFailure?.(ctx) ?? {};
449
- },
450
- } satisfies ToolHooks,
451
- }),
452
- });
453
- }
454
- }
455
-
456
- if (options.skills && options.skills.length > 0) {
457
- toolMap.set(READ_SKILL_TOOL_NAME, {
458
- ...createReadSkillTool(options.skills),
459
- handler: createReadSkillHandler(options.skills),
460
- });
461
- }
462
-
463
- async function processToolCall(
464
- toolCall: ParsedToolCallUnion<T>,
465
- turn: number,
466
- handlerContext?: ToolHandlerContext
467
- ): Promise<ToolCallResultUnion<TResults> | null> {
468
- const startTime = Date.now();
469
- const tool = toolMap.get(toolCall.name);
470
- const toolHooks = tool?.hooks;
471
-
472
- // --- PreToolUse: global then per-tool ---
473
- let effectiveArgs: unknown = toolCall.args;
474
- if (options.hooks?.onPreToolUse) {
475
- const preResult = await options.hooks.onPreToolUse({
476
- toolCall,
477
- threadId: options.threadId,
478
- turn,
479
- });
480
- if (preResult?.skip) {
481
- await appendToolResult({
482
- threadId: options.threadId,
483
- toolCallId: toolCall.id,
484
- toolName: toolCall.name,
485
- content: JSON.stringify({
486
- skipped: true,
487
- reason: "Skipped by PreToolUse hook",
488
- }),
489
- });
490
- return null;
491
- }
492
- if (preResult?.modifiedArgs !== undefined) {
493
- effectiveArgs = preResult.modifiedArgs;
494
- }
495
- }
496
- if (toolHooks?.onPreToolUse) {
497
- const preResult = await toolHooks.onPreToolUse({
498
- args: effectiveArgs,
499
- threadId: options.threadId,
500
- turn,
501
- });
502
- if (preResult?.skip) {
503
- await appendToolResult({
504
- threadId: options.threadId,
505
- toolCallId: toolCall.id,
506
- toolName: toolCall.name,
507
- content: JSON.stringify({
508
- skipped: true,
509
- reason: "Skipped by tool PreToolUse hook",
510
- }),
511
- });
512
- return null;
513
- }
514
- if (preResult?.modifiedArgs !== undefined) {
515
- effectiveArgs = preResult.modifiedArgs;
516
- }
517
- }
518
-
519
- // --- Execute handler ---
520
- let result: unknown;
521
- let content!: ToolMessageContent;
522
- let resultAppended = false;
523
-
524
- try {
525
- if (tool) {
526
- const enrichedContext = {
527
- ...(handlerContext ?? {}),
528
- threadId: options.threadId,
529
- toolCallId: toolCall.id,
530
- toolName: toolCall.name,
531
- };
532
- const response = await tool.handler(
533
- effectiveArgs as Parameters<typeof tool.handler>[0],
534
- enrichedContext as Parameters<typeof tool.handler>[1]
535
- );
536
- result = response.data;
537
- content = response.toolResponse;
538
- resultAppended = response.resultAppended === true;
539
- } else {
540
- result = { error: `Unknown tool: ${toolCall.name}` };
541
- content = JSON.stringify(result, null, 2);
542
- }
543
- } catch (error) {
544
- // --- PostToolUseFailure: per-tool then global ---
545
- const err = error instanceof Error ? error : new Error(String(error));
546
- let recovered = false;
547
-
548
- if (toolHooks?.onPostToolUseFailure) {
549
- const failureResult = await toolHooks.onPostToolUseFailure({
550
- args: effectiveArgs,
551
- error: err,
552
- threadId: options.threadId,
553
- turn,
554
- });
555
- if (failureResult?.fallbackContent !== undefined) {
556
- content = failureResult.fallbackContent;
557
- result = { error: String(error), recovered: true };
558
- recovered = true;
559
- } else if (failureResult?.suppress) {
560
- content = JSON.stringify({ error: String(error), suppressed: true });
561
- result = { error: String(error), suppressed: true };
562
- recovered = true;
563
- }
564
- }
565
-
566
- if (!recovered && options.hooks?.onPostToolUseFailure) {
567
- const failureResult = await options.hooks.onPostToolUseFailure({
568
- toolCall,
569
- error: err,
570
- threadId: options.threadId,
571
- turn,
572
- });
573
- if (failureResult?.fallbackContent !== undefined) {
574
- content = failureResult.fallbackContent;
575
- result = { error: String(error), recovered: true };
576
- recovered = true;
577
- } else if (failureResult?.suppress) {
578
- content = JSON.stringify({ error: String(error), suppressed: true });
579
- result = { error: String(error), suppressed: true };
580
- recovered = true;
581
- }
582
- }
583
-
584
- if (!recovered) {
585
- throw ApplicationFailure.fromError(error, {
586
- nonRetryable: true,
587
- });
588
- }
589
- }
590
-
591
- // Automatically append tool result to thread (unless handler already did)
592
- if (!resultAppended) {
593
- await appendToolResult({
594
- threadId: options.threadId,
595
- toolCallId: toolCall.id,
596
- toolName: toolCall.name,
597
- content,
598
- });
599
- }
600
-
601
- const toolResult = {
602
- toolCallId: toolCall.id,
603
- name: toolCall.name,
604
- data: result,
605
- } as ToolCallResultUnion<TResults>;
606
-
607
- // --- PostToolUse: per-tool then global ---
608
- const durationMs = Date.now() - startTime;
609
- if (toolHooks?.onPostToolUse) {
610
- await toolHooks.onPostToolUse({
611
- args: effectiveArgs,
612
- result: result,
613
- threadId: options.threadId,
614
- turn,
615
- durationMs,
616
- });
617
- }
618
- if (options.hooks?.onPostToolUse) {
619
- await options.hooks.onPostToolUse({
620
- toolCall,
621
- result: toolResult,
622
- threadId: options.threadId,
623
- turn,
624
- durationMs,
625
- });
626
- }
627
-
628
- return toolResult;
629
- }
630
-
631
- return {
632
- // --- Methods from registry ---
633
-
634
- hasTools(): boolean {
635
- return Array.from(toolMap.values()).some(isEnabled);
636
- },
637
-
638
- parseToolCall(toolCall: RawToolCall): ParsedToolCallUnion<T> {
639
- const tool = toolMap.get(toolCall.name);
640
-
641
- if (!tool || !isEnabled(tool)) {
642
- throw new Error(`Tool ${toolCall.name} not found`);
643
- }
644
-
645
- // Parse and validate args using the tool's schema
646
- const parsedArgs = tool.schema.parse(toolCall.args);
647
-
648
- return {
649
- id: toolCall.id ?? "",
650
- name: toolCall.name,
651
- args: parsedArgs,
652
- } as ParsedToolCallUnion<T>;
653
- },
654
-
655
- hasTool(name: string): boolean {
656
- const tool = toolMap.get(name);
657
- return tool !== undefined && isEnabled(tool);
658
- },
659
-
660
- getToolNames(): ToolNames<T>[] {
661
- return Array.from(toolMap.entries())
662
- .filter(([, tool]) => isEnabled(tool))
663
- .map(([name]) => name) as ToolNames<T>[];
664
- },
665
-
666
- getToolDefinitions(): ToolDefinition[] {
667
- const activeSubagents =
668
- options.subagents?.filter((subagent) => isEnabled(subagent)) ?? [];
669
- const activeSkills = options.skills ?? [];
670
-
671
- return [
672
- ...Array.from(toolMap)
673
- .filter(
674
- ([, tool]) =>
675
- isEnabled(tool) &&
676
- tool.name !== SUBAGENT_TOOL_NAME &&
677
- tool.name !== READ_SKILL_TOOL_NAME
678
- )
679
- .map(([name, tool]) => ({
680
- name,
681
- description: tool.description,
682
- schema: tool.schema,
683
- strict: tool.strict,
684
- max_uses: tool.max_uses,
685
- })),
686
- ...(activeSubagents.length > 0
687
- ? [createSubagentTool(activeSubagents)]
688
- : []),
689
- ...(activeSkills.length > 0 ? [createReadSkillTool(activeSkills)] : []),
690
- ];
691
- },
692
-
693
- // --- Methods for processing tool calls ---
694
-
695
- async processToolCalls(
696
- toolCalls: ParsedToolCallUnion<T>[],
697
- context?: ProcessToolCallsContext
698
- ): Promise<ToolCallResultUnion<TResults>[]> {
699
- if (toolCalls.length === 0) {
700
- return [];
701
- }
702
-
703
- const turn = context?.turn ?? 0;
704
- const handlerContext = context?.handlerContext;
705
-
706
- if (options.parallel) {
707
- const results = await Promise.all(
708
- toolCalls.map((tc) => processToolCall(tc, turn, handlerContext))
709
- );
710
- // Filter out null results (skipped tool calls)
711
- return results.filter(
712
- (r): r is NonNullable<typeof r> => r !== null
713
- ) as ToolCallResultUnion<TResults>[];
714
- }
715
-
716
- // Sequential processing
717
- const results: ToolCallResultUnion<TResults>[] = [];
718
- for (const toolCall of toolCalls) {
719
- const result = await processToolCall(toolCall, turn, handlerContext);
720
- if (result !== null) {
721
- results.push(result);
722
- }
723
- }
724
- return results;
725
- },
726
-
727
- async processToolCallsByName<
728
- TName extends ToolNames<T>,
729
- TResult,
730
- TContext = ToolHandlerContext,
731
- >(
732
- toolCalls: ParsedToolCallUnion<T>[],
733
- toolName: TName,
734
- handler: ToolHandler<ToolArgs<T, TName>, TResult, TContext>,
735
- context?: ProcessToolCallsContext<TContext>
736
- ): Promise<ToolCallResult<TName, TResult>[]> {
737
- const matchingCalls = toolCalls.filter((tc) => tc.name === toolName);
738
-
739
- if (matchingCalls.length === 0) {
740
- return [];
741
- }
742
-
743
- const handlerContext = (context?.handlerContext ?? {}) as TContext;
744
-
745
- const processOne = async (
746
- toolCall: ParsedToolCallUnion<T>
747
- ): Promise<ToolCallResult<TName, TResult>> => {
748
- const enrichedContext = {
749
- ...(handlerContext ?? {}),
750
- threadId: options.threadId,
751
- toolCallId: toolCall.id,
752
- toolName: toolCall.name as TName,
753
- } as TContext;
754
- const response = await handler(
755
- toolCall.args as ToolArgs<T, TName>,
756
- enrichedContext
757
- );
758
-
759
- // Automatically append tool result to thread (unless handler already did)
760
- if (!response.resultAppended) {
761
- await appendToolResult({
762
- threadId: options.threadId,
763
- toolCallId: toolCall.id,
764
- toolName: toolCall.name,
765
- content: response.toolResponse,
766
- });
767
- }
768
-
769
- return {
770
- toolCallId: toolCall.id,
771
- name: toolCall.name as TName,
772
- data: response.data,
773
- };
774
- };
775
-
776
- if (options.parallel) {
777
- return Promise.all(matchingCalls.map(processOne));
778
- }
779
-
780
- const results: ToolCallResult<TName, TResult>[] = [];
781
- for (const toolCall of matchingCalls) {
782
- results.push(await processOne(toolCall));
783
- }
784
- return results;
785
- },
786
-
787
- // --- Utility methods ---
788
-
789
- filterByName<TName extends ToolNames<T>>(
790
- toolCalls: ParsedToolCallUnion<T>[],
791
- name: TName
792
- ): ParsedToolCall<TName, ToolArgs<T, TName>>[] {
793
- return toolCalls.filter(
794
- (tc): tc is ParsedToolCall<TName, ToolArgs<T, TName>> =>
795
- tc.name === name
796
- );
797
- },
798
-
799
- hasToolCall(
800
- toolCalls: ParsedToolCallUnion<T>[],
801
- name: ToolNames<T>
802
- ): boolean {
803
- return toolCalls.some((tc) => tc.name === name);
804
- },
805
-
806
- getResultsByName<TName extends ToolNames<T>>(
807
- results: ToolCallResultUnion<TResults>[],
808
- name: TName
809
- ): ToolCallResult<TName, ToolResult<T, TName>>[] {
810
- return results.filter((r) => r.name === name) as ToolCallResult<
811
- TName,
812
- ToolResult<T, TName>
813
- >[];
814
- },
815
- };
816
- }
817
-
818
- /**
819
- * Wraps a tool handler to automatically append its result directly to the
820
- * thread and sets `resultAppended: true` on the response.
821
- *
822
- * Use this for tools whose responses may exceed Temporal's activity payload
823
- * limit. The wrapper appends to the thread inside the activity (where Redis
824
- * is available), then replaces `toolResponse` with an empty string so the
825
- * large payload never travels through the Temporal workflow boundary.
826
- *
827
- * @param getThread - Factory that returns a thread manager for the given threadId
828
- * @param handler - The original tool handler
829
- * @returns A wrapped handler that auto-appends and flags the response
830
- *
831
- * @example
832
- * ```typescript
833
- * import { withAutoAppend } from '@bead-ai/zeitlich/workflow';
834
- * import { createThreadManager } from '@bead-ai/zeitlich';
835
- *
836
- * const handler = withAutoAppend(
837
- * (threadId) => createThreadManager({ redis, threadId }),
838
- * async (args, ctx) => ({
839
- * toolResponse: JSON.stringify(largeResult), // appended directly to Redis
840
- * data: { summary: "..." }, // small data for workflow
841
- * }),
842
- * );
843
- * ```
844
- */
845
- export function withAutoAppend<
846
- TArgs,
847
- TResult,
848
- TContext extends ToolHandlerContext = ToolHandlerContext,
849
- >(
850
- threadHandler: (config: ToolResultConfig) => Promise<void>,
851
- handler: ActivityToolHandler<TArgs, TResult, TContext>
852
- ): ActivityToolHandler<TArgs, TResult, TContext> {
853
- return async (args: TArgs, context: TContext) => {
854
- const response = await handler(args, context);
855
- const threadId = (context as Record<string, unknown>).threadId as string;
856
- const toolCallId = (context as Record<string, unknown>)
857
- .toolCallId as string;
858
- const toolName = (context as Record<string, unknown>).toolName as string;
859
-
860
- // Append directly (inside the activity, bypassing Temporal payload)
861
- await threadHandler({
862
- threadId,
863
- toolCallId,
864
- toolName,
865
- content: response.toolResponse,
866
- });
867
-
868
- // Return with empty toolResponse to keep the Temporal payload small
869
- return {
870
- toolResponse: "Response appended via withAutoAppend",
871
- data: response.data,
872
- resultAppended: true,
873
- };
874
- };
875
- }
876
-
877
- /**
878
- * Identity function that creates a generic inference context for a tool definition.
879
- * TypeScript infers TResult from the handler and flows it to hooks automatically.
880
- *
881
- * @example
882
- * ```typescript
883
- * tools: {
884
- * AskUser: defineTool({
885
- * ...askUserTool,
886
- * handler: handleAskUser,
887
- * hooks: {
888
- * onPostToolUse: ({ result }) => {
889
- * // result is correctly typed as the handler's return data type
890
- * },
891
- * },
892
- * }),
893
- * }
894
- * ```
895
- */
896
- export function defineTool<
897
- TName extends string,
898
- TSchema extends z.ZodType,
899
- TResult,
900
- TContext = ToolHandlerContext,
901
- >(
902
- tool: ToolWithHandler<TName, TSchema, TResult, TContext>
903
- ): ToolWithHandler<TName, TSchema, TResult, TContext> {
904
- return tool;
905
- }
906
-
907
- /**
908
- * Identity function that provides full type inference for subagent configurations.
909
- * Verifies the workflow function's input parameters match the configured context,
910
- * and properly types the lifecycle hooks with Task tool args and inferred result type.
911
- *
912
- * @example
913
- * ```ts
914
- * // With typed context — workflow must accept { prompt, context }
915
- * const researcher = defineSubagent({
916
- * name: "researcher",
917
- * description: "Researches topics",
918
- * workflow: researcherWorkflow, // (input: { prompt: string; context: { apiKey: string } }) => Promise<...>
919
- * context: { apiKey: "..." },
920
- * resultSchema: z.object({ findings: z.string() }),
921
- * hooks: {
922
- * onPostExecution: ({ result }) => {
923
- * // result is typed as { findings: string }
924
- * },
925
- * },
926
- * });
927
- *
928
- * // Without context — workflow only needs { prompt }
929
- * const writer = defineSubagent({
930
- * name: "writer",
931
- * description: "Writes content",
932
- * workflow: writerWorkflow, // (input: { prompt: string }) => Promise<...>
933
- * resultSchema: z.object({ content: z.string() }),
934
- * });
935
- * ```
936
- */
937
- // With context — verifies workflow accepts { prompt, context: TContext }
938
- export function defineSubagent<
939
- TResult extends z.ZodType = z.ZodType,
940
- TContext extends Record<string, unknown> = Record<string, unknown>,
941
- >(
942
- config: Omit<SubagentConfig<TResult>, "hooks" | "workflow" | "context"> & {
943
- workflow:
944
- | string
945
- | ((input: {
946
- prompt: string;
947
- context: TContext;
948
- }) => Promise<ToolHandlerResponse<z.infer<TResult> | null>>);
949
- context: TContext;
950
- hooks?: SubagentHooks<SubagentArgs, z.infer<TResult>>;
951
- }
952
- ): SubagentConfig<TResult>;
953
- // Without context — verifies workflow accepts { prompt }
954
- export function defineSubagent<TResult extends z.ZodType = z.ZodType>(
955
- config: Omit<SubagentConfig<TResult>, "hooks" | "workflow"> & {
956
- workflow:
957
- | string
958
- | ((input: {
959
- prompt: string;
960
- }) => Promise<ToolHandlerResponse<z.infer<TResult> | null>>);
961
- hooks?: SubagentHooks<SubagentArgs, z.infer<TResult>>;
962
- }
963
- ): SubagentConfig<TResult>;
964
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
965
- export function defineSubagent(config: any): SubagentConfig {
966
- return config;
967
- }
968
-
969
- /**
970
- * Utility to check if there were no tool calls besides a specific one
971
- */
972
- export function hasNoOtherToolCalls<T extends ToolMap>(
973
- toolCalls: ParsedToolCallUnion<T>[],
974
- excludeName: ToolNames<T>
975
- ): boolean {
976
- return toolCalls.filter((tc) => tc.name !== excludeName).length === 0;
977
- }