zeitlich 0.2.22 → 0.2.24

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 +278 -59
  2. package/dist/adapters/sandbox/bedrock/index.cjs +427 -0
  3. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -0
  4. package/dist/adapters/sandbox/bedrock/index.d.cts +23 -0
  5. package/dist/adapters/sandbox/bedrock/index.d.ts +23 -0
  6. package/dist/adapters/sandbox/bedrock/index.js +424 -0
  7. package/dist/adapters/sandbox/bedrock/index.js.map +1 -0
  8. package/dist/adapters/sandbox/bedrock/workflow.cjs +33 -0
  9. package/dist/adapters/sandbox/bedrock/workflow.cjs.map +1 -0
  10. package/dist/adapters/sandbox/bedrock/workflow.d.cts +29 -0
  11. package/dist/adapters/sandbox/bedrock/workflow.d.ts +29 -0
  12. package/dist/adapters/sandbox/bedrock/workflow.js +31 -0
  13. package/dist/adapters/sandbox/bedrock/workflow.js.map +1 -0
  14. package/dist/adapters/sandbox/daytona/index.cjs +4 -1
  15. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/daytona/index.d.cts +2 -1
  17. package/dist/adapters/sandbox/daytona/index.d.ts +2 -1
  18. package/dist/adapters/sandbox/daytona/index.js +4 -1
  19. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  20. package/dist/adapters/sandbox/daytona/workflow.cjs +1 -0
  21. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -1
  22. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  23. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  24. package/dist/adapters/sandbox/daytona/workflow.js +1 -0
  25. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -1
  26. package/dist/adapters/sandbox/inmemory/index.cjs +16 -2
  27. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/inmemory/index.d.cts +3 -2
  29. package/dist/adapters/sandbox/inmemory/index.d.ts +3 -2
  30. package/dist/adapters/sandbox/inmemory/index.js +16 -2
  31. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  32. package/dist/adapters/sandbox/inmemory/workflow.cjs +1 -0
  33. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -1
  34. package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
  35. package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
  36. package/dist/adapters/sandbox/inmemory/workflow.js +1 -0
  37. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -1
  38. package/dist/adapters/sandbox/virtual/index.cjs +45 -11
  39. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  40. package/dist/adapters/sandbox/virtual/index.d.cts +6 -5
  41. package/dist/adapters/sandbox/virtual/index.d.ts +6 -5
  42. package/dist/adapters/sandbox/virtual/index.js +45 -11
  43. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  44. package/dist/adapters/sandbox/virtual/workflow.cjs +1 -0
  45. package/dist/adapters/sandbox/virtual/workflow.cjs.map +1 -1
  46. package/dist/adapters/sandbox/virtual/workflow.d.cts +3 -3
  47. package/dist/adapters/sandbox/virtual/workflow.d.ts +3 -3
  48. package/dist/adapters/sandbox/virtual/workflow.js +1 -0
  49. package/dist/adapters/sandbox/virtual/workflow.js.map +1 -1
  50. package/dist/adapters/thread/google-genai/index.d.cts +3 -3
  51. package/dist/adapters/thread/google-genai/index.d.ts +3 -3
  52. package/dist/adapters/thread/google-genai/workflow.d.cts +3 -3
  53. package/dist/adapters/thread/google-genai/workflow.d.ts +3 -3
  54. package/dist/adapters/thread/langchain/index.d.cts +3 -3
  55. package/dist/adapters/thread/langchain/index.d.ts +3 -3
  56. package/dist/adapters/thread/langchain/workflow.d.cts +3 -3
  57. package/dist/adapters/thread/langchain/workflow.d.ts +3 -3
  58. package/dist/index.cjs +443 -71
  59. package/dist/index.cjs.map +1 -1
  60. package/dist/index.d.cts +64 -10
  61. package/dist/index.d.ts +64 -10
  62. package/dist/index.js +442 -71
  63. package/dist/index.js.map +1 -1
  64. package/dist/{queries-Bw6WEPMw.d.cts → queries-BYGBImeC.d.cts} +1 -1
  65. package/dist/{queries-C27raDaB.d.ts → queries-DwBe2CAA.d.ts} +1 -1
  66. package/dist/{types-C5bkx6kQ.d.ts → types-7PeMi1bD.d.cts} +167 -36
  67. package/dist/{types-BJ8itUAl.d.cts → types-Bf8KV0Ci.d.cts} +6 -6
  68. package/dist/{types-HBosetv3.d.cts → types-ChAMwU3q.d.cts} +2 -0
  69. package/dist/{types-HBosetv3.d.ts → types-ChAMwU3q.d.ts} +2 -0
  70. package/dist/{types-YbL7JpEA.d.cts → types-D_igp10o.d.cts} +11 -0
  71. package/dist/{types-YbL7JpEA.d.ts → types-D_igp10o.d.ts} +11 -0
  72. package/dist/types-DhTCEMhr.d.cts +64 -0
  73. package/dist/{types-ENYCKFBk.d.ts → types-LVKmCNds.d.ts} +6 -6
  74. package/dist/types-d9NznUqd.d.ts +64 -0
  75. package/dist/{types-ClsHhtwL.d.cts → types-hmferhc2.d.ts} +167 -36
  76. package/dist/workflow.cjs +308 -63
  77. package/dist/workflow.cjs.map +1 -1
  78. package/dist/workflow.d.cts +54 -32
  79. package/dist/workflow.d.ts +54 -32
  80. package/dist/workflow.js +306 -61
  81. package/dist/workflow.js.map +1 -1
  82. package/package.json +27 -2
  83. package/src/adapters/sandbox/bedrock/filesystem.ts +313 -0
  84. package/src/adapters/sandbox/bedrock/index.ts +259 -0
  85. package/src/adapters/sandbox/bedrock/proxy.ts +56 -0
  86. package/src/adapters/sandbox/bedrock/types.ts +24 -0
  87. package/src/adapters/sandbox/daytona/filesystem.ts +1 -1
  88. package/src/adapters/sandbox/daytona/index.ts +4 -0
  89. package/src/adapters/sandbox/daytona/proxy.ts +4 -3
  90. package/src/adapters/sandbox/e2b/index.ts +5 -0
  91. package/src/adapters/sandbox/inmemory/index.ts +24 -4
  92. package/src/adapters/sandbox/inmemory/proxy.ts +2 -2
  93. package/src/adapters/sandbox/virtual/filesystem.ts +44 -18
  94. package/src/adapters/sandbox/virtual/provider.ts +13 -0
  95. package/src/adapters/sandbox/virtual/proxy.ts +1 -0
  96. package/src/adapters/sandbox/virtual/types.ts +9 -4
  97. package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +26 -0
  98. package/src/index.ts +2 -1
  99. package/src/lib/lifecycle.ts +57 -0
  100. package/src/lib/sandbox/manager.ts +13 -1
  101. package/src/lib/sandbox/node-fs.ts +115 -0
  102. package/src/lib/sandbox/types.ts +13 -4
  103. package/src/lib/session/index.ts +1 -0
  104. package/src/lib/session/session-edge-cases.integration.test.ts +447 -33
  105. package/src/lib/session/session.integration.test.ts +149 -32
  106. package/src/lib/session/session.ts +138 -33
  107. package/src/lib/session/types.ts +56 -17
  108. package/src/lib/skills/fs-provider.ts +65 -4
  109. package/src/lib/skills/handler.ts +43 -1
  110. package/src/lib/skills/index.ts +0 -1
  111. package/src/lib/skills/register.ts +17 -1
  112. package/src/lib/skills/skills.integration.test.ts +308 -24
  113. package/src/lib/skills/types.ts +6 -0
  114. package/src/lib/subagent/define.ts +5 -4
  115. package/src/lib/subagent/handler.ts +143 -14
  116. package/src/lib/subagent/index.ts +3 -0
  117. package/src/lib/subagent/register.ts +10 -3
  118. package/src/lib/subagent/signals.ts +8 -0
  119. package/src/lib/subagent/subagent.integration.test.ts +853 -150
  120. package/src/lib/subagent/tool.ts +2 -2
  121. package/src/lib/subagent/types.ts +77 -19
  122. package/src/lib/subagent/workflow.ts +83 -12
  123. package/src/lib/tool-router/router.integration.test.ts +137 -4
  124. package/src/lib/tool-router/router.ts +19 -6
  125. package/src/lib/tool-router/types.ts +11 -0
  126. package/src/lib/workflow.test.ts +89 -21
  127. package/src/lib/workflow.ts +33 -18
  128. package/src/workflow.ts +6 -1
  129. package/tsup.config.ts +3 -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,7 +43,7 @@ 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;
@@ -52,6 +59,23 @@ export type SubagentContext =
52
59
  export type InferSubagentResult<T extends SubagentConfig> =
53
60
  T extends SubagentConfig<infer S> ? z.infer<S> : null;
54
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
+
55
79
  /**
56
80
  * Configuration for a subagent that can be spawned by the parent workflow.
57
81
  *
@@ -65,23 +89,34 @@ export interface SubagentConfig<TResult extends z.ZodType = z.ZodType> {
65
89
  /** Whether this subagent is available (default: true). Disabled subagents are excluded from the Subagent tool. */
66
90
  enabled?: boolean | (() => boolean);
67
91
  /** Temporal workflow function or type name (used with executeChild) */
68
- workflow: string | SubagentWorkflow<TResult>;
92
+ workflow: SubagentWorkflow<TResult>;
69
93
  /** Optional task queue - defaults to parent's queue if not specified */
70
94
  taskQueue?: string;
71
95
  /** Optional Zod schema to validate the child workflow's result. If omitted, result is passed through as-is. */
72
96
  resultSchema?: TResult;
73
97
  /** Optional context passed to the subagent — a static object or a function evaluated at invocation time */
74
98
  context?: SubagentContext;
75
- /** Allow the parent agent to pass a threadId for this subagent to continue (default: false) */
76
- allowThreadContinuation?: boolean;
77
99
  /** Per-subagent lifecycle hooks */
78
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";
79
111
  /**
80
112
  * Sandbox strategy for this subagent.
81
- * - `'inherit'` (default): reuse the parent's sandbox (shared filesystem/exec).
82
- * - `'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}
83
118
  */
84
- sandbox?: "inherit" | "own";
119
+ sandbox?: SubagentSandboxConfig;
85
120
  }
86
121
 
87
122
  /**
@@ -102,6 +137,8 @@ export interface SubagentHooks<TArgs = unknown, TResult = unknown> {
102
137
  threadId: string;
103
138
  turn: number;
104
139
  durationMs: number;
140
+ /** Unvalidated metadata from the child workflow (e.g. infrastructure state) */
141
+ metadata?: Record<string, unknown>;
105
142
  }) => void | Promise<void>;
106
143
  /** Called when this subagent execution fails */
107
144
  onExecutionFailure?: (ctx: {
@@ -112,16 +149,37 @@ export interface SubagentHooks<TArgs = unknown, TResult = unknown> {
112
149
  }) => PostToolUseFailureHookResult | Promise<PostToolUseFailureHookResult>;
113
150
  }
114
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
+
115
173
  /**
116
174
  * Session config fields passed from parent to child workflow.
117
175
  */
118
176
  export interface SubagentSessionInput {
119
177
  /** Agent name — spread directly into `createSession` */
120
178
  agentName: string;
121
- /** Thread ID to continue from */
122
- threadId?: string;
123
- /** Whether to continue an existing thread */
124
- continueThread?: boolean;
125
- /** Sandbox ID inherited from the parent agent */
126
- 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;
127
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
@@ -579,7 +579,7 @@ describe("createToolRouter integration", () => {
579
579
  });
580
580
  });
581
581
 
582
- it("throws when handler fails and no hook recovers", async () => {
582
+ it("suppresses error when handler fails and no hook recovers", async () => {
583
583
  const router = createToolRouter({
584
584
  tools: { Fail: failingTool } as const,
585
585
  threadId: "t-1",
@@ -592,9 +592,12 @@ describe("createToolRouter integration", () => {
592
592
  args: { reason: "unrecoverable" },
593
593
  });
594
594
 
595
- await expect(
596
- router.processToolCalls([parsed], { turn: 1 })
597
- ).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
+ });
598
601
  });
599
602
 
600
603
  // --- Disabled tools ---
@@ -707,6 +710,136 @@ describe("createToolRouter integration", () => {
707
710
  expect(router.getResultsByName(results, "Add")).toHaveLength(1);
708
711
  });
709
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
+
710
843
  // --- resultAppended flag ---
711
844
 
712
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) {
@@ -195,7 +201,8 @@ export function createToolRouter<T extends ToolMap>(
195
201
  async function processToolCall(
196
202
  toolCall: ParsedToolCallUnion<T>,
197
203
  turn: number,
198
- sandboxId?: string
204
+ sandboxId?: string,
205
+ sandboxStateUpdate?: Record<string, unknown>
199
206
  ): Promise<ToolCallResultUnion<TResults> | null> {
200
207
  const startTime = Date.now();
201
208
  const tool = toolMap.get(toolCall.name);
@@ -220,6 +227,7 @@ export function createToolRouter<T extends ToolMap>(
220
227
  let result: unknown;
221
228
  let content!: ToolMessageContent;
222
229
  let resultAppended = false;
230
+ let metadata: Record<string, unknown> | undefined;
223
231
 
224
232
  try {
225
233
  if (tool) {
@@ -228,6 +236,7 @@ export function createToolRouter<T extends ToolMap>(
228
236
  toolCallId: toolCall.id,
229
237
  toolName: toolCall.name,
230
238
  ...(sandboxId !== undefined && { sandboxId }),
239
+ ...(sandboxStateUpdate && { sandboxStateUpdate }),
231
240
  };
232
241
  const response = await tool.handler(
233
242
  effectiveArgs as Parameters<typeof tool.handler>[0],
@@ -236,6 +245,7 @@ export function createToolRouter<T extends ToolMap>(
236
245
  result = response.data;
237
246
  content = response.toolResponse;
238
247
  resultAppended = response.resultAppended === true;
248
+ metadata = response.metadata;
239
249
  } else {
240
250
  result = { error: `Unknown tool: ${toolCall.name}` };
241
251
  content = JSON.stringify(result, null, 2);
@@ -272,6 +282,7 @@ export function createToolRouter<T extends ToolMap>(
272
282
  toolCallId: toolCall.id,
273
283
  name: toolCall.name,
274
284
  data: result,
285
+ ...(metadata && { metadata }),
275
286
  } as ToolCallResultUnion<TResults>;
276
287
 
277
288
  // --- Post-hooks ---
@@ -341,10 +352,11 @@ export function createToolRouter<T extends ToolMap>(
341
352
 
342
353
  const turn = context?.turn ?? 0;
343
354
  const sandboxId = context?.sandboxId;
355
+ const sandboxStateUpdate = context?.sandboxStateUpdate;
344
356
 
345
357
  if (options.parallel) {
346
358
  const results = await Promise.all(
347
- toolCalls.map((tc) => processToolCall(tc, turn, sandboxId))
359
+ toolCalls.map((tc) => processToolCall(tc, turn, sandboxId, sandboxStateUpdate))
348
360
  );
349
361
  return results.filter(
350
362
  (r): r is NonNullable<typeof r> => r !== null
@@ -353,7 +365,7 @@ export function createToolRouter<T extends ToolMap>(
353
365
 
354
366
  const results: ToolCallResultUnion<TResults>[] = [];
355
367
  for (const toolCall of toolCalls) {
356
- const result = await processToolCall(toolCall, turn, sandboxId);
368
+ const result = await processToolCall(toolCall, turn, sandboxId, sandboxStateUpdate);
357
369
  if (result !== null) {
358
370
  results.push(result);
359
371
  }
@@ -410,6 +422,7 @@ export function createToolRouter<T extends ToolMap>(
410
422
  toolCallId: toolCall.id,
411
423
  name: toolCall.name as TName,
412
424
  data: response.data,
425
+ ...(response.metadata && { metadata: response.metadata }),
413
426
  };
414
427
  };
415
428
 
@@ -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
  /**
@@ -153,6 +157,8 @@ export interface RouterContext {
153
157
  toolCallId: string;
154
158
  toolName: string;
155
159
  sandboxId?: string;
160
+ /** Sandbox state update from the provider (passed through for subagent inheritance) */
161
+ sandboxStateUpdate?: Record<string, unknown>;
156
162
  }
157
163
 
158
164
  /**
@@ -225,6 +231,8 @@ export interface ToolCallResult<
225
231
  name: TName;
226
232
  data: TResult;
227
233
  usage?: TokenUsage;
234
+ /** Unvalidated metadata passthrough from handler to hooks (e.g. infrastructure state) */
235
+ metadata?: Record<string, unknown>;
228
236
  }
229
237
 
230
238
  /**
@@ -257,6 +265,8 @@ export interface ProcessToolCallsContext {
257
265
  turn?: number;
258
266
  /** Active sandbox ID (when a sandbox is configured for this session) */
259
267
  sandboxId?: string;
268
+ /** Sandbox state update from the provider (threaded to subagent handler for inheritance) */
269
+ sandboxStateUpdate?: Record<string, unknown>;
260
270
  }
261
271
 
262
272
  // ============================================================================
@@ -301,6 +311,7 @@ export interface ToolHooks<TArgs = unknown, TResult = unknown> {
301
311
  threadId: string;
302
312
  turn: number;
303
313
  durationMs: number;
314
+ metadata?: Record<string, unknown>;
304
315
  }) => void | Promise<void>;
305
316
  /** Called when this tool execution fails */
306
317
  onPostToolUseFailure?: (ctx: {