zeitlich 0.2.21 → 0.2.23

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 (129) hide show
  1. package/README.md +303 -105
  2. package/dist/adapters/sandbox/daytona/index.cjs +7 -1
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/daytona/index.d.cts +3 -1
  5. package/dist/adapters/sandbox/daytona/index.d.ts +3 -1
  6. package/dist/adapters/sandbox/daytona/index.js +7 -1
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  8. package/dist/adapters/sandbox/daytona/workflow.cjs +33 -0
  9. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -0
  10. package/dist/adapters/sandbox/daytona/workflow.d.cts +27 -0
  11. package/dist/adapters/sandbox/daytona/workflow.d.ts +27 -0
  12. package/dist/adapters/sandbox/daytona/workflow.js +31 -0
  13. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -0
  14. package/dist/adapters/sandbox/inmemory/index.cjs +18 -1
  15. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/inmemory/index.d.cts +4 -2
  17. package/dist/adapters/sandbox/inmemory/index.d.ts +4 -2
  18. package/dist/adapters/sandbox/inmemory/index.js +18 -1
  19. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  20. package/dist/adapters/sandbox/inmemory/workflow.cjs +33 -0
  21. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -0
  22. package/dist/adapters/sandbox/inmemory/workflow.d.cts +25 -0
  23. package/dist/adapters/sandbox/inmemory/workflow.d.ts +25 -0
  24. package/dist/adapters/sandbox/inmemory/workflow.js +31 -0
  25. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -0
  26. package/dist/adapters/sandbox/virtual/index.cjs +36 -9
  27. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/virtual/index.d.cts +8 -5
  29. package/dist/adapters/sandbox/virtual/index.d.ts +8 -5
  30. package/dist/adapters/sandbox/virtual/index.js +36 -9
  31. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  32. package/dist/adapters/sandbox/virtual/workflow.cjs +33 -0
  33. package/dist/adapters/sandbox/virtual/workflow.cjs.map +1 -0
  34. package/dist/adapters/sandbox/virtual/workflow.d.cts +27 -0
  35. package/dist/adapters/sandbox/virtual/workflow.d.ts +27 -0
  36. package/dist/adapters/sandbox/virtual/workflow.js +31 -0
  37. package/dist/adapters/sandbox/virtual/workflow.js.map +1 -0
  38. package/dist/adapters/thread/google-genai/index.cjs +9 -1
  39. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  40. package/dist/adapters/thread/google-genai/index.d.cts +31 -19
  41. package/dist/adapters/thread/google-genai/index.d.ts +31 -19
  42. package/dist/adapters/thread/google-genai/index.js +9 -1
  43. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  44. package/dist/adapters/thread/google-genai/workflow.cjs +33 -0
  45. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -0
  46. package/dist/adapters/thread/google-genai/workflow.d.cts +32 -0
  47. package/dist/adapters/thread/google-genai/workflow.d.ts +32 -0
  48. package/dist/adapters/thread/google-genai/workflow.js +31 -0
  49. package/dist/adapters/thread/google-genai/workflow.js.map +1 -0
  50. package/dist/adapters/thread/langchain/index.cjs +9 -1
  51. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  52. package/dist/adapters/thread/langchain/index.d.cts +27 -16
  53. package/dist/adapters/thread/langchain/index.d.ts +27 -16
  54. package/dist/adapters/thread/langchain/index.js +9 -1
  55. package/dist/adapters/thread/langchain/index.js.map +1 -1
  56. package/dist/adapters/thread/langchain/workflow.cjs +33 -0
  57. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -0
  58. package/dist/adapters/thread/langchain/workflow.d.cts +32 -0
  59. package/dist/adapters/thread/langchain/workflow.d.ts +32 -0
  60. package/dist/adapters/thread/langchain/workflow.js +31 -0
  61. package/dist/adapters/thread/langchain/workflow.js.map +1 -0
  62. package/dist/index.cjs +282 -90
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +38 -16
  65. package/dist/index.d.ts +38 -16
  66. package/dist/index.js +281 -87
  67. package/dist/index.js.map +1 -1
  68. package/dist/queries-DModcWRy.d.cts +44 -0
  69. package/dist/queries-byD0jr1Y.d.ts +44 -0
  70. package/dist/{types-BkAYmc96.d.ts → types-B50pBPEV.d.ts} +190 -38
  71. package/dist/{types-YbL7JpEA.d.cts → types-Bll19FZJ.d.cts} +7 -0
  72. package/dist/{types-YbL7JpEA.d.ts → types-Bll19FZJ.d.ts} +7 -0
  73. package/dist/{queries-6Avfh74U.d.ts → types-BuXdFhaZ.d.cts} +7 -48
  74. package/dist/{types-BMRzfELQ.d.cts → types-ChAMwU3q.d.cts} +17 -1
  75. package/dist/{types-BMRzfELQ.d.ts → types-ChAMwU3q.d.ts} +17 -1
  76. package/dist/{types-CES_30qx.d.cts → types-DQW8l7pY.d.cts} +190 -38
  77. package/dist/{queries-CHa2iv_I.d.cts → types-GZ76HZSj.d.ts} +7 -48
  78. package/dist/workflow.cjs +244 -86
  79. package/dist/workflow.cjs.map +1 -1
  80. package/dist/workflow.d.cts +54 -65
  81. package/dist/workflow.d.ts +54 -65
  82. package/dist/workflow.js +243 -83
  83. package/dist/workflow.js.map +1 -1
  84. package/package.json +54 -2
  85. package/src/adapters/sandbox/daytona/filesystem.ts +1 -1
  86. package/src/adapters/sandbox/daytona/index.ts +8 -0
  87. package/src/adapters/sandbox/daytona/proxy.ts +56 -0
  88. package/src/adapters/sandbox/e2b/filesystem.ts +147 -0
  89. package/src/adapters/sandbox/e2b/index.ts +164 -0
  90. package/src/adapters/sandbox/e2b/types.ts +23 -0
  91. package/src/adapters/sandbox/inmemory/index.ts +27 -3
  92. package/src/adapters/sandbox/inmemory/proxy.ts +53 -0
  93. package/src/adapters/sandbox/virtual/filesystem.ts +41 -17
  94. package/src/adapters/sandbox/virtual/provider.ts +9 -1
  95. package/src/adapters/sandbox/virtual/proxy.ts +53 -0
  96. package/src/adapters/sandbox/virtual/types.ts +9 -4
  97. package/src/adapters/thread/google-genai/activities.ts +51 -17
  98. package/src/adapters/thread/google-genai/index.ts +1 -0
  99. package/src/adapters/thread/google-genai/proxy.ts +61 -0
  100. package/src/adapters/thread/langchain/activities.ts +47 -14
  101. package/src/adapters/thread/langchain/index.ts +1 -0
  102. package/src/adapters/thread/langchain/proxy.ts +61 -0
  103. package/src/lib/lifecycle.ts +57 -0
  104. package/src/lib/sandbox/manager.ts +52 -6
  105. package/src/lib/sandbox/sandbox.test.ts +12 -11
  106. package/src/lib/sandbox/types.ts +31 -4
  107. package/src/lib/session/index.ts +4 -5
  108. package/src/lib/session/session-edge-cases.integration.test.ts +491 -66
  109. package/src/lib/session/session.integration.test.ts +92 -80
  110. package/src/lib/session/session.ts +108 -96
  111. package/src/lib/session/types.ts +87 -17
  112. package/src/lib/subagent/define.ts +6 -5
  113. package/src/lib/subagent/handler.ts +148 -16
  114. package/src/lib/subagent/index.ts +4 -0
  115. package/src/lib/subagent/register.ts +10 -3
  116. package/src/lib/subagent/signals.ts +8 -0
  117. package/src/lib/subagent/subagent.integration.test.ts +893 -128
  118. package/src/lib/subagent/tool.ts +2 -2
  119. package/src/lib/subagent/types.ts +84 -21
  120. package/src/lib/subagent/workflow.ts +83 -12
  121. package/src/lib/tool-router/router-edge-cases.integration.test.ts +4 -1
  122. package/src/lib/tool-router/router.integration.test.ts +141 -5
  123. package/src/lib/tool-router/router.ts +13 -3
  124. package/src/lib/tool-router/types.ts +7 -0
  125. package/src/lib/workflow.test.ts +104 -27
  126. package/src/lib/workflow.ts +37 -19
  127. package/src/tools/bash/bash.test.ts +16 -7
  128. package/src/workflow.ts +11 -14
  129. package/tsup.config.ts +6 -0
@@ -6,7 +6,7 @@ export const SUBAGENT_TOOL_NAME = "Subagent" as const;
6
6
  function buildSubagentDescription(subagents: SubagentConfig[]): string {
7
7
  const subagentList = subagents
8
8
  .map((s) => {
9
- const continuation = s.allowThreadContinuation
9
+ const continuation = s.thread && s.thread !== "new"
10
10
  ? "\n*(Supports thread continuation — pass a threadId to resume a previous conversation)*"
11
11
  : "";
12
12
  return `## ${s.agentName}\n${s.description}${continuation}`;
@@ -39,7 +39,7 @@ export function createSubagentTool<T extends SubagentConfig[]>(
39
39
 
40
40
  const names = subagents.map((s) => s.agentName);
41
41
  const hasThreadContinuation = subagents.some(
42
- (s) => s.allowThreadContinuation
42
+ (s) => s.thread && s.thread !== "new"
43
43
  );
44
44
 
45
45
  const baseFields = {
@@ -4,26 +4,33 @@ import type {
4
4
  PreToolUseHookResult,
5
5
  PostToolUseFailureHookResult,
6
6
  } from "../tool-router/types";
7
+ import type {
8
+ ThreadInit,
9
+ SandboxInit,
10
+ SubagentSandboxShutdown,
11
+ } from "../lifecycle";
7
12
 
8
13
  /** ToolHandlerResponse with threadId required (subagents must always surface their thread) */
9
14
  export type SubagentHandlerResponse<TResult = null> =
10
- ToolHandlerResponse<TResult> & { threadId: string };
15
+ ToolHandlerResponse<TResult> & { threadId: string; sandboxId?: string };
11
16
 
12
17
  /**
13
18
  * Raw workflow input fields passed from parent to child workflow.
14
19
  * `defineSubagentWorkflow` maps this into `SubagentSessionInput`.
15
20
  */
16
21
  export interface SubagentWorkflowInput {
17
- /** Thread ID from parent for continuation */
18
- previousThreadId?: string;
19
- /** Sandbox ID inherited from parent */
20
- sandboxId?: string;
22
+ /** Thread initialization strategy forwarded from the parent */
23
+ thread?: ThreadInit;
24
+ /** Sandbox initialization strategy forwarded from the parent */
25
+ sandbox?: SandboxInit;
26
+ /** Sandbox shutdown override from the parent (takes precedence over workflow default) */
27
+ sandboxShutdown?: SubagentSandboxShutdown;
21
28
  }
22
29
 
23
30
  export type SubagentWorkflow<TResult extends z.ZodType = z.ZodType> = (
24
31
  prompt: string,
25
32
  workflowInput: SubagentWorkflowInput,
26
- context?: Record<string, unknown>,
33
+ context?: Record<string, unknown>
27
34
  ) => Promise<SubagentHandlerResponse<z.infer<TResult> | null>>;
28
35
 
29
36
  /**
@@ -36,17 +43,39 @@ export type SubagentDefinition<
36
43
  > = ((
37
44
  prompt: string,
38
45
  workflowInput: SubagentWorkflowInput,
39
- context?: TContext,
46
+ context?: TContext
40
47
  ) => Promise<SubagentHandlerResponse<z.infer<TResult> | null>>) & {
41
48
  readonly agentName: string;
42
49
  readonly description: string;
43
50
  readonly resultSchema?: TResult;
44
51
  };
45
52
 
53
+ /** Context value or factory — resolved at invocation time when a function is provided */
54
+ export type SubagentContext =
55
+ | Record<string, unknown>
56
+ | (() => Record<string, unknown>);
57
+
46
58
  /** Infer the z.infer'd result type from a SubagentConfig, or null if no schema */
47
59
  export type InferSubagentResult<T extends SubagentConfig> =
48
60
  T extends SubagentConfig<infer S> ? z.infer<S> : null;
49
61
 
62
+ /**
63
+ * Sandbox configuration for a subagent.
64
+ *
65
+ * String shorthands:
66
+ * - `"none"` — no sandbox (default).
67
+ * - `"inherit"` — reuse the parent's sandbox (shared filesystem/exec).
68
+ * - `"own"` — the child creates and owns its own sandbox (shutdown defaults to `"destroy"`).
69
+ *
70
+ * Object form (only for `source: "own"`):
71
+ * - `{ source: "own", shutdown?: SubagentSandboxShutdown }` — own sandbox with explicit shutdown policy.
72
+ */
73
+ export type SubagentSandboxConfig =
74
+ | "none"
75
+ | "inherit"
76
+ | "own"
77
+ | { source: "own"; shutdown?: SubagentSandboxShutdown };
78
+
50
79
  /**
51
80
  * Configuration for a subagent that can be spawned by the parent workflow.
52
81
  *
@@ -60,23 +89,34 @@ export interface SubagentConfig<TResult extends z.ZodType = z.ZodType> {
60
89
  /** Whether this subagent is available (default: true). Disabled subagents are excluded from the Subagent tool. */
61
90
  enabled?: boolean | (() => boolean);
62
91
  /** Temporal workflow function or type name (used with executeChild) */
63
- workflow: string | SubagentWorkflow<TResult>;
92
+ workflow: SubagentWorkflow<TResult>;
64
93
  /** Optional task queue - defaults to parent's queue if not specified */
65
94
  taskQueue?: string;
66
95
  /** Optional Zod schema to validate the child workflow's result. If omitted, result is passed through as-is. */
67
96
  resultSchema?: TResult;
68
- /** Optional static context passed to the subagent on every invocation */
69
- context?: Record<string, unknown>;
70
- /** Allow the parent agent to pass a threadId for this subagent to continue (default: false) */
71
- allowThreadContinuation?: boolean;
97
+ /** Optional context passed to the subagent a static object or a function evaluated at invocation time */
98
+ context?: SubagentContext;
72
99
  /** Per-subagent lifecycle hooks */
73
100
  hooks?: SubagentHooks;
101
+ /**
102
+ * Thread mode for this subagent.
103
+ *
104
+ * - `"new"` (default) — always start a fresh thread.
105
+ * - `"fork"` — the parent can pass a `threadId`; messages are copied into
106
+ * a new thread and the subagent continues there.
107
+ * - `"continue"` — the parent can pass a `threadId`; the subagent appends
108
+ * directly to the existing thread in-place.
109
+ */
110
+ thread?: "new" | "fork" | "continue";
74
111
  /**
75
112
  * Sandbox strategy for this subagent.
76
- * - `'inherit'` (default): reuse the parent's sandbox (shared filesystem/exec).
77
- * - `'own'`: the child creates and owns its own sandbox.
113
+ *
114
+ * String shorthands: `"none"` (default) | `"inherit"` | `"own"`.
115
+ * Object form: `{ source: "own", shutdown?: SubagentSandboxShutdown }`.
116
+ *
117
+ * @see {@link SubagentSandboxConfig}
78
118
  */
79
- sandbox?: "inherit" | "own";
119
+ sandbox?: SubagentSandboxConfig;
80
120
  }
81
121
 
82
122
  /**
@@ -97,6 +137,8 @@ export interface SubagentHooks<TArgs = unknown, TResult = unknown> {
97
137
  threadId: string;
98
138
  turn: number;
99
139
  durationMs: number;
140
+ /** Unvalidated metadata from the child workflow (e.g. infrastructure state) */
141
+ metadata?: Record<string, unknown>;
100
142
  }) => void | Promise<void>;
101
143
  /** Called when this subagent execution fails */
102
144
  onExecutionFailure?: (ctx: {
@@ -107,16 +149,37 @@ export interface SubagentHooks<TArgs = unknown, TResult = unknown> {
107
149
  }) => PostToolUseFailureHookResult | Promise<PostToolUseFailureHookResult>;
108
150
  }
109
151
 
152
+ /**
153
+ * Extended response from the subagent `fn` — includes optional cleanup callbacks
154
+ * stripped before signaling the parent.
155
+ *
156
+ * When `TSandboxShutdown` is `"pause-until-parent-close"`, both `destroySandbox`
157
+ * and `sandboxId` become required so the parent can coordinate cleanup.
158
+ */
159
+ export type SubagentFnResult<
160
+ TResult = null,
161
+ TSandboxShutdown extends SubagentSandboxShutdown = SubagentSandboxShutdown,
162
+ > = SubagentHandlerResponse<TResult> &
163
+ (TSandboxShutdown extends "pause-until-parent-close"
164
+ ? { destroySandbox: () => Promise<void>; sandboxId: string }
165
+ : { destroySandbox?: () => Promise<void> });
166
+
167
+ /** Payload sent by a child workflow to signal its result back to the parent */
168
+ export interface ChildResultSignalPayload {
169
+ childWorkflowId: string;
170
+ result: SubagentHandlerResponse;
171
+ }
172
+
110
173
  /**
111
174
  * Session config fields passed from parent to child workflow.
112
175
  */
113
176
  export interface SubagentSessionInput {
114
177
  /** Agent name — spread directly into `createSession` */
115
178
  agentName: string;
116
- /** Thread ID to continue from */
117
- threadId?: string;
118
- /** Whether to continue an existing thread */
119
- continueThread?: boolean;
120
- /** Sandbox ID inherited from the parent agent */
121
- sandboxId?: string;
179
+ /** Thread initialization strategy */
180
+ thread?: ThreadInit;
181
+ /** Sandbox initialization strategy */
182
+ sandbox?: SandboxInit;
183
+ /** Sandbox shutdown policy (default: "destroy") */
184
+ sandboxShutdown?: SubagentSandboxShutdown;
122
185
  }
@@ -1,10 +1,20 @@
1
1
  import type { z } from "zod";
2
+ import {
3
+ workflowInfo,
4
+ getExternalWorkflowHandle,
5
+ setHandler,
6
+ condition,
7
+ ApplicationFailure,
8
+ } from "@temporalio/workflow";
2
9
  import type {
3
10
  SubagentDefinition,
11
+ SubagentFnResult,
4
12
  SubagentHandlerResponse,
5
13
  SubagentWorkflowInput,
6
14
  SubagentSessionInput,
7
15
  } from "./types";
16
+ import type { SubagentSandboxShutdown } from "../lifecycle";
17
+ import { childResultSignal, destroySandboxSignal } from "./signals";
8
18
 
9
19
  /**
10
20
  * Defines a subagent workflow with embedded metadata (name, description, resultSchema).
@@ -54,34 +64,50 @@ import type {
54
64
  */
55
65
  // Without resultSchema — data is null
56
66
  export function defineSubagentWorkflow<
67
+ TSandboxShutdown extends SubagentSandboxShutdown = "destroy",
57
68
  TContext extends Record<string, unknown> = Record<string, unknown>,
58
69
  >(
59
- config: { name: string; description: string },
70
+ config: {
71
+ name: string;
72
+ description: string;
73
+ sandboxShutdown?: TSandboxShutdown;
74
+ },
60
75
  fn: (
61
76
  prompt: string,
62
77
  sessionInput: SubagentSessionInput,
63
78
  context: TContext
64
- ) => Promise<SubagentHandlerResponse<null>>
79
+ ) => Promise<SubagentFnResult<null, TSandboxShutdown>>
65
80
  ): SubagentDefinition<z.ZodNull, TContext>;
66
81
  // With resultSchema — data is inferred from the schema
67
82
  export function defineSubagentWorkflow<
68
83
  TResult extends z.ZodType,
84
+ TSandboxShutdown extends SubagentSandboxShutdown = "destroy",
69
85
  TContext extends Record<string, unknown> = Record<string, unknown>,
70
86
  >(
71
- config: { name: string; description: string; resultSchema: TResult },
87
+ config: {
88
+ name: string;
89
+ description: string;
90
+ resultSchema: TResult;
91
+ sandboxShutdown?: TSandboxShutdown;
92
+ },
72
93
  fn: (
73
94
  prompt: string,
74
95
  sessionInput: SubagentSessionInput,
75
96
  context: TContext
76
- ) => Promise<SubagentHandlerResponse<z.infer<TResult> | null>>
97
+ ) => Promise<SubagentFnResult<z.infer<TResult> | null, TSandboxShutdown>>
77
98
  ): SubagentDefinition<TResult, TContext>;
78
99
  export function defineSubagentWorkflow(
79
- config: { name: string; description: string; resultSchema?: z.ZodType },
100
+ config: {
101
+ name: string;
102
+ description: string;
103
+ resultSchema?: z.ZodType;
104
+ sandboxShutdown?: SubagentSandboxShutdown;
105
+ },
80
106
  fn: (
81
107
  prompt: string,
82
108
  sessionInput: SubagentSessionInput,
83
109
  context: Record<string, unknown>
84
- ) => Promise<SubagentHandlerResponse<unknown>>
110
+ ) => Promise<SubagentFnResult<unknown>>
85
111
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
112
  ): SubagentDefinition<any, any> {
87
113
  const workflow = async (
@@ -89,15 +115,60 @@ export function defineSubagentWorkflow(
89
115
  workflowInput: SubagentWorkflowInput,
90
116
  context?: Record<string, unknown>
91
117
  ): Promise<SubagentHandlerResponse<unknown>> => {
118
+ const effectiveShutdown =
119
+ workflowInput.sandboxShutdown ?? config.sandboxShutdown ?? "destroy";
120
+
92
121
  const sessionInput: SubagentSessionInput = {
93
122
  agentName: config.name,
94
- ...(workflowInput.previousThreadId && {
95
- threadId: workflowInput.previousThreadId,
96
- continueThread: true,
97
- }),
98
- ...(workflowInput.sandboxId && { sandboxId: workflowInput.sandboxId }),
123
+ sandboxShutdown: effectiveShutdown,
124
+ ...(workflowInput.thread && { thread: workflowInput.thread }),
125
+ ...(workflowInput.sandbox && { sandbox: workflowInput.sandbox }),
99
126
  };
100
- return fn(prompt, sessionInput, context ?? {});
127
+ const { destroySandbox, ...result } = await fn(
128
+ prompt,
129
+ sessionInput,
130
+ context ?? {}
131
+ );
132
+
133
+ if (effectiveShutdown === "pause-until-parent-close") {
134
+ if (!destroySandbox) {
135
+ throw ApplicationFailure.create({
136
+ message: `Subagent "${config.name}" has sandboxShutdown="pause-until-parent-close" but fn did not return a destroySandbox callback`,
137
+ nonRetryable: true,
138
+ });
139
+ }
140
+ if (!result.sandboxId) {
141
+ throw ApplicationFailure.create({
142
+ message: `Subagent "${config.name}" has sandboxShutdown="pause-until-parent-close" but fn did not return a sandboxId`,
143
+ nonRetryable: true,
144
+ });
145
+ }
146
+ }
147
+
148
+ const { parent } = workflowInfo();
149
+ if (!parent) {
150
+ throw ApplicationFailure.create({
151
+ message: "Subagent workflow called without a parent workflow",
152
+ nonRetryable: true,
153
+ });
154
+ }
155
+
156
+ const parentHandle = getExternalWorkflowHandle(parent.workflowId);
157
+ await parentHandle.signal(childResultSignal, {
158
+ childWorkflowId: workflowInfo().workflowId,
159
+ result,
160
+ });
161
+
162
+ if (destroySandbox) {
163
+ let destroyRequested = false;
164
+ setHandler(destroySandboxSignal, () => {
165
+ destroyRequested = true;
166
+ });
167
+ await condition(() => destroyRequested);
168
+ await destroySandbox();
169
+ }
170
+
171
+ return result;
101
172
  };
102
173
 
103
174
  // for temporal workflow name
@@ -22,7 +22,10 @@ vi.mock("@temporalio/workflow", () => {
22
22
  return err;
23
23
  }
24
24
  }
25
- return { ApplicationFailure: MockApplicationFailure };
25
+ return {
26
+ ApplicationFailure: MockApplicationFailure,
27
+ uuid4: () => "00000000-0000-0000-0000-000000000000",
28
+ };
26
29
  });
27
30
 
28
31
  import { createToolRouter, defineTool, hasNoOtherToolCalls } from "./router";
@@ -22,7 +22,10 @@ vi.mock("@temporalio/workflow", () => {
22
22
  return err;
23
23
  }
24
24
  }
25
- return { ApplicationFailure: MockApplicationFailure };
25
+ return {
26
+ ApplicationFailure: MockApplicationFailure,
27
+ uuid4: () => "00000000-0000-0000-0000-000000000000",
28
+ };
26
29
  });
27
30
 
28
31
  import { createToolRouter, defineTool } from "./router";
@@ -576,7 +579,7 @@ describe("createToolRouter integration", () => {
576
579
  });
577
580
  });
578
581
 
579
- it("throws when handler fails and no hook recovers", async () => {
582
+ it("suppresses error when handler fails and no hook recovers", async () => {
580
583
  const router = createToolRouter({
581
584
  tools: { Fail: failingTool } as const,
582
585
  threadId: "t-1",
@@ -589,9 +592,12 @@ describe("createToolRouter integration", () => {
589
592
  args: { reason: "unrecoverable" },
590
593
  });
591
594
 
592
- await expect(
593
- router.processToolCalls([parsed], { turn: 1 })
594
- ).rejects.toThrow("unrecoverable");
595
+ const results = await router.processToolCalls([parsed], { turn: 1 });
596
+ expect(results).toHaveLength(1);
597
+ expect(at(results, 0).data).toEqual({
598
+ error: "Error: unrecoverable",
599
+ suppressed: true,
600
+ });
595
601
  });
596
602
 
597
603
  // --- Disabled tools ---
@@ -704,6 +710,136 @@ describe("createToolRouter integration", () => {
704
710
  expect(router.getResultsByName(results, "Add")).toHaveLength(1);
705
711
  });
706
712
 
713
+ // --- Metadata passthrough ---
714
+
715
+ it("handler metadata flows to ToolCallResult", async () => {
716
+ const metaTool = defineTool({
717
+ name: "Meta" as const,
718
+ description: "returns metadata",
719
+ schema: z.object({}),
720
+ handler: async () => ({
721
+ toolResponse: "ok",
722
+ data: null,
723
+ metadata: { jobId: "j-99", env: "prod" },
724
+ }),
725
+ });
726
+
727
+ const router = createToolRouter({
728
+ tools: { Meta: metaTool } as const,
729
+ threadId: "t-1",
730
+ appendToolResult: appendSpy.fn,
731
+ });
732
+
733
+ const parsed = router.parseToolCall({ id: "tc-1", name: "Meta", args: {} });
734
+ const results = await router.processToolCalls([parsed], { turn: 1 });
735
+
736
+ expect(at(results, 0).metadata).toEqual({ jobId: "j-99", env: "prod" });
737
+ });
738
+
739
+ it("handler metadata flows to per-tool post-hook", async () => {
740
+ let hookMetadata: Record<string, unknown> | undefined;
741
+
742
+ const metaTool = defineTool({
743
+ name: "Meta" as const,
744
+ description: "returns metadata",
745
+ schema: z.object({}),
746
+ handler: async () => ({
747
+ toolResponse: "ok",
748
+ data: null,
749
+ metadata: { region: "us-east-1" },
750
+ }),
751
+ hooks: {
752
+ onPostToolUse: async ({ metadata }) => {
753
+ hookMetadata = metadata;
754
+ },
755
+ },
756
+ });
757
+
758
+ const router = createToolRouter({
759
+ tools: { Meta: metaTool } as const,
760
+ threadId: "t-1",
761
+ appendToolResult: appendSpy.fn,
762
+ });
763
+
764
+ const parsed = router.parseToolCall({ id: "tc-1", name: "Meta", args: {} });
765
+ await router.processToolCalls([parsed], { turn: 1 });
766
+
767
+ expect(hookMetadata).toEqual({ region: "us-east-1" });
768
+ });
769
+
770
+ it("handler metadata flows to global post-hook via result", async () => {
771
+ let resultMetadata: Record<string, unknown> | undefined;
772
+
773
+ const metaTool = defineTool({
774
+ name: "Meta" as const,
775
+ description: "returns metadata",
776
+ schema: z.object({}),
777
+ handler: async () => ({
778
+ toolResponse: "ok",
779
+ data: null,
780
+ metadata: { traceId: "abc" },
781
+ }),
782
+ });
783
+
784
+ const router = createToolRouter({
785
+ tools: { Meta: metaTool } as const,
786
+ threadId: "t-1",
787
+ appendToolResult: appendSpy.fn,
788
+ hooks: {
789
+ onPostToolUse: async ({ result }) => {
790
+ resultMetadata = result.metadata;
791
+ },
792
+ },
793
+ });
794
+
795
+ const parsed = router.parseToolCall({ id: "tc-1", name: "Meta", args: {} });
796
+ await router.processToolCalls([parsed], { turn: 1 });
797
+
798
+ expect(resultMetadata).toEqual({ traceId: "abc" });
799
+ });
800
+
801
+ it("metadata is undefined when handler does not set it", async () => {
802
+ const router = createToolRouter({
803
+ tools: createTools(),
804
+ threadId: "t-1",
805
+ appendToolResult: appendSpy.fn,
806
+ });
807
+
808
+ const parsed = router.parseToolCall({
809
+ id: "tc-1",
810
+ name: "Echo",
811
+ args: { text: "hi" },
812
+ });
813
+ const results = await router.processToolCalls([parsed], { turn: 1 });
814
+
815
+ expect(at(results, 0).metadata).toBeUndefined();
816
+ });
817
+
818
+ it("processToolCallsByName passes metadata through", async () => {
819
+ const router = createToolRouter({
820
+ tools: createTools(),
821
+ threadId: "t-1",
822
+ appendToolResult: appendSpy.fn,
823
+ });
824
+
825
+ const calls = [
826
+ router.parseToolCall({ id: "tc-1", name: "Echo", args: { text: "a" } }),
827
+ ];
828
+
829
+ const results = await router.processToolCallsByName(
830
+ calls,
831
+ "Echo",
832
+ async (args: { text: string }) => ({
833
+ toolResponse: `custom: ${args.text}`,
834
+ data: { custom: args.text },
835
+ metadata: { source: "custom-handler" },
836
+ })
837
+ );
838
+
839
+ expect(results).toHaveLength(1);
840
+ expect(at(results, 0).metadata).toEqual({ source: "custom-handler" });
841
+ });
842
+
707
843
  // --- resultAppended flag ---
708
844
 
709
845
  it("skips appendToolResult when handler sets resultAppended", async () => {
@@ -20,7 +20,7 @@ import type {
20
20
  } from "./types";
21
21
 
22
22
  import type { z } from "zod";
23
- import { ApplicationFailure, uuid4 } from "@temporalio/workflow";
23
+ import { uuid4 } from "@temporalio/workflow";
24
24
 
25
25
  /**
26
26
  * Creates a tool router for declarative tool call processing.
@@ -110,7 +110,7 @@ export function createToolRouter<T extends ToolMap>(
110
110
 
111
111
  /**
112
112
  * Run per-tool → global failure hooks. Returns recovery content/result,
113
- * or throws if no hook recovers.
113
+ * or a generic error response if no hook recovers.
114
114
  */
115
115
  async function runFailureHooks(
116
116
  toolCall: ParsedToolCallUnion<T>,
@@ -160,7 +160,12 @@ export function createToolRouter<T extends ToolMap>(
160
160
  };
161
161
  }
162
162
 
163
- throw ApplicationFailure.fromError(error, { nonRetryable: true });
163
+ return {
164
+ content: JSON.stringify({
165
+ error: "The tool encountered an error. Please try again or use a different approach.",
166
+ }),
167
+ result: { error: errorStr, suppressed: true },
168
+ };
164
169
  }
165
170
 
166
171
  /** Run per-tool → global post-hooks. */
@@ -179,6 +184,7 @@ export function createToolRouter<T extends ToolMap>(
179
184
  threadId: options.threadId,
180
185
  turn,
181
186
  durationMs,
187
+ ...(toolResult.metadata && { metadata: toolResult.metadata }),
182
188
  });
183
189
  }
184
190
  if (options.hooks?.onPostToolUse) {
@@ -220,6 +226,7 @@ export function createToolRouter<T extends ToolMap>(
220
226
  let result: unknown;
221
227
  let content!: ToolMessageContent;
222
228
  let resultAppended = false;
229
+ let metadata: Record<string, unknown> | undefined;
223
230
 
224
231
  try {
225
232
  if (tool) {
@@ -236,6 +243,7 @@ export function createToolRouter<T extends ToolMap>(
236
243
  result = response.data;
237
244
  content = response.toolResponse;
238
245
  resultAppended = response.resultAppended === true;
246
+ metadata = response.metadata;
239
247
  } else {
240
248
  result = { error: `Unknown tool: ${toolCall.name}` };
241
249
  content = JSON.stringify(result, null, 2);
@@ -272,6 +280,7 @@ export function createToolRouter<T extends ToolMap>(
272
280
  toolCallId: toolCall.id,
273
281
  name: toolCall.name,
274
282
  data: result,
283
+ ...(metadata && { metadata }),
275
284
  } as ToolCallResultUnion<TResults>;
276
285
 
277
286
  // --- Post-hooks ---
@@ -410,6 +419,7 @@ export function createToolRouter<T extends ToolMap>(
410
419
  toolCallId: toolCall.id,
411
420
  name: toolCall.name as TName,
412
421
  data: response.data,
422
+ ...(response.metadata && { metadata: response.metadata }),
413
423
  };
414
424
  };
415
425
 
@@ -142,6 +142,10 @@ export interface ToolHandlerResponse<TResult = null> {
142
142
  usage?: TokenUsage;
143
143
  /** Thread ID used by the handler (surfaced to the LLM for subagent thread continuation) */
144
144
  threadId?: string;
145
+ /** Sandbox ID created or used by the handler (e.g. child agent sandbox) */
146
+ sandboxId?: string;
147
+ /** Unvalidated metadata passthrough from handler to hooks (e.g. infrastructure state) */
148
+ metadata?: Record<string, unknown>;
145
149
  }
146
150
 
147
151
  /**
@@ -225,6 +229,8 @@ export interface ToolCallResult<
225
229
  name: TName;
226
230
  data: TResult;
227
231
  usage?: TokenUsage;
232
+ /** Unvalidated metadata passthrough from handler to hooks (e.g. infrastructure state) */
233
+ metadata?: Record<string, unknown>;
228
234
  }
229
235
 
230
236
  /**
@@ -301,6 +307,7 @@ export interface ToolHooks<TArgs = unknown, TResult = unknown> {
301
307
  threadId: string;
302
308
  turn: number;
303
309
  durationMs: number;
310
+ metadata?: Record<string, unknown>;
304
311
  }) => void | Promise<void>;
305
312
  /** Called when this tool execution fails */
306
313
  onPostToolUseFailure?: (ctx: {