mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
feat(eve): expose the tool call id on ToolContext and ApprovalContext (#545)
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
approval: ({ session, toolInput }) => {
|
||||
|
||||
@@ -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:
|
||||
|
||||
- `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.getSandbox()`: the live [sandbox](/docs/sandbox) handle.
|
||||
- `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();
|
||||
|
||||
await runtime.runAsSession({ sandbox }, async () => {
|
||||
const ctx = buildBaseToolContext(controller.signal);
|
||||
const ctx = buildBaseToolContext({ abortSignal: controller.signal, toolCallId: "call_1" });
|
||||
const live = await ctx.getSandbox();
|
||||
await live.run({ command: "echo ready" });
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe("buildBaseToolContext – getSandbox abort binding", () => {
|
||||
const runtime = createTestRuntime();
|
||||
|
||||
await runtime.runAsSession({ sandbox }, async () => {
|
||||
const ctx = buildBaseToolContext(undefined);
|
||||
const ctx = buildBaseToolContext({ toolCallId: "call_1" });
|
||||
const live = await ctx.getSandbox();
|
||||
await live.run({ command: "echo ready" });
|
||||
});
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { buildCallbackContext } from "#context/build-callback-context.js";
|
||||
import type { SessionContext } from "#public/definitions/callback-context.js";
|
||||
import { bindSandboxAbortSignal } from "#execution/sandbox/abort-bound-session.js";
|
||||
import type { ToolExecuteOptions } from "#shared/tool-definition.js";
|
||||
|
||||
/** Base context shared by tool executors. */
|
||||
export type BaseToolContext = SessionContext & {
|
||||
readonly abortSignal: AbortSignal;
|
||||
readonly callId: string;
|
||||
};
|
||||
|
||||
/** 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 signal = abortSignal ?? new AbortController().signal;
|
||||
const signal = options.abortSignal ?? new AbortController().signal;
|
||||
|
||||
return {
|
||||
...callbackContext,
|
||||
abortSignal: signal,
|
||||
callId: options.toolCallId,
|
||||
getSandbox: async () => bindSandboxAbortSignal(await callbackContext.getSandbox(), signal),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ function replayTools(metadata: readonly DurableDynamicToolMetadata[]): HarnessTo
|
||||
tools.push({
|
||||
description: m.description,
|
||||
execute: (input: unknown, options) =>
|
||||
stepFn(m.closureVars, input, buildBaseToolContext(options?.abortSignal)),
|
||||
stepFn(m.closureVars, input, buildBaseToolContext(options)),
|
||||
inputSchema: jsonSchema(m.inputSchema),
|
||||
name: m.name,
|
||||
approval: buildReplayedApproval(m),
|
||||
|
||||
@@ -50,6 +50,8 @@ function qualifyDynamicToolNames(
|
||||
return result;
|
||||
}
|
||||
|
||||
const executeOptions = { messages: [], toolCallId: "call_1" };
|
||||
|
||||
const stubEntry = defineTool({
|
||||
description: "test",
|
||||
inputSchema: { type: "object" },
|
||||
@@ -278,7 +280,7 @@ describe("replayDynamicSessionTools", () => {
|
||||
|
||||
// Execute the replayed tool — mock provides the callback context
|
||||
const tool = tools[0]!;
|
||||
tool.execute!({ query: "test" });
|
||||
tool.execute!({ query: "test" }, executeOptions);
|
||||
expect(stepFn).toHaveBeenCalledWith(
|
||||
{ apiUrl: "https://api.example.com", tenantName: "Acme" },
|
||||
{ query: "test" },
|
||||
@@ -319,11 +321,11 @@ describe("replayDynamicSessionTools", () => {
|
||||
const tools = replayDynamicSessionTools(metadata, []);
|
||||
|
||||
const tool = tools[0]!;
|
||||
tool.execute!({});
|
||||
tool.execute!({}, executeOptions);
|
||||
|
||||
// Mutating the metadata object after replay should NOT affect calls
|
||||
closureVars.counter = 999;
|
||||
tool.execute!({});
|
||||
tool.execute!({}, executeOptions);
|
||||
|
||||
// Both calls get the same closure vars reference from metadata.
|
||||
// This documents current behavior: replay passes by reference.
|
||||
@@ -451,6 +453,7 @@ function createApprovalContext(input: {
|
||||
}): ApprovalContext {
|
||||
return {
|
||||
approvedTools: new Set(),
|
||||
callId: "call_1",
|
||||
getSandbox: vi.fn(),
|
||||
getSkill: vi.fn(),
|
||||
session: {
|
||||
@@ -794,7 +797,7 @@ describe("framework dynamic tools (no bundler transform)", () => {
|
||||
expect(replayedTools[0]!.name).toBe("search");
|
||||
|
||||
// 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" });
|
||||
});
|
||||
|
||||
@@ -822,7 +825,7 @@ describe("framework dynamic tools (no bundler transform)", () => {
|
||||
expect(tools).toHaveLength(1);
|
||||
expect(tools[0]!.name).toBe("assist");
|
||||
|
||||
await tools[0]!.execute!({ action: "help" });
|
||||
await tools[0]!.execute!({ action: "help" }, executeOptions);
|
||||
expect(executeFn).toHaveBeenCalledWith({ action: "help" });
|
||||
});
|
||||
|
||||
@@ -1006,7 +1009,7 @@ describe("framework dynamic tools (no bundler transform)", () => {
|
||||
|
||||
ctx.clearVirtualContext();
|
||||
let tools = buildDynamicTools(ctx);
|
||||
const result1 = await tools[0]!.execute!({});
|
||||
const result1 = await tools[0]!.execute!({}, executeOptions);
|
||||
expect(result1).toEqual({ version: 1 });
|
||||
|
||||
// Re-dispatch overwrites the resolver's slot
|
||||
@@ -1020,7 +1023,7 @@ describe("framework dynamic tools (no bundler transform)", () => {
|
||||
ctx.clearVirtualContext();
|
||||
tools = buildDynamicTools(ctx);
|
||||
expect(tools[0]!.description).toBe("v2");
|
||||
const result2 = await tools[0]!.execute!({});
|
||||
const result2 = await tools[0]!.execute!({}, executeOptions);
|
||||
expect(result2).toEqual({ version: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ function toHarnessToolDefinition(name: string, entry: DynamicToolEntry): Harness
|
||||
return {
|
||||
description: entry.description,
|
||||
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),
|
||||
name,
|
||||
approval: entry.approval,
|
||||
@@ -119,7 +119,7 @@ export function replayDynamicSessionTools(
|
||||
tools.push({
|
||||
description: m.description,
|
||||
execute: (input: unknown, options) =>
|
||||
stepFn(m.closureVars, input, buildBaseToolContext(options?.abortSignal)),
|
||||
stepFn(m.closureVars, input, buildBaseToolContext(options)),
|
||||
inputSchema: jsonSchema(m.inputSchema),
|
||||
name: m.name,
|
||||
outputSchema: m.outputSchema === undefined ? undefined : jsonSchema(m.outputSchema),
|
||||
|
||||
@@ -98,6 +98,21 @@ describe("tool-hosted authorization", () => {
|
||||
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 () => {
|
||||
let calls = 0;
|
||||
const inlineAuth: AuthorizationDefinition = {
|
||||
|
||||
@@ -54,19 +54,19 @@ import type { ToolExecuteOptions } from "#shared/tool-definition.js";
|
||||
export function createToolExecuteWithAuth(input: {
|
||||
readonly scope: string;
|
||||
readonly execute: (toolInput: unknown, ctx: unknown) => unknown;
|
||||
}): (toolInput: unknown, options?: ToolExecuteOptions) => Promise<unknown> {
|
||||
}): (toolInput: unknown, options: ToolExecuteOptions) => Promise<unknown> {
|
||||
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>();
|
||||
|
||||
try {
|
||||
return await execute(
|
||||
toolInput,
|
||||
buildToolContext({
|
||||
abortSignal: options?.abortSignal,
|
||||
inlineAuthState: {},
|
||||
justAuthorizedScopes,
|
||||
options,
|
||||
scope,
|
||||
}),
|
||||
);
|
||||
@@ -81,13 +81,13 @@ export function createToolExecuteWithAuth(input: {
|
||||
}
|
||||
|
||||
function buildToolContext(input: {
|
||||
readonly abortSignal: AbortSignal | undefined;
|
||||
readonly options: ToolExecuteOptions;
|
||||
readonly scope: string;
|
||||
readonly justAuthorizedScopes: Set<string>;
|
||||
readonly inlineAuthState: InlineAuthState;
|
||||
}): ToolContext {
|
||||
const { scope, justAuthorizedScopes, inlineAuthState } = input;
|
||||
const base = buildBaseToolContext(input.abortSignal);
|
||||
const base = buildBaseToolContext(input.options);
|
||||
return {
|
||||
...base,
|
||||
async getToken(provider?: ToolAuthProvider, options?: ToolAuthOptions): Promise<TokenResult> {
|
||||
|
||||
@@ -22,7 +22,7 @@ export type HarnessRuntimeActionDefinition = {
|
||||
export interface HarnessToolDefinition {
|
||||
readonly approvalKey?: (toolInput: Readonly<Record<string, unknown>>) => string;
|
||||
readonly description: string;
|
||||
readonly execute?: (input: any, options?: ToolExecuteOptions) => any;
|
||||
readonly execute?: (input: any, options: ToolExecuteOptions) => any;
|
||||
readonly inputSchema: FlexibleSchema;
|
||||
readonly name: string;
|
||||
readonly approval?: Approval;
|
||||
|
||||
@@ -198,6 +198,40 @@ describe("buildToolSet", () => {
|
||||
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", () => {
|
||||
const schema = {
|
||||
properties: { city: { type: "string" } },
|
||||
@@ -815,6 +849,30 @@ describe("buildToolSet", () => {
|
||||
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 () => {
|
||||
let capturedCtx: Parameters<NonNullable<HarnessToolDefinition["approval"]>>[0] | undefined;
|
||||
const tools: HarnessToolMap = new Map<string, HarnessToolDefinition>([
|
||||
|
||||
@@ -34,7 +34,10 @@ type ToolModelOutputValue =
|
||||
|
||||
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.
|
||||
@@ -302,8 +305,8 @@ export async function buildToolSetWithProviderTools(input: {
|
||||
function buildApprovalFn(
|
||||
definition: HarnessToolDefinition,
|
||||
input: { readonly approvedTools?: ReadonlySet<string> },
|
||||
): (toolInput: unknown) => Promise<NativeApprovalStatus> {
|
||||
return async (toolInput: unknown) => {
|
||||
): (toolInput: unknown, callId: string) => Promise<NativeApprovalStatus> {
|
||||
return async (toolInput: unknown, callId: string) => {
|
||||
if (definition.approval === undefined) return undefined;
|
||||
|
||||
const toolInputRecord = isObject(toolInput) ? toolInput : undefined;
|
||||
@@ -311,6 +314,7 @@ function buildApprovalFn(
|
||||
const status = await definition.approval({
|
||||
...buildCallbackContext(),
|
||||
approvedTools: input.approvedTools ?? new Set(),
|
||||
callId,
|
||||
toolInput: toolInputRecord,
|
||||
toolName: definition.name,
|
||||
});
|
||||
@@ -327,6 +331,6 @@ export function buildToolApproval(
|
||||
if (toolDefinition === undefined) return undefined;
|
||||
|
||||
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.`);
|
||||
}
|
||||
|
||||
return await execute(input);
|
||||
return await execute(input, { messages: [], toolCallId: "call_test" });
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -31,8 +31,8 @@ export default defineTool({
|
||||
...bash,
|
||||
description: "Run a vetted shell command in the project sandbox.",
|
||||
approval: always(),
|
||||
async execute(input) {
|
||||
return bash.execute(input);
|
||||
async execute(input, ctx) {
|
||||
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)
|
||||
* 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
|
||||
* 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 {
|
||||
readonly approvedTools: ReadonlySet<string>;
|
||||
readonly callId: string;
|
||||
readonly toolInput?: ApprovalToolInput<TInput>;
|
||||
readonly toolName: string;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,11 @@ export interface ToolAuthOptions {
|
||||
export type ToolContext = SessionContext & {
|
||||
/** Aborts when the active turn is cancelled. */
|
||||
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
|
||||
* auth shapes as a connection's `auth` field, including `connect("...")`
|
||||
|
||||
@@ -8,9 +8,10 @@ import type { ToolDefinition } from "#public/definitions/tool.js";
|
||||
* {@link ResolvedToolDefinition} so it can be re-exported as a public
|
||||
* {@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)`.
|
||||
* 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 {
|
||||
if (!definition.execute) {
|
||||
@@ -21,7 +22,14 @@ export function toPublicToolDefinition(definition: ResolvedToolDefinition): Tool
|
||||
const inputSchema = definition.inputSchema;
|
||||
const publicDefinition: ToolDefinition = {
|
||||
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>,
|
||||
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");
|
||||
|
||||
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.");
|
||||
});
|
||||
|
||||
@@ -57,7 +59,11 @@ describe("load_skill executor", () => {
|
||||
const execute = SKILL_TOOL_DEFINITION.execute;
|
||||
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.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ export type ToolExecuteOptions = Omit<ToolExecutionOptions<unknown>, "context">;
|
||||
|
||||
export type ToolExecuteFn<TInput = unknown, TOutput = unknown> = (
|
||||
input: TInput,
|
||||
options?: ToolExecuteOptions,
|
||||
options: ToolExecuteOptions,
|
||||
) => Promise<TOutput> | TOutput;
|
||||
|
||||
interface ToolDefinitionBase {
|
||||
|
||||
@@ -269,7 +269,9 @@ describe("resolveAgent", () => {
|
||||
sourceId: "tools/get-weather.mjs",
|
||||
sourceKind: "module",
|
||||
});
|
||||
expect(resolved.tools[0]?.execute?.({ city: "Brooklyn" })).toEqual({
|
||||
expect(
|
||||
resolved.tools[0]?.execute?.({ city: "Brooklyn" }, { messages: [], toolCallId: "call_1" }),
|
||||
).toEqual({
|
||||
city: "Brooklyn",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -423,7 +423,12 @@ describe("runtime compiled artifact loaders", () => {
|
||||
}
|
||||
).default.description,
|
||||
).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",
|
||||
source: "lib",
|
||||
});
|
||||
@@ -547,7 +552,12 @@ describe("runtime compiled artifact loaders", () => {
|
||||
(tool as { name?: string }).name?.endsWith("_sandbox"),
|
||||
),
|
||||
).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",
|
||||
source: "subagent-lib",
|
||||
});
|
||||
@@ -576,7 +586,9 @@ describe("runtime compiled artifact loaders", () => {
|
||||
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",
|
||||
route: "@/ path alias",
|
||||
source: "alias-lib",
|
||||
@@ -605,7 +617,9 @@ describe("runtime compiled artifact loaders", () => {
|
||||
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",
|
||||
source: "lib",
|
||||
});
|
||||
@@ -631,7 +645,9 @@ describe("runtime compiled artifact loaders", () => {
|
||||
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",
|
||||
source: "updated-lib",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user