feat(eve): expose the tool call id on ToolContext and ApprovalContext (#545)

This commit is contained in:
Matan Kushner
2026-07-05 16:17:28 -04:00
committed by GitHub
parent e2d7c577ce
commit f3a05c5ef7
22 changed files with 172 additions and 41 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"eve": patch
---
`ToolContext` and `ApprovalContext` now expose `callId`, the tool call id carried by the call's stream events, so approval-gated tools can key records to one identity across proposal, rejection, and execution.
+1 -1
View File
@@ -38,7 +38,7 @@ export default defineTool({
By default, omitted `approval` behaves like `never()`, so tool calls may execute without human approval. Require human approval or other safeguards for sensitive, irreversible, regulated, financial, healthcare, employment, housing, legal, safety-impacting, user-impacting, or external side-effecting actions. By default, omitted `approval` behaves like `never()`, so tool calls may execute without human approval. Require human approval or other safeguards for sensitive, irreversible, regulated, financial, healthcare, employment, housing, legal, safety-impacting, user-impacting, or external side-effecting actions.
When the decision depends on the input, pass your own policy instead of a helper. It receives the same session context as tool execution, plus `{ toolName, toolInput, approvedTools }`, and returns an AI SDK 7 approval status synchronously or as a promise. Use `ctx.session.auth.current` to guard by the caller of the current turn and `ctx.session.auth.initiator` to guard by the caller that created the session. Return `"user-approval"` to pause for a person or `"not-applicable"` to continue without a prompt. `toolInput` can be undefined, so guard the access. This policy denies cross-tenant calls, then requires approval only when an amount crosses a threshold: When the decision depends on the input, pass your own policy instead of a helper. It receives the same session context as tool execution, plus `{ toolName, toolInput, approvedTools, callId }`, and returns an AI SDK 7 approval status synchronously or as a promise. Use `ctx.session.auth.current` to guard by the caller of the current turn and `ctx.session.auth.initiator` to guard by the caller that created the session. Return `"user-approval"` to pause for a person or `"not-applicable"` to continue without a prompt. `toolInput` can be undefined, so guard the access. This policy denies cross-tenant calls, then requires approval only when an amount crosses a threshold:
```ts ```ts
approval: ({ session, toolInput }) => { approval: ({ session, toolInput }) => {
+1
View File
@@ -37,6 +37,7 @@ When a tool returns structured data, add an optional `outputSchema`. With Zod or
`execute` gets a `ctx` carrying the runtime accessors: `execute` gets a `ctx` carrying the runtime accessors:
- `ctx.session`: session metadata, turn, auth, parent lineage. - `ctx.session`: session metadata, turn, auth, parent lineage.
- `ctx.callId`: the id of the current tool call, carried by the call's [stream events](/docs/concepts/sessions-runs-and-streaming) and approval context.
- `ctx.abortSignal`: aborts when the active turn is cancelled. Pass it to cancellation-aware work; sandbox sessions from `ctx.getSandbox()` are already bound to it. - `ctx.abortSignal`: aborts when the active turn is cancelled. Pass it to cancellation-aware work; sandbox sessions from `ctx.getSandbox()` are already bound to it.
- `ctx.getSandbox()`: the live [sandbox](/docs/sandbox) handle. - `ctx.getSandbox()`: the live [sandbox](/docs/sandbox) handle.
- `ctx.getSkill(id)`: read a packaged [skill](/docs/skills)'s metadata and files. - `ctx.getSkill(id)`: read a packaged [skill](/docs/skills)'s metadata and files.
@@ -17,7 +17,7 @@ describe("buildBaseToolContext getSandbox abort binding", () => {
const controller = new AbortController(); const controller = new AbortController();
await runtime.runAsSession({ sandbox }, async () => { await runtime.runAsSession({ sandbox }, async () => {
const ctx = buildBaseToolContext(controller.signal); const ctx = buildBaseToolContext({ abortSignal: controller.signal, toolCallId: "call_1" });
const live = await ctx.getSandbox(); const live = await ctx.getSandbox();
await live.run({ command: "echo ready" }); await live.run({ command: "echo ready" });
}); });
@@ -36,7 +36,7 @@ describe("buildBaseToolContext getSandbox abort binding", () => {
const runtime = createTestRuntime(); const runtime = createTestRuntime();
await runtime.runAsSession({ sandbox }, async () => { await runtime.runAsSession({ sandbox }, async () => {
const ctx = buildBaseToolContext(undefined); const ctx = buildBaseToolContext({ toolCallId: "call_1" });
const live = await ctx.getSandbox(); const live = await ctx.getSandbox();
await live.run({ command: "echo ready" }); await live.run({ command: "echo ready" });
}); });
@@ -1,20 +1,25 @@
import { buildCallbackContext } from "#context/build-callback-context.js"; import { buildCallbackContext } from "#context/build-callback-context.js";
import type { SessionContext } from "#public/definitions/callback-context.js"; import type { SessionContext } from "#public/definitions/callback-context.js";
import { bindSandboxAbortSignal } from "#execution/sandbox/abort-bound-session.js"; import { bindSandboxAbortSignal } from "#execution/sandbox/abort-bound-session.js";
import type { ToolExecuteOptions } from "#shared/tool-definition.js";
/** Base context shared by tool executors. */ /** Base context shared by tool executors. */
export type BaseToolContext = SessionContext & { export type BaseToolContext = SessionContext & {
readonly abortSignal: AbortSignal; readonly abortSignal: AbortSignal;
readonly callId: string;
}; };
/** Builds the base context for one tool execution. */ /** Builds the base context for one tool execution. */
export function buildBaseToolContext(abortSignal: AbortSignal | undefined): BaseToolContext { export function buildBaseToolContext(
options: Pick<ToolExecuteOptions, "abortSignal" | "toolCallId">,
): BaseToolContext {
const callbackContext = buildCallbackContext(); const callbackContext = buildCallbackContext();
const signal = abortSignal ?? new AbortController().signal; const signal = options.abortSignal ?? new AbortController().signal;
return { return {
...callbackContext, ...callbackContext,
abortSignal: signal, abortSignal: signal,
callId: options.toolCallId,
getSandbox: async () => bindSandboxAbortSignal(await callbackContext.getSandbox(), signal), getSandbox: async () => bindSandboxAbortSignal(await callbackContext.getSandbox(), signal),
}; };
} }
@@ -51,7 +51,7 @@ function replayTools(metadata: readonly DurableDynamicToolMetadata[]): HarnessTo
tools.push({ tools.push({
description: m.description, description: m.description,
execute: (input: unknown, options) => execute: (input: unknown, options) =>
stepFn(m.closureVars, input, buildBaseToolContext(options?.abortSignal)), stepFn(m.closureVars, input, buildBaseToolContext(options)),
inputSchema: jsonSchema(m.inputSchema), inputSchema: jsonSchema(m.inputSchema),
name: m.name, name: m.name,
approval: buildReplayedApproval(m), approval: buildReplayedApproval(m),
@@ -50,6 +50,8 @@ function qualifyDynamicToolNames(
return result; return result;
} }
const executeOptions = { messages: [], toolCallId: "call_1" };
const stubEntry = defineTool({ const stubEntry = defineTool({
description: "test", description: "test",
inputSchema: { type: "object" }, inputSchema: { type: "object" },
@@ -278,7 +280,7 @@ describe("replayDynamicSessionTools", () => {
// Execute the replayed tool — mock provides the callback context // Execute the replayed tool — mock provides the callback context
const tool = tools[0]!; const tool = tools[0]!;
tool.execute!({ query: "test" }); tool.execute!({ query: "test" }, executeOptions);
expect(stepFn).toHaveBeenCalledWith( expect(stepFn).toHaveBeenCalledWith(
{ apiUrl: "https://api.example.com", tenantName: "Acme" }, { apiUrl: "https://api.example.com", tenantName: "Acme" },
{ query: "test" }, { query: "test" },
@@ -319,11 +321,11 @@ describe("replayDynamicSessionTools", () => {
const tools = replayDynamicSessionTools(metadata, []); const tools = replayDynamicSessionTools(metadata, []);
const tool = tools[0]!; const tool = tools[0]!;
tool.execute!({}); tool.execute!({}, executeOptions);
// Mutating the metadata object after replay should NOT affect calls // Mutating the metadata object after replay should NOT affect calls
closureVars.counter = 999; closureVars.counter = 999;
tool.execute!({}); tool.execute!({}, executeOptions);
// Both calls get the same closure vars reference from metadata. // Both calls get the same closure vars reference from metadata.
// This documents current behavior: replay passes by reference. // This documents current behavior: replay passes by reference.
@@ -451,6 +453,7 @@ function createApprovalContext(input: {
}): ApprovalContext { }): ApprovalContext {
return { return {
approvedTools: new Set(), approvedTools: new Set(),
callId: "call_1",
getSandbox: vi.fn(), getSandbox: vi.fn(),
getSkill: vi.fn(), getSkill: vi.fn(),
session: { session: {
@@ -794,7 +797,7 @@ describe("framework dynamic tools (no bundler transform)", () => {
expect(replayedTools[0]!.name).toBe("search"); expect(replayedTools[0]!.name).toBe("search");
// Execute the replayed tool — the original closure is invoked // Execute the replayed tool — the original closure is invoked
await replayedTools[0]!.execute!({ query: "test" }); await replayedTools[0]!.execute!({ query: "test" }, executeOptions);
expect(executeFn).toHaveBeenCalledWith({ query: "test" }); expect(executeFn).toHaveBeenCalledWith({ query: "test" });
}); });
@@ -822,7 +825,7 @@ describe("framework dynamic tools (no bundler transform)", () => {
expect(tools).toHaveLength(1); expect(tools).toHaveLength(1);
expect(tools[0]!.name).toBe("assist"); expect(tools[0]!.name).toBe("assist");
await tools[0]!.execute!({ action: "help" }); await tools[0]!.execute!({ action: "help" }, executeOptions);
expect(executeFn).toHaveBeenCalledWith({ action: "help" }); expect(executeFn).toHaveBeenCalledWith({ action: "help" });
}); });
@@ -1006,7 +1009,7 @@ describe("framework dynamic tools (no bundler transform)", () => {
ctx.clearVirtualContext(); ctx.clearVirtualContext();
let tools = buildDynamicTools(ctx); let tools = buildDynamicTools(ctx);
const result1 = await tools[0]!.execute!({}); const result1 = await tools[0]!.execute!({}, executeOptions);
expect(result1).toEqual({ version: 1 }); expect(result1).toEqual({ version: 1 });
// Re-dispatch overwrites the resolver's slot // Re-dispatch overwrites the resolver's slot
@@ -1020,7 +1023,7 @@ describe("framework dynamic tools (no bundler transform)", () => {
ctx.clearVirtualContext(); ctx.clearVirtualContext();
tools = buildDynamicTools(ctx); tools = buildDynamicTools(ctx);
expect(tools[0]!.description).toBe("v2"); expect(tools[0]!.description).toBe("v2");
const result2 = await tools[0]!.execute!({}); const result2 = await tools[0]!.execute!({}, executeOptions);
expect(result2).toEqual({ version: 2 }); expect(result2).toEqual({ version: 2 });
}); });
}); });
@@ -33,7 +33,7 @@ function toHarnessToolDefinition(name: string, entry: DynamicToolEntry): Harness
return { return {
description: entry.description, description: entry.description,
execute: (input: unknown, options) => execute: (input: unknown, options) =>
entry.execute(input as Record<string, unknown>, buildBaseToolContext(options?.abortSignal)), entry.execute(input as Record<string, unknown>, buildBaseToolContext(options)),
inputSchema: convertInputSchema(entry.inputSchema), inputSchema: convertInputSchema(entry.inputSchema),
name, name,
approval: entry.approval, approval: entry.approval,
@@ -119,7 +119,7 @@ export function replayDynamicSessionTools(
tools.push({ tools.push({
description: m.description, description: m.description,
execute: (input: unknown, options) => execute: (input: unknown, options) =>
stepFn(m.closureVars, input, buildBaseToolContext(options?.abortSignal)), stepFn(m.closureVars, input, buildBaseToolContext(options)),
inputSchema: jsonSchema(m.inputSchema), inputSchema: jsonSchema(m.inputSchema),
name: m.name, name: m.name,
outputSchema: m.outputSchema === undefined ? undefined : jsonSchema(m.outputSchema), outputSchema: m.outputSchema === undefined ? undefined : jsonSchema(m.outputSchema),
@@ -98,6 +98,21 @@ describe("tool-hosted authorization", () => {
expect(calls).toBe(1); expect(calls).toBe(1);
}); });
it("exposes the tool call id on the authored context", async () => {
const tool = authoredTool({
name: "observe_call_id",
execute(_input, ctx) {
return { callId: ctx.callId };
},
});
const runtime = createTestRuntime({ tools: [tool] });
const result = await runtime.runAsSession(undefined, async () => runtime.executeTool(tool, {}));
// The test harness dispatches every executeTool call as "call_test".
expect(result).toEqual({ callId: "call_test" });
});
it("resolves and caches an inline provider on a plain tool", async () => { it("resolves and caches an inline provider on a plain tool", async () => {
let calls = 0; let calls = 0;
const inlineAuth: AuthorizationDefinition = { const inlineAuth: AuthorizationDefinition = {
+5 -5
View File
@@ -54,19 +54,19 @@ import type { ToolExecuteOptions } from "#shared/tool-definition.js";
export function createToolExecuteWithAuth(input: { export function createToolExecuteWithAuth(input: {
readonly scope: string; readonly scope: string;
readonly execute: (toolInput: unknown, ctx: unknown) => unknown; readonly execute: (toolInput: unknown, ctx: unknown) => unknown;
}): (toolInput: unknown, options?: ToolExecuteOptions) => Promise<unknown> { }): (toolInput: unknown, options: ToolExecuteOptions) => Promise<unknown> {
const { scope, execute } = input; const { scope, execute } = input;
return async (toolInput: unknown, options?: ToolExecuteOptions): Promise<unknown> => { return async (toolInput: unknown, options: ToolExecuteOptions): Promise<unknown> => {
const justAuthorizedScopes = new Set<string>(); const justAuthorizedScopes = new Set<string>();
try { try {
return await execute( return await execute(
toolInput, toolInput,
buildToolContext({ buildToolContext({
abortSignal: options?.abortSignal,
inlineAuthState: {}, inlineAuthState: {},
justAuthorizedScopes, justAuthorizedScopes,
options,
scope, scope,
}), }),
); );
@@ -81,13 +81,13 @@ export function createToolExecuteWithAuth(input: {
} }
function buildToolContext(input: { function buildToolContext(input: {
readonly abortSignal: AbortSignal | undefined; readonly options: ToolExecuteOptions;
readonly scope: string; readonly scope: string;
readonly justAuthorizedScopes: Set<string>; readonly justAuthorizedScopes: Set<string>;
readonly inlineAuthState: InlineAuthState; readonly inlineAuthState: InlineAuthState;
}): ToolContext { }): ToolContext {
const { scope, justAuthorizedScopes, inlineAuthState } = input; const { scope, justAuthorizedScopes, inlineAuthState } = input;
const base = buildBaseToolContext(input.abortSignal); const base = buildBaseToolContext(input.options);
return { return {
...base, ...base,
async getToken(provider?: ToolAuthProvider, options?: ToolAuthOptions): Promise<TokenResult> { async getToken(provider?: ToolAuthProvider, options?: ToolAuthOptions): Promise<TokenResult> {
+1 -1
View File
@@ -22,7 +22,7 @@ export type HarnessRuntimeActionDefinition = {
export interface HarnessToolDefinition { export interface HarnessToolDefinition {
readonly approvalKey?: (toolInput: Readonly<Record<string, unknown>>) => string; readonly approvalKey?: (toolInput: Readonly<Record<string, unknown>>) => string;
readonly description: string; readonly description: string;
readonly execute?: (input: any, options?: ToolExecuteOptions) => any; readonly execute?: (input: any, options: ToolExecuteOptions) => any;
readonly inputSchema: FlexibleSchema; readonly inputSchema: FlexibleSchema;
readonly name: string; readonly name: string;
readonly approval?: Approval; readonly approval?: Approval;
+58
View File
@@ -198,6 +198,40 @@ describe("buildToolSet", () => {
expect(receivedSignal?.aborted).toBe(false); expect(receivedSignal?.aborted).toBe(false);
}); });
it("passes the AI SDK toolCallId to the authored tool context as callId", async () => {
let receivedCallId: string | undefined;
const tools: HarnessToolMap = new Map<string, HarnessToolDefinition>([
[
"observe_call_id",
{
description: "Observe the tool call id.",
execute: createToolExecuteWithAuth({
execute(_input, ctx) {
receivedCallId = (ctx as ToolContext).callId;
return { ok: true };
},
scope: "observe_call_id",
}),
inputSchema: jsonSchema({ type: "object" }),
name: "observe_call_id",
},
],
]);
const ctx = new ContextContainer();
ctx.set(SessionKey, {
auth: { current: null, initiator: null },
sessionId: "session-1",
turn: { id: "turn-1", sequence: 0 },
});
const result = buildToolSet({ tools });
await contextStorage.run(ctx, () =>
executeSdkTool({ tool: result.observe_call_id, toolCallId: "call_observe" }),
);
expect(receivedCallId).toBe("call_observe");
});
it("passes through the input schema to the SDK tool", () => { it("passes through the input schema to the SDK tool", () => {
const schema = { const schema = {
properties: { city: { type: "string" } }, properties: { city: { type: "string" } },
@@ -815,6 +849,30 @@ describe("buildToolSet", () => {
expect(capturedInput).toEqual(toolInput); expect(capturedInput).toEqual(toolInput);
}); });
it("passes the callId from the AI SDK into approval", async () => {
let capturedCallId: string | undefined;
const tools: HarnessToolMap = new Map<string, HarnessToolDefinition>([
[
"vercel__list_projects",
{
description: "List projects in the team.",
execute: async () => "ok",
inputSchema: jsonSchema({}),
name: "vercel__list_projects",
approval: (ctx) => {
capturedCallId = ctx.callId;
return "user-approval";
},
},
],
]);
const result = buildToolSet({ tools });
await resolveApproval(result, "vercel__list_projects", {});
expect(capturedCallId).toBe("call_1");
});
it("passes the active caller and session context into approval", async () => { it("passes the active caller and session context into approval", async () => {
let capturedCtx: Parameters<NonNullable<HarnessToolDefinition["approval"]>>[0] | undefined; let capturedCtx: Parameters<NonNullable<HarnessToolDefinition["approval"]>>[0] | undefined;
const tools: HarnessToolMap = new Map<string, HarnessToolDefinition>([ const tools: HarnessToolMap = new Map<string, HarnessToolDefinition>([
+8 -4
View File
@@ -34,7 +34,10 @@ type ToolModelOutputValue =
type NativeApprovalStatus = Exclude<ApprovalStatus, boolean>; type NativeApprovalStatus = Exclude<ApprovalStatus, boolean>;
const toolApprovals = new WeakMap<object, (toolInput: unknown) => Promise<NativeApprovalStatus>>(); const toolApprovals = new WeakMap<
object,
(toolInput: unknown, callId: string) => Promise<NativeApprovalStatus>
>();
/** /**
* Builds an AI SDK `ToolSet` from unified harness tool definitions. * Builds an AI SDK `ToolSet` from unified harness tool definitions.
@@ -302,8 +305,8 @@ export async function buildToolSetWithProviderTools(input: {
function buildApprovalFn( function buildApprovalFn(
definition: HarnessToolDefinition, definition: HarnessToolDefinition,
input: { readonly approvedTools?: ReadonlySet<string> }, input: { readonly approvedTools?: ReadonlySet<string> },
): (toolInput: unknown) => Promise<NativeApprovalStatus> { ): (toolInput: unknown, callId: string) => Promise<NativeApprovalStatus> {
return async (toolInput: unknown) => { return async (toolInput: unknown, callId: string) => {
if (definition.approval === undefined) return undefined; if (definition.approval === undefined) return undefined;
const toolInputRecord = isObject(toolInput) ? toolInput : undefined; const toolInputRecord = isObject(toolInput) ? toolInput : undefined;
@@ -311,6 +314,7 @@ function buildApprovalFn(
const status = await definition.approval({ const status = await definition.approval({
...buildCallbackContext(), ...buildCallbackContext(),
approvedTools: input.approvedTools ?? new Set(), approvedTools: input.approvedTools ?? new Set(),
callId,
toolInput: toolInputRecord, toolInput: toolInputRecord,
toolName: definition.name, toolName: definition.name,
}); });
@@ -327,6 +331,6 @@ export function buildToolApproval(
if (toolDefinition === undefined) return undefined; if (toolDefinition === undefined) return undefined;
const approval = toolApprovals.get(toolDefinition); const approval = toolApprovals.get(toolDefinition);
return (await approval?.(toolCall.input)) as ToolApprovalStatus; return (await approval?.(toolCall.input, toolCall.toolCallId)) as ToolApprovalStatus;
}; };
} }
@@ -224,7 +224,7 @@ export function createTestRuntime(descriptor: TestAppDescriptor = {}): TestRunti
throw new Error(`Tool "${tool.name}" is not executable.`); throw new Error(`Tool "${tool.name}" is not executable.`);
} }
return await execute(input); return await execute(input, { messages: [], toolCallId: "call_test" });
} }
return { return {
@@ -31,8 +31,8 @@ export default defineTool({
...bash, ...bash,
description: "Run a vetted shell command in the project sandbox.", description: "Run a vetted shell command in the project sandbox.",
approval: always(), approval: always(),
async execute(input) { async execute(input, ctx) {
return bash.execute(input); return bash.execute(input, ctx);
}, },
}); });
`, `,
@@ -11,10 +11,13 @@ type ApprovalToolInput<TInput> = TInput extends object ? Readonly<TInput> : TInp
* `approvedTools` is the set of tool names (or compound approval keys) * `approvedTools` is the set of tool names (or compound approval keys)
* already approved at least once in the current session. `toolName` is the * already approved at least once in the current session. `toolName` is the
* runtime name of the tool being evaluated. `toolInput` is the raw input the * runtime name of the tool being evaluated. `toolInput` is the raw input the
* model passed, available for input-aware decisions. * model passed, available for input-aware decisions. `callId` is the id of
* the call being evaluated the same `callId` carried by the call's stream
* events and its `execute` context.
*/ */
export interface ApprovalContext<TInput = Record<string, unknown>> extends SessionContext { export interface ApprovalContext<TInput = Record<string, unknown>> extends SessionContext {
readonly approvedTools: ReadonlySet<string>; readonly approvedTools: ReadonlySet<string>;
readonly callId: string;
readonly toolInput?: ApprovalToolInput<TInput>; readonly toolInput?: ApprovalToolInput<TInput>;
readonly toolName: string; readonly toolName: string;
} }
@@ -74,6 +74,11 @@ export interface ToolAuthOptions {
export type ToolContext = SessionContext & { export type ToolContext = SessionContext & {
/** Aborts when the active turn is cancelled. */ /** Aborts when the active turn is cancelled. */
readonly abortSignal: AbortSignal; readonly abortSignal: AbortSignal;
/**
* Id of the current tool call the same `callId` carried by the call's
* stream events and its {@link ApprovalContext}.
*/
readonly callId: string;
/** /**
* Resolves the bearer token for an inline provider. This accepts the same * Resolves the bearer token for an inline provider. This accepts the same
* auth shapes as a connection's `auth` field, including `connect("...")` * auth shapes as a connection's `auth` field, including `connect("...")`
+11 -3
View File
@@ -8,9 +8,10 @@ import type { ToolDefinition } from "#public/definitions/tool.js";
* {@link ResolvedToolDefinition} so it can be re-exported as a public * {@link ResolvedToolDefinition} so it can be re-exported as a public
* {@link ToolDefinition}. * {@link ToolDefinition}.
* *
* Framework tools have the internal `(input) => output` signature. * Framework tools have the internal `(input, options) => output` signature.
* The public {@link ToolDefinition.execute} expects `(input, ctx)`. * The public {@link ToolDefinition.execute} expects `(input, ctx)`.
* This wrapper bridges the gap `ctx` is trailing and omitted. * This wrapper bridges the gap the public `ctx` is mapped back onto the
* internal execute options.
*/ */
export function toPublicToolDefinition(definition: ResolvedToolDefinition): ToolDefinition { export function toPublicToolDefinition(definition: ResolvedToolDefinition): ToolDefinition {
if (!definition.execute) { if (!definition.execute) {
@@ -21,7 +22,14 @@ export function toPublicToolDefinition(definition: ResolvedToolDefinition): Tool
const inputSchema = definition.inputSchema; const inputSchema = definition.inputSchema;
const publicDefinition: ToolDefinition = { const publicDefinition: ToolDefinition = {
description: definition.description, description: definition.description,
execute: (input) => internalExecute(input), execute: (input, ctx) =>
internalExecute(input, {
abortSignal: ctx.abortSignal,
// The public context carries no model history, so the bridged
// options cannot reproduce the AI SDK's `messages`.
messages: [],
toolCallId: ctx.callId,
}),
inputSchema: (inputSchema ?? {}) as unknown as StandardJSONSchemaV1<unknown>, inputSchema: (inputSchema ?? {}) as unknown as StandardJSONSchemaV1<unknown>,
outputSchema: definition.outputSchema, outputSchema: definition.outputSchema,
}; };
@@ -36,7 +36,9 @@ describe("load_skill executor", () => {
if (execute === undefined) throw new Error("load_skill tool is missing an execute function"); if (execute === undefined) throw new Error("load_skill tool is missing an execute function");
await expect( await expect(
contextStorage.run(ctx, () => execute({ skill: "talk-like-a-dog" })), contextStorage.run(ctx, () =>
execute({ skill: "talk-like-a-dog" }, { messages: [], toolCallId: "call_1" }),
),
).rejects.toThrow("Available skills: custom__bark, custom__talk-like-a-dog."); ).rejects.toThrow("Available skills: custom__bark, custom__talk-like-a-dog.");
}); });
@@ -57,7 +59,11 @@ describe("load_skill executor", () => {
const execute = SKILL_TOOL_DEFINITION.execute; const execute = SKILL_TOOL_DEFINITION.execute;
if (execute === undefined) throw new Error("load_skill tool is missing an execute function"); if (execute === undefined) throw new Error("load_skill tool is missing an execute function");
await expect(contextStorage.run(ctx, () => execute({ skill: "linear" }))).rejects.toThrow( await expect(
contextStorage.run(ctx, () =>
execute({ skill: "linear" }, { messages: [], toolCallId: "call_1" }),
),
).rejects.toThrow(
'"linear" is an installed connection, not a skill. Use connection_search with connection "linear" to find its tools.', '"linear" is an installed connection, not a skill. Use connection_search with connection "linear" to find its tools.',
); );
}); });
+1 -1
View File
@@ -10,7 +10,7 @@ export type ToolExecuteOptions = Omit<ToolExecutionOptions<unknown>, "context">;
export type ToolExecuteFn<TInput = unknown, TOutput = unknown> = ( export type ToolExecuteFn<TInput = unknown, TOutput = unknown> = (
input: TInput, input: TInput,
options?: ToolExecuteOptions, options: ToolExecuteOptions,
) => Promise<TOutput> | TOutput; ) => Promise<TOutput> | TOutput;
interface ToolDefinitionBase { interface ToolDefinitionBase {
+3 -1
View File
@@ -269,7 +269,9 @@ describe("resolveAgent", () => {
sourceId: "tools/get-weather.mjs", sourceId: "tools/get-weather.mjs",
sourceKind: "module", sourceKind: "module",
}); });
expect(resolved.tools[0]?.execute?.({ city: "Brooklyn" })).toEqual({ expect(
resolved.tools[0]?.execute?.({ city: "Brooklyn" }, { messages: [], toolCallId: "call_1" }),
).toEqual({
city: "Brooklyn", city: "Brooklyn",
}); });
}); });
@@ -423,7 +423,12 @@ describe("runtime compiled artifact loaders", () => {
} }
).default.description, ).default.description,
).toBe("Get the weather."); ).toBe("Get the weather.");
await expect(resolvedAgent.tools[0]?.execute?.({ city: "Brooklyn" })).resolves.toEqual({ await expect(
resolvedAgent.tools[0]?.execute?.(
{ city: "Brooklyn" },
{ messages: [], toolCallId: "call_1" },
),
).resolves.toEqual({
city: "Brooklyn", city: "Brooklyn",
source: "lib", source: "lib",
}); });
@@ -547,7 +552,12 @@ describe("runtime compiled artifact loaders", () => {
(tool as { name?: string }).name?.endsWith("_sandbox"), (tool as { name?: string }).name?.endsWith("_sandbox"),
), ),
).toBe(false); ).toBe(false);
await expect(researcherNode?.agent.tools[0]?.execute?.({ query: "climate" })).resolves.toEqual({ await expect(
researcherNode?.agent.tools[0]?.execute?.(
{ query: "climate" },
{ messages: [], toolCallId: "call_1" },
),
).resolves.toEqual({
query: "climate", query: "climate",
source: "subagent-lib", source: "subagent-lib",
}); });
@@ -576,7 +586,9 @@ describe("runtime compiled artifact loaders", () => {
throw new Error("Expected the get_weather tool to be available."); throw new Error("Expected the get_weather tool to be available.");
} }
await expect(getWeatherTool.execute?.({ city: "Brooklyn" })).resolves.toEqual({ await expect(
getWeatherTool.execute?.({ city: "Brooklyn" }, { messages: [], toolCallId: "call_1" }),
).resolves.toEqual({
city: "Brooklyn", city: "Brooklyn",
route: "@/ path alias", route: "@/ path alias",
source: "alias-lib", source: "alias-lib",
@@ -605,7 +617,9 @@ describe("runtime compiled artifact loaders", () => {
throw new Error("Expected one compiled tool before the source update."); throw new Error("Expected one compiled tool before the source update.");
} }
await expect(firstTool.execute?.({ city: "Brooklyn" })).resolves.toEqual({ await expect(
firstTool.execute?.({ city: "Brooklyn" }, { messages: [], toolCallId: "call_1" }),
).resolves.toEqual({
city: "Brooklyn", city: "Brooklyn",
source: "lib", source: "lib",
}); });
@@ -631,7 +645,9 @@ describe("runtime compiled artifact loaders", () => {
throw new Error("Expected one compiled tool after the source update."); throw new Error("Expected one compiled tool after the source update.");
} }
await expect(secondTool.execute?.({ city: "Brooklyn" })).resolves.toEqual({ await expect(
secondTool.execute?.({ city: "Brooklyn" }, { messages: [], toolCallId: "call_1" }),
).resolves.toEqual({
city: "Brooklyn", city: "Brooklyn",
source: "updated-lib", source: "updated-lib",
}); });