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
2 CodeToolInputSchema,2 CodeToolInputSchema,
3 createCodeTool,3 createCodeTool,
4 createDynamicDiscovery,4 createDynamicDiscovery,
5 allowAllToolInteractions,
5 createStaticDiscoveryFromTools,6 createStaticDiscoveryFromTools,
6 createSystemToolMap,7 createSystemToolMap,
7 createToolsFromAiSdkTools,8 createToolsFromAiSdkTools,
@@ -10,6 +11,8 @@ export { · 2 unmodified lines
10 mergeToolMaps,11 mergeToolMaps,
11 toExecutorTool,12 toExecutorTool,
12 toTool,13 toTool,
14 ToolInteractionDeniedError,
15 ToolInteractionPendingError,
13 wrapTool,16 wrapTool,
14 type AiSdkToolMap,17 type AiSdkToolMap,
15 type CatalogPrimitive,18 type CatalogPrimitive,
@@ -30,6 +33,8 @@ export { · 14 unmodified lines
30 type ToolDescriptor,33 type ToolDescriptor,
31 type ToolDirectory,34 type ToolDirectory,
32 type ToolInput,35 type ToolInput,
36 type OnElicitation,
37 type OnToolInteraction,
33 type ToolInvocationInput,38 type ToolInvocationInput,
34 type ToolInvoker,39 type ToolInvoker,
35 type ToolMap,40 type ToolMap,
packages/ai-sdk-adapter/src/index.ts+11−0
@@ -9,10 +9,13 @@ import { · 8 unmodified lines
9 type CodeExecutor,9 type CodeExecutor,
10 type CodeToolOutput,10 type CodeToolOutput,
11 type ExecutableTool,11 type ExecutableTool,
12 type OnElicitation,
13 type OnToolInteraction,
12 type ToolMap,14 type ToolMap,
13} from "@executor-v3/codemode-core";15} from "@executor-v3/codemode-core";
14 16
15export {17export {
18 allowAllToolInteractions,
16 buildExecuteDescription,19 buildExecuteDescription,
17 createDiscoveryPrimitives,20 createDiscoveryPrimitives,
18 createDynamicDiscovery,21 createDynamicDiscovery,
@@ -23,6 +26,8 @@ export { · 4 unmodified lines
23 mergeToolMaps,26 mergeToolMaps,
24 toExecutorTool,27 toExecutorTool,
25 toTool,28 toTool,
29 ToolInteractionDeniedError,
30 ToolInteractionPendingError,
26 toolDescriptorsFromTools,31 toolDescriptorsFromTools,
27 wrapTool,32 wrapTool,
28 type CatalogPrimitive,33 type CatalogPrimitive,
@@ -42,6 +47,8 @@ export { · 13 unmodified lines
42 type ToolDescriptor,47 type ToolDescriptor,
43 type ToolDirectory,48 type ToolDirectory,
44 type ToolInput,49 type ToolInput,
50 type OnElicitation,
51 type OnToolInteraction,
45 type ToolInvocationInput,52 type ToolInvocationInput,
46 type ToolInvoker,53 type ToolInvoker,
47 type ToolMap,54 type ToolMap,
@@ -70,6 +77,8 @@ export function createToolsFromAiSdkTools(input: { · 22 unmodified lines
70export function createCodeTool(input: {77export function createCodeTool(input: {
71 tools: ToolMap;78 tools: ToolMap;
72 executor: CodeExecutor;79 executor: CodeExecutor;
80 onToolInteraction?: OnToolInteraction;
81 onElicitation?: OnElicitation;
73 description?: string;82 description?: string;
74}) {83}) {
75 return tool({84 return tool({
@@ -86,6 +95,8 @@ export function createCodeTool(input: { · 10 unmodified lines
86 code,95 code,
87 tools: input.tools,96 tools: input.tools,
88 executor: input.executor,97 executor: input.executor,
98 onToolInteraction: input.onToolInteraction,
99 onElicitation: input.onElicitation,
89 }),100 }),
90 ),101 ),
91 });102 });
packages/codemode-core/src/execution.ts+6−0
@@ -4,6 +4,8 @@ import { makeToolInvokerFromTools } from "./tool-map"; · 3 unmodified lines
4import type {4import type {
5 CodeExecutor,5 CodeExecutor,
6 CodeToolOutput,6 CodeToolOutput,
7 OnElicitation,
8 OnToolInteraction,
7 ToolInvoker,9 ToolInvoker,
8 ToolMap,10 ToolMap,
9} from "./types";11} from "./types";
@@ -16,6 +18,8 @@ export const executeCodeWithTools = (input: { · 6 unmodified lines
16 executor: CodeExecutor;18 executor: CodeExecutor;
17 tools?: ToolMap;19 tools?: ToolMap;
18 sourceKey?: string;20 sourceKey?: string;
21 onToolInteraction?: OnToolInteraction;
22 onElicitation?: OnElicitation;
19 toolInvoker?: ToolInvoker;23 toolInvoker?: ToolInvoker;
20}): Effect.Effect<CodeToolOutput, Error> =>24}): Effect.Effect<CodeToolOutput, Error> =>
21 Effect.gen(function* () {25 Effect.gen(function* () {
@@ -24,6 +28,8 @@ export const executeCodeWithTools = (input: { · 2 unmodified lines
24 ? makeToolInvokerFromTools({28 ? makeToolInvokerFromTools({
25 tools: input.tools,29 tools: input.tools,
26 sourceKey: input.sourceKey,30 sourceKey: input.sourceKey,
31 onToolInteraction: input.onToolInteraction,
32 onElicitation: input.onElicitation,
27 })33 })
28 : null);34 : null);
29 35
packages/codemode-core/src/index.test.ts+156−0
@@ -1,8 +1,12 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Fiber from "effect/Fiber";
4import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
6import * as TestClock from "effect/TestClock";
4 7
5import {8import {
9 allowAllToolInteractions,
6 createDynamicDiscovery,10 createDynamicDiscovery,
7 createStaticDiscoveryFromTools,11 createStaticDiscoveryFromTools,
8 createSystemToolMap,12 createSystemToolMap,
@@ -10,6 +14,8 @@ import { · 1 unmodified line
10 makeToolInvokerFromTools,14 makeToolInvokerFromTools,
11 mergeToolMaps,15 mergeToolMaps,
12 standardSchemaFromJsonSchema,16 standardSchemaFromJsonSchema,
17 ToolInteractionDeniedError,
18 ToolInteractionPendingError,
13 toTool,19 toTool,
14 type CodeExecutor,20 type CodeExecutor,
15 type ToolDescriptor,21 type ToolDescriptor,
@@ -81,6 +87,155 @@ describe("codemode-core", () => { · 65 unmodified lines
81 );87 );
82 88
83 89
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
84 it.effect("builds Standard Schema validators from JSON Schema", () =>239 it.effect("builds Standard Schema validators from JSON Schema", () =>
85 Effect.gen(function* () {240 Effect.gen(function* () {
86 const tools = {241 const tools = {
@@ -278,6 +433,7 @@ describe("codemode-core", () => { · 191 unmodified lines
278 code: "return await tools.math.add({ a: 2, b: 3 });",433 code: "return await tools.math.add({ a: 2, b: 3 });",
279 tools,434 tools,
280 executor,435 executor,
436 onToolInteraction: allowAllToolInteractions,
281 });437 });
282 438
283 expect(output.code).toContain("tools.math.add");439 expect(output.code).toContain("tools.math.add");
packages/codemode-core/src/tool-map.ts+273−2
@@ -1,11 +1,20 @@
1import type { StandardSchemaV1 } from "@standard-schema/spec";1import type { StandardSchemaV1 } from "@standard-schema/spec";
2import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3 4
4import type {5import type {
6 ElicitationRequest,
7 ElicitationResponse,
5 ExecutableTool,8 ExecutableTool,
9 OnElicitation,
10 OnToolInteraction,
6 ToolDefinition,11 ToolDefinition,
7 ToolDescriptor,12 ToolDescriptor,
13 ToolElicitationRequest,
8 ToolInput,14 ToolInput,
15 ToolInteractionDecision,
16 ToolInteractionRequest,
17 ToolInvocationContext,
9 ToolInvoker,18 ToolInvoker,
10 ToolMap,19 ToolMap,
11 ToolMetadata,20 ToolMetadata,
@@ -98,6 +107,216 @@ const parseInput = (input: { · 86 unmodified lines
98 );107 );
99};108};
100 109
110const 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
120const executeDecision: ToolInteractionDecision = { kind: "execute" };
121
122const 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
143const defaultInteractionDecision = (input: {
144 metadata?: ToolMetadata;
145 path: string;
146}): ToolInteractionDecision =>
147 toElicitationDecision(input) ?? executeDecision;
148
149export class ToolInteractionPendingError extends Data.TaggedError(
150 "ToolInteractionPendingError",
151)<{
152 readonly path: string;
153 readonly elicitation: ElicitationRequest;
154 readonly interactionId?: string;
155}> {}
156
157export class ToolInteractionDeniedError extends Data.TaggedError(
158 "ToolInteractionDeniedError",
159)<{
160 readonly path: string;
161 readonly reason: string;
162}> {}
163
164export const allowAllToolInteractions: OnToolInteraction = () =>
165 Effect.succeed(executeDecision);
166
167const 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
199const isRecord = (value: unknown): value is Record<string, unknown> =>
200 typeof value === "object" && value !== null && !Array.isArray(value);
201
202const 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
232const 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
252const 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};
101 320
102export function wrapTool(input: {321export function wrapTool(input: {
103 tool: ExecutableTool;322 tool: ExecutableTool;
@@ -241,6 +460,7 @@ export function toolDescriptorsFromTools(input: { · 137 unmodified lines
241 sourceKey: metadata?.sourceKey ?? "in_memory.tools",460 sourceKey: metadata?.sourceKey ?? "in_memory.tools",
242 description: definition.description,461 description: definition.description,
243 interaction: metadata?.interaction,462 interaction: metadata?.interaction,
463 elicitation: metadata?.elicitation,
244 inputHint:464 inputHint:
245 metadata?.inputHint ?? inferHintFromSchemaJson(inputSchemaJson, "input"),465 metadata?.inputHint ?? inferHintFromSchemaJson(inputSchemaJson, "input"),
246 outputHint:466 outputHint:
@@ -252,18 +472,43 @@ export function toolDescriptorsFromTools(input: { · 5 unmodified lines
252 });472 });
253}473}
254 474
475const asInteractionIdPart = (value: string): string =>
476 value.replace(/[^a-zA-Z0-9._-]/g, "_");
477
478const 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
255export const makeToolInvokerFromTools = (input: {497export const makeToolInvokerFromTools = (input: {
256 tools: ToolMap;498 tools: ToolMap;
257 sourceKey?: string;499 sourceKey?: string;
500 onToolInteraction?: OnToolInteraction;
501 onElicitation?: OnElicitation;
258}): ToolInvoker => {502}): ToolInvoker => {
259 const resolvedTools = resolveToolsFromMap({503 const resolvedTools = resolveToolsFromMap({
260 tools: input.tools,504 tools: input.tools,
261 sourceKey: input.sourceKey,505 sourceKey: input.sourceKey,
262 });506 });
263 const byPath = new Map(resolvedTools.map((entry) => [entry.path as string, entry]));507 const byPath = new Map(resolvedTools.map((entry) => [entry.path as string, entry]));
508 const nextInteractionId = createInteractionIdFactory();
264 509
265 return {510 return {
266 invoke: ({ path, args }) =>511 invoke: ({ path, args, context }) =>
267 Effect.gen(function* () {512 Effect.gen(function* () {
268 const entry = byPath.get(path);513 const entry = byPath.get(path);
269 if (!entry) {514 if (!entry) {
@@ -276,8 +521,34 @@ export const makeToolInvokerFromTools = (input: { · 6 unmodified lines
276 path,521 path,
277 });522 });
278 523
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
279 return yield* Effect.tryPromise({550 return yield* Effect.tryPromise({
280 try: () => Promise.resolve(entry.tool.execute(parsedInput)),551 try: () => Promise.resolve(entry.tool.execute(executableInput)),
281 catch: toError,552 catch: toError,
282 });553 });
283 }),554 }),
packages/codemode-core/src/types.ts+71−0
@@ -6,8 +6,77 @@ export type ToolPath = string & { readonly __toolPath: unique symbol }; · 5 unmodified lines
6export type StandardSchema<Input = unknown, Output = unknown> =6export type StandardSchema<Input = unknown, Output = unknown> =
7 StandardSchemaV1<Input, Output>;7 StandardSchemaV1<Input, Output>;
8 8
9export type ElicitationFormRequest = {
10 mode?: "form";
11 message: string;
12 requestedSchema: Record<string, unknown>;
13};
14
15export type ElicitationUrlRequest = {
16 mode: "url";
17 message: string;
18 url: string;
19 elicitationId: string;
20};
21
22export type ElicitationRequest =
23 | ElicitationFormRequest
24 | ElicitationUrlRequest;
25
26export type ElicitationAction = "accept" | "decline" | "cancel";
27
28export type ElicitationResponse = {
29 action: ElicitationAction;
30 content?: Record<string, unknown>;
31};
32
33export type ToolInvocationContext = {
34 runId?: string;
35 callId?: string;
36 scope?: string;
37 actor?: string;
38 [key: string]: unknown;
39};
40
41export type ToolInteractionRequest = {
42 path: ToolPath;
43 sourceKey: string;
44 args: unknown;
45 metadata?: ToolMetadata;
46 context?: ToolInvocationContext;
47 defaultElicitation: ElicitationRequest | null;
48};
49
50export type ToolInteractionDecision =
51 | { kind: "execute" }
52 | { kind: "decline"; reason: string }
53 | {
54 kind: "elicit";
55 elicitation: ElicitationRequest;
56 interactionId?: string;
57 };
58
59export 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
69export type OnToolInteraction = (
70 input: ToolInteractionRequest,
71) => Effect.Effect<ToolInteractionDecision, unknown>;
72
73export type OnElicitation = (
74 input: ToolElicitationRequest,
75) => Effect.Effect<ElicitationResponse, unknown>;
76
9export type ToolMetadata = {77export type ToolMetadata = {
10 interaction?: "auto" | "required";78 interaction?: "auto" | "required";
79 elicitation?: ElicitationRequest;
11 inputHint?: string;80 inputHint?: string;
12 outputHint?: string;81 outputHint?: string;
13 inputSchemaJson?: string;82 inputSchemaJson?: string;
@@ -49,6 +118,7 @@ export type ToolDescriptor = { · 35 unmodified lines
49 sourceKey: string;118 sourceKey: string;
50 description?: string;119 description?: string;
51 interaction?: "auto" | "required";120 interaction?: "auto" | "required";
121 elicitation?: ElicitationRequest;
52 inputHint?: string;122 inputHint?: string;
53 outputHint?: string;123 outputHint?: string;
54 inputSchemaJson?: string;124 inputSchemaJson?: string;
@@ -144,6 +214,7 @@ export type ExecuteResult = { · 89 unmodified lines
144export type ToolInvocationInput = {214export type ToolInvocationInput = {
145 path: string;215 path: string;
146 args: unknown;216 args: unknown;
217 context?: ToolInvocationContext;
147};218};
148 219
149export interface ToolInvoker {220export 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
3import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
4 4
5import {5import {
6 allowAllToolInteractions,
6 createCodeTool,7 createCodeTool,
7 executeCodeWithTools,8 executeCodeWithTools,
8 makeToolInvokerFromTools,9 makeToolInvokerFromTools,
@@ -55,6 +56,7 @@ describe("kitchen-sink", () => { · 46 unmodified lines
55 ].join("\n"),56 ].join("\n"),
56 tools,57 tools,
57 executor,58 executor,
59 onToolInteraction: allowAllToolInteractions,
58 });60 });
59 61
60 expect(output.result).toEqual({ sum: 42 });62 expect(output.result).toEqual({ sum: 42 });