History

implement MCP-style elicitation callbacks for tool execution

Unifies interaction handling around execute/decline/elicit decisions and lets hosts own polling or blocking behavior via callbacks instead of baking retry logic into executor.

Rhys Sullivan committed 8 months ago

Commit 2150c841 parent b4f66967 files · +524 −2Browse files

Unified · Split

7 files
  1. packages/ai-sdk-adapter/src/ai.ts +5 −0
  2. packages/ai-sdk-adapter/src/index.ts +11 −0
  3. packages/codemode-core/src/execution.ts +6 −0
  4. packages/codemode-core/src/index.test.ts +156 −0
  5. packages/codemode-core/src/tool-map.ts +273 −2
  6. packages/codemode-core/src/types.ts +71 −0
  7. packages/kitchen-sink/src/kitchen-sink.test.ts +2 −0
packages/ai-sdk-adapter/src/ai.ts+5−0
@@ -2,6 +2,7 @@ export { · 1 unmodified line
22 CodeToolInputSchema,
33 createCodeTool,
44 createDynamicDiscovery,
5+ allowAllToolInteractions,
56 createStaticDiscoveryFromTools,
67 createSystemToolMap,
78 createToolsFromAiSdkTools,
@@ -10,6 +11,8 @@ export { · 2 unmodified lines
1011 mergeToolMaps,
1112 toExecutorTool,
1213 toTool,
14+ ToolInteractionDeniedError,
15+ ToolInteractionPendingError,
1316 wrapTool,
1417 type AiSdkToolMap,
1518 type CatalogPrimitive,
@@ -30,6 +33,8 @@ export { · 14 unmodified lines
3033 type ToolDescriptor,
3134 type ToolDirectory,
3235 type ToolInput,
36+ type OnElicitation,
37+ type OnToolInteraction,
3338 type ToolInvocationInput,
3439 type ToolInvoker,
3540 type ToolMap,
packages/ai-sdk-adapter/src/index.ts+11−0
@@ -9,10 +9,13 @@ import { · 8 unmodified lines
99 type CodeExecutor,
1010 type CodeToolOutput,
1111 type ExecutableTool,
12+ type OnElicitation,
13+ type OnToolInteraction,
1214 type ToolMap,
1315 } from "@executor-v3/codemode-core";
1416
1517 export {
18+ allowAllToolInteractions,
1619 buildExecuteDescription,
1720 createDiscoveryPrimitives,
1821 createDynamicDiscovery,
@@ -23,6 +26,8 @@ export { · 4 unmodified lines
2326 mergeToolMaps,
2427 toExecutorTool,
2528 toTool,
29+ ToolInteractionDeniedError,
30+ ToolInteractionPendingError,
2631 toolDescriptorsFromTools,
2732 wrapTool,
2833 type CatalogPrimitive,
@@ -42,6 +47,8 @@ export { · 13 unmodified lines
4247 type ToolDescriptor,
4348 type ToolDirectory,
4449 type ToolInput,
50+ type OnElicitation,
51+ type OnToolInteraction,
4552 type ToolInvocationInput,
4653 type ToolInvoker,
4754 type ToolMap,
@@ -70,6 +77,8 @@ export function createToolsFromAiSdkTools(input: { · 22 unmodified lines
7077 export function createCodeTool(input: {
7178 tools: ToolMap;
7279 executor: CodeExecutor;
80+ onToolInteraction?: OnToolInteraction;
81+ onElicitation?: OnElicitation;
7382 description?: string;
7483 }) {
7584 return tool({
@@ -86,6 +95,8 @@ export function createCodeTool(input: { · 10 unmodified lines
8695 code,
8796 tools: input.tools,
8897 executor: input.executor,
98+ onToolInteraction: input.onToolInteraction,
99+ onElicitation: input.onElicitation,
89100 }),
90101 ),
91102 });
packages/codemode-core/src/execution.ts+6−0
@@ -4,6 +4,8 @@ import { makeToolInvokerFromTools } from "./tool-map"; · 3 unmodified lines
44 import type {
55 CodeExecutor,
66 CodeToolOutput,
7+ OnElicitation,
8+ OnToolInteraction,
79 ToolInvoker,
810 ToolMap,
911 } from "./types";
@@ -16,6 +18,8 @@ export const executeCodeWithTools = (input: { · 6 unmodified lines
1618 executor: CodeExecutor;
1719 tools?: ToolMap;
1820 sourceKey?: string;
21+ onToolInteraction?: OnToolInteraction;
22+ onElicitation?: OnElicitation;
1923 toolInvoker?: ToolInvoker;
2024 }): Effect.Effect<CodeToolOutput, Error> =>
2125 Effect.gen(function* () {
@@ -24,6 +28,8 @@ export const executeCodeWithTools = (input: { · 2 unmodified lines
2428 ? makeToolInvokerFromTools({
2529 tools: input.tools,
2630 sourceKey: input.sourceKey,
31+ onToolInteraction: input.onToolInteraction,
32+ onElicitation: input.onElicitation,
2733 })
2834 : null);
2935
packages/codemode-core/src/index.test.ts+156−0
@@ -1,8 +1,12 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
3+import * as Fiber from "effect/Fiber";
4+import * as Option from "effect/Option";
35 import * as Schema from "effect/Schema";
6+import * as TestClock from "effect/TestClock";
47
58 import {
9+ allowAllToolInteractions,
610 createDynamicDiscovery,
711 createStaticDiscoveryFromTools,
812 createSystemToolMap,
@@ -10,6 +14,8 @@ import { · 1 unmodified line
1014 makeToolInvokerFromTools,
1115 mergeToolMaps,
1216 standardSchemaFromJsonSchema,
17+ ToolInteractionDeniedError,
18+ ToolInteractionPendingError,
1319 toTool,
1420 type CodeExecutor,
1521 type ToolDescriptor,
@@ -81,6 +87,155 @@ describe("codemode-core", () => { · 65 unmodified lines
8187 );
8288
8389
90+ it.effect("returns pending interaction error for required tools", () =>
91+ Effect.gen(function* () {
92+ const invoker = makeToolInvokerFromTools({
93+ tools: {
94+ "issues.create": toTool({
95+ tool: {
96+ inputSchema: titleInputSchema,
97+ execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
98+ },
99+ metadata: {
100+ interaction: "required",
101+ },
102+ }),
103+ },
104+ });
105+
106+ const pendingError = yield* Effect.flip(
107+ invoker.invoke({
108+ path: "issues.create",
109+ args: { title: "hello" },
110+ }),
111+ );
112+
113+ expect(pendingError).toBeInstanceOf(ToolInteractionPendingError);
114+ if (pendingError instanceof ToolInteractionPendingError) {
115+ expect(pendingError.path).toBe("issues.create");
116+ expect(pendingError.elicitation.mode).toBe("form");
117+ }
118+ }),
119+ );
120+
121+ it.effect("waits for elicitation response and executes when accepted", () =>
122+ Effect.gen(function* () {
123+ const invoker = makeToolInvokerFromTools({
124+ tools: {
125+ "issues.create": toTool({
126+ tool: {
127+ inputSchema: titleInputSchema,
128+ execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
129+ },
130+ metadata: {
131+ interaction: "required",
132+ },
133+ }),
134+ },
135+ onElicitation: () =>
136+ Effect.succeed({
137+ action: "accept" as const,
138+ }),
139+ });
140+
141+ const output = yield* invoker.invoke({
142+ path: "issues.create",
143+ args: { title: "hello" },
144+ context: { runId: "run_1", callId: "call_1" },
145+ });
146+
147+ expect(output).toEqual({ id: "issue_1", title: "hello" });
148+ }),
149+ );
150+
151+ it.effect("waits through polling-style elicitation callback and then executes", () =>
152+ Effect.gen(function* () {
153+ let pollingSleeps = 0;
154+
155+ const invoker = makeToolInvokerFromTools({
156+ tools: {
157+ "issues.create": toTool({
158+ tool: {
159+ inputSchema: titleInputSchema,
160+ execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
161+ },
162+ metadata: {
163+ interaction: "required",
164+ },
165+ }),
166+ },
167+ onElicitation: () =>
168+ Effect.gen(function* () {
169+ for (let attempt = 0; attempt < 3; attempt += 1) {
170+ pollingSleeps += 1;
171+ yield* Effect.sleep("200 seconds");
172+ }
173+
174+ return {
175+ action: "accept" as const,
176+ };
177+ }),
178+ });
179+
180+ const fiber = yield* invoker.invoke({
181+ path: "issues.create",
182+ args: { title: "hello" },
183+ context: { runId: "run_poll", callId: "call_poll" },
184+ }).pipe(Effect.fork);
185+
186+ yield* TestClock.adjust("200 seconds");
187+ const firstPoll = yield* Fiber.poll(fiber);
188+ expect(Option.isNone(firstPoll)).toBe(true);
189+
190+ yield* TestClock.adjust("200 seconds");
191+ const secondPoll = yield* Fiber.poll(fiber);
192+ expect(Option.isNone(secondPoll)).toBe(true);
193+
194+ yield* TestClock.adjust("200 seconds");
195+ const output = yield* Fiber.join(fiber);
196+
197+ expect(output).toEqual({ id: "issue_1", title: "hello" });
198+ expect(pollingSleeps).toBe(3);
199+ }),
200+ );
201+
202+ it.effect("fails when elicitation is declined", () =>
203+ Effect.gen(function* () {
204+ const invoker = makeToolInvokerFromTools({
205+ tools: {
206+ "issues.create": toTool({
207+ tool: {
208+ inputSchema: titleInputSchema,
209+ execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
210+ },
211+ metadata: {
212+ interaction: "required",
213+ },
214+ }),
215+ },
216+ onElicitation: () =>
217+ Effect.succeed({
218+ action: "decline" as const,
219+ content: {
220+ reason: "User declined tool execution",
221+ },
222+ }),
223+ });
224+
225+ const declinedError = yield* Effect.flip(
226+ invoker.invoke({
227+ path: "issues.create",
228+ args: { title: "hello" },
229+ }),
230+ );
231+
232+ expect(declinedError).toBeInstanceOf(ToolInteractionDeniedError);
233+ if (declinedError instanceof ToolInteractionDeniedError) {
234+ expect(declinedError.reason).toContain("User declined");
235+ }
236+ }),
237+ );
238+
84239 it.effect("builds Standard Schema validators from JSON Schema", () =>
85240 Effect.gen(function* () {
86241 const tools = {
@@ -278,6 +433,7 @@ describe("codemode-core", () => { · 191 unmodified lines
278433 code: "return await tools.math.add({ a: 2, b: 3 });",
279434 tools,
280435 executor,
436+ onToolInteraction: allowAllToolInteractions,
281437 });
282438
283439 expect(output.code).toContain("tools.math.add");
packages/codemode-core/src/tool-map.ts+273−2
@@ -1,11 +1,20 @@
11 import type { StandardSchemaV1 } from "@standard-schema/spec";
2+import * as Data from "effect/Data";
23 import * as Effect from "effect/Effect";
34
45 import type {
6+ ElicitationRequest,
7+ ElicitationResponse,
58 ExecutableTool,
9+ OnElicitation,
10+ OnToolInteraction,
611 ToolDefinition,
712 ToolDescriptor,
13+ ToolElicitationRequest,
814 ToolInput,
15+ ToolInteractionDecision,
16+ ToolInteractionRequest,
17+ ToolInvocationContext,
918 ToolInvoker,
1019 ToolMap,
1120 ToolMetadata,
@@ -98,6 +107,216 @@ const parseInput = (input: { · 86 unmodified lines
98107 );
99108 };
100109
110+const defaultRequiredElicitation = (path: string): ElicitationRequest => ({
111+ mode: "form",
112+ message: `Approval required before invoking ${path}`,
113+ requestedSchema: {
114+ type: "object",
115+ properties: {},
116+ additionalProperties: false,
117+ },
118+});
119+
120+const executeDecision: ToolInteractionDecision = { kind: "execute" };
121+
122+const toElicitationDecision = (input: {
123+ metadata?: ToolMetadata;
124+ path: string;
125+}): ToolInteractionDecision | null => {
126+ if (input.metadata?.elicitation) {
127+ return {
128+ kind: "elicit",
129+ elicitation: input.metadata.elicitation,
130+ };
131+ }
132+
133+ if (input.metadata?.interaction === "required") {
134+ return {
135+ kind: "elicit",
136+ elicitation: defaultRequiredElicitation(input.path),
137+ };
138+ }
139+
140+ return null;
141+};
142+
143+const defaultInteractionDecision = (input: {
144+ metadata?: ToolMetadata;
145+ path: string;
146+}): ToolInteractionDecision =>
147+ toElicitationDecision(input) ?? executeDecision;
148+
149+export class ToolInteractionPendingError extends Data.TaggedError(
150+ "ToolInteractionPendingError",
151+)<{
152+ readonly path: string;
153+ readonly elicitation: ElicitationRequest;
154+ readonly interactionId?: string;
155+}> {}
156+
157+export class ToolInteractionDeniedError extends Data.TaggedError(
158+ "ToolInteractionDeniedError",
159+)<{
160+ readonly path: string;
161+ readonly reason: string;
162+}> {}
163+
164+export const allowAllToolInteractions: OnToolInteraction = () =>
165+ Effect.succeed(executeDecision);
166+
167+const evaluateInteractionDecision = (input: {
168+ path: ToolPath;
169+ args: unknown;
170+ metadata?: ToolMetadata;
171+ sourceKey: string;
172+ context?: ToolInvocationContext;
173+ onToolInteraction?: OnToolInteraction;
174+}): Effect.Effect<ToolInteractionDecision, unknown> => {
175+ const defaultDecision = defaultInteractionDecision({
176+ metadata: input.metadata,
177+ path: input.path,
178+ });
179+
180+ if (!input.onToolInteraction) {
181+ return Effect.succeed(defaultDecision);
182+ }
183+
184+ const request: ToolInteractionRequest = {
185+ path: input.path,
186+ sourceKey: input.sourceKey,
187+ args: input.args,
188+ metadata: input.metadata,
189+ context: input.context,
190+ defaultElicitation:
191+ defaultDecision.kind === "elicit"
192+ ? defaultDecision.elicitation
193+ : null,
194+ };
195+
196+ return input.onToolInteraction(request);
197+};
198+
199+const isRecord = (value: unknown): value is Record<string, unknown> =>
200+ typeof value === "object" && value !== null && !Array.isArray(value);
201+
202+const mergeAcceptedElicitationContent = (input: {
203+ path: ToolPath;
204+ args: unknown;
205+ response: ElicitationResponse;
206+ inputSchema: unknown;
207+}): Effect.Effect<unknown, Error> => {
208+ if (!input.response.content) {
209+ return Effect.succeed(input.args);
210+ }
211+
212+ if (!isRecord(input.args)) {
213+ return Effect.fail(
214+ new Error(
215+ `Tool ${input.path} cannot merge elicitation content into non-object arguments`,
216+ ),
217+ );
218+ }
219+
220+ const merged = {
221+ ...input.args,
222+ ...input.response.content,
223+ };
224+
225+ return parseInput({
226+ schema: input.inputSchema,
227+ value: merged,
228+ path: input.path,
229+ });
230+};
231+
232+const declineReasonFromResponse = (input: {
233+ path: ToolPath;
234+ response: ElicitationResponse;
235+}): string => {
236+ const reason =
237+ input.response.content
238+ && typeof input.response.content.reason === "string"
239+ && input.response.content.reason.trim().length > 0
240+ ? input.response.content.reason.trim()
241+ : null;
242+
243+ if (reason) {
244+ return reason;
245+ }
246+
247+ return input.response.action === "cancel"
248+ ? `Interaction cancelled for ${input.path}`
249+ : `Interaction declined for ${input.path}`;
250+};
251+
252+const resolveInteractionDecision = (input: {
253+ path: ToolPath;
254+ args: unknown;
255+ inputSchema: unknown;
256+ metadata?: ToolMetadata;
257+ sourceKey: string;
258+ context?: ToolInvocationContext;
259+ decision: ToolInteractionDecision;
260+ interactionId: string;
261+ onElicitation?: OnElicitation;
262+}): Effect.Effect<unknown, ToolInteractionPendingError | ToolInteractionDeniedError | Error> => {
263+ if (input.decision.kind === "execute") {
264+ return Effect.succeed(input.args);
265+ }
266+
267+ if (input.decision.kind === "decline") {
268+ return Effect.fail(
269+ new ToolInteractionDeniedError({
270+ path: input.path,
271+ reason: input.decision.reason,
272+ }),
273+ );
274+ }
275+
276+ if (!input.onElicitation) {
277+ return Effect.fail(
278+ new ToolInteractionPendingError({
279+ path: input.path,
280+ elicitation: input.decision.elicitation,
281+ interactionId: input.interactionId,
282+ }),
283+ );
284+ }
285+
286+ const elicitationRequest: ToolElicitationRequest = {
287+ interactionId: input.interactionId,
288+ path: input.path,
289+ sourceKey: input.sourceKey,
290+ args: input.args,
291+ metadata: input.metadata,
292+ context: input.context,
293+ elicitation: input.decision.elicitation,
294+ };
295+
296+ return input.onElicitation(elicitationRequest).pipe(
297+ Effect.mapError(toError),
298+ Effect.flatMap((response) => {
299+ if (response.action !== "accept") {
300+ return Effect.fail(
301+ new ToolInteractionDeniedError({
302+ path: input.path,
303+ reason: declineReasonFromResponse({
304+ path: input.path,
305+ response,
306+ }),
307+ }),
308+ );
309+ }
310+
311+ return mergeAcceptedElicitationContent({
312+ path: input.path,
313+ args: input.args,
314+ response,
315+ inputSchema: input.inputSchema,
316+ });
317+ }),
318+ );
319+};
101320
102321 export function wrapTool(input: {
103322 tool: ExecutableTool;
@@ -241,6 +460,7 @@ export function toolDescriptorsFromTools(input: { · 137 unmodified lines
241460 sourceKey: metadata?.sourceKey ?? "in_memory.tools",
242461 description: definition.description,
243462 interaction: metadata?.interaction,
463+ elicitation: metadata?.elicitation,
244464 inputHint:
245465 metadata?.inputHint ?? inferHintFromSchemaJson(inputSchemaJson, "input"),
246466 outputHint:
@@ -252,18 +472,43 @@ export function toolDescriptorsFromTools(input: { · 5 unmodified lines
252472 });
253473 }
254474
475+const asInteractionIdPart = (value: string): string =>
476+ value.replace(/[^a-zA-Z0-9._-]/g, "_");
477+
478+const createInteractionIdFactory = () => {
479+ let sequence = 0;
480+
481+ return (input: { path: ToolPath; context?: ToolInvocationContext }): string => {
482+ sequence += 1;
483+
484+ const runId =
485+ typeof input.context?.runId === "string" && input.context.runId.length > 0
486+ ? input.context.runId
487+ : "run";
488+ const callId =
489+ typeof input.context?.callId === "string" && input.context.callId.length > 0
490+ ? input.context.callId
491+ : `call_${String(sequence)}`;
492+
493+ return `${asInteractionIdPart(runId)}:${asInteractionIdPart(callId)}:${asInteractionIdPart(String(input.path))}:${String(sequence)}`;
494+ };
495+};
496+
255497 export const makeToolInvokerFromTools = (input: {
256498 tools: ToolMap;
257499 sourceKey?: string;
500+ onToolInteraction?: OnToolInteraction;
501+ onElicitation?: OnElicitation;
258502 }): ToolInvoker => {
259503 const resolvedTools = resolveToolsFromMap({
260504 tools: input.tools,
261505 sourceKey: input.sourceKey,
262506 });
263507 const byPath = new Map(resolvedTools.map((entry) => [entry.path as string, entry]));
508+ const nextInteractionId = createInteractionIdFactory();
264509
265510 return {
266− invoke: ({ path, args }) =>
511+ invoke: ({ path, args, context }) =>
267512 Effect.gen(function* () {
268513 const entry = byPath.get(path);
269514 if (!entry) {
@@ -276,8 +521,34 @@ export const makeToolInvokerFromTools = (input: { · 6 unmodified lines
276521 path,
277522 });
278523
524+ const decision = yield* evaluateInteractionDecision({
525+ path: entry.path,
526+ args: parsedInput,
527+ metadata: entry.metadata,
528+ sourceKey: entry.metadata?.sourceKey ?? "in_memory.tools",
529+ context,
530+ onToolInteraction: input.onToolInteraction,
531+ });
532+
533+ const interactionId =
534+ decision.kind === "elicit" && decision.interactionId
535+ ? decision.interactionId
536+ : nextInteractionId({ path: entry.path, context });
537+
538+ const executableInput = yield* resolveInteractionDecision({
539+ path: entry.path,
540+ args: parsedInput,
541+ inputSchema: entry.tool.inputSchema,
542+ metadata: entry.metadata,
543+ sourceKey: entry.metadata?.sourceKey ?? "in_memory.tools",
544+ context,
545+ decision,
546+ interactionId,
547+ onElicitation: input.onElicitation,
548+ });
549+
279550 return yield* Effect.tryPromise({
280− try: () => Promise.resolve(entry.tool.execute(parsedInput)),
551+ try: () => Promise.resolve(entry.tool.execute(executableInput)),
281552 catch: toError,
282553 });
283554 }),
packages/codemode-core/src/types.ts+71−0
@@ -6,8 +6,77 @@ export type ToolPath = string & { readonly __toolPath: unique symbol }; · 5 unmodified lines
66 export type StandardSchema<Input = unknown, Output = unknown> =
77 StandardSchemaV1<Input, Output>;
88
9+export type ElicitationFormRequest = {
10+ mode?: "form";
11+ message: string;
12+ requestedSchema: Record<string, unknown>;
13+};
14+
15+export type ElicitationUrlRequest = {
16+ mode: "url";
17+ message: string;
18+ url: string;
19+ elicitationId: string;
20+};
21+
22+export type ElicitationRequest =
23+ | ElicitationFormRequest
24+ | ElicitationUrlRequest;
25+
26+export type ElicitationAction = "accept" | "decline" | "cancel";
27+
28+export type ElicitationResponse = {
29+ action: ElicitationAction;
30+ content?: Record<string, unknown>;
31+};
32+
33+export type ToolInvocationContext = {
34+ runId?: string;
35+ callId?: string;
36+ scope?: string;
37+ actor?: string;
38+ [key: string]: unknown;
39+};
40+
41+export type ToolInteractionRequest = {
42+ path: ToolPath;
43+ sourceKey: string;
44+ args: unknown;
45+ metadata?: ToolMetadata;
46+ context?: ToolInvocationContext;
47+ defaultElicitation: ElicitationRequest | null;
48+};
49+
50+export type ToolInteractionDecision =
51+ | { kind: "execute" }
52+ | { kind: "decline"; reason: string }
53+ | {
54+ kind: "elicit";
55+ elicitation: ElicitationRequest;
56+ interactionId?: string;
57+ };
58+
59+export type ToolElicitationRequest = {
60+ interactionId: string;
61+ path: ToolPath;
62+ sourceKey: string;
63+ args: unknown;
64+ metadata?: ToolMetadata;
65+ context?: ToolInvocationContext;
66+ elicitation: ElicitationRequest;
67+};
68+
69+export type OnToolInteraction = (
70+ input: ToolInteractionRequest,
71+) => Effect.Effect<ToolInteractionDecision, unknown>;
72+
73+export type OnElicitation = (
74+ input: ToolElicitationRequest,
75+) => Effect.Effect<ElicitationResponse, unknown>;
76+
977 export type ToolMetadata = {
1078 interaction?: "auto" | "required";
79+ elicitation?: ElicitationRequest;
1180 inputHint?: string;
1281 outputHint?: string;
1382 inputSchemaJson?: string;
@@ -49,6 +118,7 @@ export type ToolDescriptor = { · 35 unmodified lines
49118 sourceKey: string;
50119 description?: string;
51120 interaction?: "auto" | "required";
121+ elicitation?: ElicitationRequest;
52122 inputHint?: string;
53123 outputHint?: string;
54124 inputSchemaJson?: string;
@@ -144,6 +214,7 @@ export type ExecuteResult = { · 89 unmodified lines
144214 export type ToolInvocationInput = {
145215 path: string;
146216 args: unknown;
217+ context?: ToolInvocationContext;
147218 };
148219
149220 export interface ToolInvoker {
packages/kitchen-sink/src/kitchen-sink.test.ts+2−0
@@ -3,6 +3,7 @@ import * as Effect from "effect/Effect"; · 2 unmodified lines
33 import * as Schema from "effect/Schema";
44
55 import {
6+ allowAllToolInteractions,
67 createCodeTool,
78 executeCodeWithTools,
89 makeToolInvokerFromTools,
@@ -55,6 +56,7 @@ describe("kitchen-sink", () => { · 46 unmodified lines
5556 ].join("\n"),
5657 tools,
5758 executor,
59+ onToolInteraction: allowAllToolInteractions,
5860 });
5961
6062 expect(output.result).toEqual({ sum: 42 });