From bc16fc98c0c82000724e41905cbefae046902a24 Mon Sep 17 00:00:00 2001 From: "vercel-gh-bot-4[bot]" <312518292+vercel-gh-bot-4[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:16:40 +0000 Subject: [PATCH] fix(eve): preserve authored tool and channel type precision --- .changeset/tidy-tools-cross-channels.md | 5 + .../compatibility/tool/v7.ts | 26 +++++ .../extension-contracts/reports/tool/v8.json | 22 ++++ .../src/channel/cross-channel-receive.test.ts | 13 ++- .../eve/src/channel/cross-channel-receive.ts | 2 +- .../src/compiler/extension-compatibility.ts | 2 +- .../eve/src/public/definitions/exact.test.ts | 88 +++++++++++++++ packages/eve/src/public/definitions/tool.ts | 102 ++++++++++++------ 8 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 .changeset/tidy-tools-cross-channels.md create mode 100644 packages/eve/extension-contracts/compatibility/tool/v7.ts create mode 100644 packages/eve/extension-contracts/reports/tool/v8.json diff --git a/.changeset/tidy-tools-cross-channels.md b/.changeset/tidy-tools-cross-channels.md new file mode 100644 index 000000000..0d5011a61 --- /dev/null +++ b/.changeset/tidy-tools-cross-channels.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Preserve authored tool executor returns and closed cross-channel receive-target types. diff --git a/packages/eve/extension-contracts/compatibility/tool/v7.ts b/packages/eve/extension-contracts/compatibility/tool/v7.ts new file mode 100644 index 000000000..c7ce99f07 --- /dev/null +++ b/packages/eve/extension-contracts/compatibility/tool/v7.ts @@ -0,0 +1,26 @@ +import { defineTool, type ToolDefinition } from "#public/tools/index.js"; + +interface NormalizeInput { + readonly value: string; +} + +interface NormalizeOutput { + readonly normalized: string; +} + +const normalize: ToolDefinition = defineTool< + NormalizeInput, + NormalizeOutput +>({ + description: "Normalize authored text.", + inputSchema: { + type: "object", + properties: { value: { type: "string" } }, + required: ["value"], + }, + async execute(input) { + return { normalized: input.value.trim() }; + }, +}); + +export default normalize; diff --git a/packages/eve/extension-contracts/reports/tool/v8.json b/packages/eve/extension-contracts/reports/tool/v8.json new file mode 100644 index 000000000..f04f2bb21 --- /dev/null +++ b/packages/eve/extension-contracts/reports/tool/v8.json @@ -0,0 +1,22 @@ +{ + "kind": "eve-extension-capability-contract", + "capability": "tool", + "epoch": 8, + "sha256": "427ca5a2b844c76496ed33ab0cddfbd1b0de1c9da2483ff8c16a7d0df3f85d37", + "exports": [ + "defineBashTool", + "defineGlobTool", + "defineGrepTool", + "defineReadFileTool", + "defineTool", + "defineWriteFileTool", + "disableTool", + "experimental_workflow", + "isDisabledToolSentinel", + "isExperimentalWorkflowToolDefinition", + "toolOutput", + "toolOutputPart", + "toolResultFrom", + "webSearch" + ] +} diff --git a/packages/eve/src/channel/cross-channel-receive.test.ts b/packages/eve/src/channel/cross-channel-receive.test.ts index 4a4f29838..4fff9548b 100644 --- a/packages/eve/src/channel/cross-channel-receive.test.ts +++ b/packages/eve/src/channel/cross-channel-receive.test.ts @@ -1,9 +1,14 @@ import { describe, expect, it, vi } from "vitest"; import { CHANNEL_SENTINEL, type CompiledChannel } from "#channel/compiled-channel.js"; -import { createCrossChannelToFn, type CrossChannelTarget } from "#channel/cross-channel-receive.js"; +import { + createCrossChannelToFn, + type CrossChannelTarget, + type CrossChannelToFn, +} from "#channel/cross-channel-receive.js"; import type { Session } from "#channel/session.js"; import type { Runtime } from "#channel/types.js"; +import type { SlackChannel } from "#public/channels/slack/slackChannel.js"; function makeRuntime(): Runtime { return { @@ -186,3 +191,9 @@ describe("createCrossChannelToFn", () => { expect(slack.receive.mock.calls[0]![0]).toEqual(expect.objectContaining({ auth })); }); }); + +function typeOnlyFixtures(to: CrossChannelToFn, slack: SlackChannel): void { + to(slack, { channelId: "C123" }); +} + +void typeOnlyFixtures; diff --git a/packages/eve/src/channel/cross-channel-receive.ts b/packages/eve/src/channel/cross-channel-receive.ts index 319fa1704..99279022f 100644 --- a/packages/eve/src/channel/cross-channel-receive.ts +++ b/packages/eve/src/channel/cross-channel-receive.ts @@ -25,7 +25,7 @@ export interface CrossChannelTargetHandle { } /** Selects another authored channel and one of its proactive targets. */ -export type CrossChannelToFn = ( +export type CrossChannelToFn = >( channel: TChannel, target: InferReceiveTarget, ) => CrossChannelTargetHandle; diff --git a/packages/eve/src/compiler/extension-compatibility.ts b/packages/eve/src/compiler/extension-compatibility.ts index 76ab3b337..d7061d3a3 100644 --- a/packages/eve/src/compiler/extension-compatibility.ts +++ b/packages/eve/src/compiler/extension-compatibility.ts @@ -21,7 +21,7 @@ interface ExtensionCapabilityContract { const EXTENSION_CAPABILITY_CONTRACTS = { extension: { current: 1, supported: [1], dropped: {} }, - tool: { current: 7, supported: [1, 2, 3, 4, 5, 6, 7], dropped: {} }, + tool: { current: 8, supported: [1, 2, 3, 4, 5, 6, 7, 8], dropped: {} }, dynamicTool: { current: 10, supported: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dropped: {} }, connection: { current: 2, supported: [1, 2], dropped: {} }, hook: { current: 8, supported: [1, 2, 3, 4, 5, 6, 7, 8], dropped: {} }, diff --git a/packages/eve/src/public/definitions/exact.test.ts b/packages/eve/src/public/definitions/exact.test.ts index 2cff701fa..dab0c67a0 100644 --- a/packages/eve/src/public/definitions/exact.test.ts +++ b/packages/eve/src/public/definitions/exact.test.ts @@ -1,4 +1,5 @@ import { describe, expect, expectTypeOf, it } from "vitest"; +import { z } from "zod"; import type { UnstampedMessageStreamEvent } from "#protocol/message.js"; import { defineAgent, defineDynamic } from "#public/definitions/agent.js"; @@ -61,6 +62,93 @@ describe("definition helper exact inputs", () => { expectTypeOf(streamedTool).toMatchTypeOf< ToolDefinition, { phase: string }> >(); + expectTypeOf(streamedTool.execute({}, null as never)).toEqualTypeOf< + AsyncGenerator<{ phase: string }, void, unknown> + >(); + }); + + it("preserves ordinary async tool executor return types", () => { + const asyncTool = defineTool({ + description: "Return a report.", + inputSchema: { type: "object" }, + async execute() { + return { report: "complete" }; + }, + }); + + expectTypeOf(asyncTool.execute({}, null as never)).toEqualTypeOf>(); + + const projectedTool = defineTool({ + description: "Project a report.", + inputSchema: { type: "object" }, + toModelOutput(output) { + return { type: "text", value: output.report }; + }, + async execute() { + return { report: "complete" }; + }, + }); + + expectTypeOf(projectedTool.execute({}, null as never)).toEqualTypeOf< + Promise<{ report: string }> + >(); + + const schemaProjectedTool = defineTool({ + description: "Project a schema-backed report.", + inputSchema: z.object({}), + toModelOutput(output) { + return { type: "text", value: output.report }; + }, + async execute() { + return { report: "complete" }; + }, + }); + + expectTypeOf(schemaProjectedTool.execute({}, null as never)).toEqualTypeOf< + Promise<{ report: string }> + >(); + + const mixedTool = defineTool({ + description: "Return or stream a report.", + inputSchema: { type: "object" }, + toModelOutput(output) { + return { type: "text", value: output.report }; + }, + execute(): Promise<{ report: string }> | AsyncIterable<{ report: string }> { + return Promise.resolve({ report: "complete" }); + }, + }); + + expectTypeOf(mixedTool).toMatchTypeOf< + ToolDefinition, { report: string }> + >(); + expectTypeOf(mixedTool.execute({}, null as never)).toEqualTypeOf< + Promise<{ report: string }> | AsyncIterable<{ report: string }> + >(); + + const explicitlyTypedTool = defineTool<{ report: string }>({ + description: "Return an explicitly typed report.", + inputSchema: { type: "object" }, + async execute() { + return { report: "complete" }; + }, + }); + + expectTypeOf(explicitlyTypedTool).toMatchTypeOf< + ToolDefinition, { report: string }> + >(); + + const broadlyTypedTool = defineTool({ + description: "Normalize text.", + inputSchema: { type: "string" }, + async execute(input) { + return input.toUpperCase(); + }, + }); + + expectTypeOf(broadlyTypedTool.execute("hello", null as never)).toEqualTypeOf< + Promise | string | AsyncIterable + >(); }); it("keeps the public hook event map aligned with runtime stream events", () => { diff --git a/packages/eve/src/public/definitions/tool.ts b/packages/eve/src/public/definitions/tool.ts index 16c08e78b..bb1b9e843 100644 --- a/packages/eve/src/public/definitions/tool.ts +++ b/packages/eve/src/public/definitions/tool.ts @@ -141,6 +141,26 @@ export interface ToolDefinition extends Pub toModelOutput?: (output: TOutput) => ToolModelOutput | Promise; } +type ToolExecutionResult = Promise | TOutput | AsyncIterable; +type ToolExecutor = ( + input: TInput, + ctx: ToolContext, +) => ToolExecutionResult; +type InferToolExecutionResult = + TResult extends AsyncIterable ? TOutput : Awaited; +type InferToolExecutionOutput unknown> = + InferToolExecutionResult>; +type ResolveToolExecutionOutput< + TOutput, + TExecute extends (...args: never[]) => unknown, +> = unknown extends TOutput ? InferToolExecutionOutput : TOutput; +type AuthoredToolDefinition unknown> = Omit< + ToolDefinition, + "execute" +> & { + execute(input: TInput, ctx: ToolContext): ReturnType; +}; + /** * Defines a tool configuration, used both for static tools (default export * from `agent/tools/*.ts`) and as the entry wrapper inside `defineDynamic` @@ -152,70 +172,92 @@ export interface ToolDefinition extends Pub export function defineTool< TInputSchema extends StandardJSONSchemaV1, TOutputSchema extends StandardJSONSchemaV1, + TExecute extends ToolExecutor< + StandardJSONSchemaV1.InferOutput, + StandardJSONSchemaV1.InferOutput + > = ToolExecutor< + StandardJSONSchemaV1.InferOutput, + StandardJSONSchemaV1.InferOutput + >, >(definition: { description: ToolDefinition["description"]; inputSchema: TInputSchema; outputSchema: TOutputSchema; - execute( - input: StandardJSONSchemaV1.InferOutput, - ctx: ToolContext, - ): - | Promise> - | StandardJSONSchemaV1.InferOutput - | AsyncIterable>; + execute: TExecute; approval?: ToolDefinition, unknown>["approval"]; toModelOutput?: ToolDefinition< unknown, StandardJSONSchemaV1.InferOutput >["toModelOutput"]; -}): ToolDefinition< +}): AuthoredToolDefinition< StandardJSONSchemaV1.InferOutput, - StandardJSONSchemaV1.InferOutput + StandardJSONSchemaV1.InferOutput, + TExecute >; export function defineTool< TSchema extends StandardJSONSchemaV1, TOutput, + TExecute extends ToolExecutor, TOutput> = ToolExecutor< + StandardJSONSchemaV1.InferOutput, + TOutput + >, >(definition: { description: ToolDefinition["description"]; inputSchema: TSchema; outputSchema?: JsonObject; - execute( - input: StandardJSONSchemaV1.InferOutput, - ctx: ToolContext, - ): Promise | TOutput | AsyncIterable; + execute: TExecute; approval?: ToolDefinition, unknown>["approval"]; - toModelOutput?: ToolDefinition["toModelOutput"]; -}): ToolDefinition, TOutput>; + toModelOutput?: ToolDefinition< + unknown, + NoInfer> + >["toModelOutput"]; +}): AuthoredToolDefinition< + StandardJSONSchemaV1.InferOutput, + ResolveToolExecutionOutput, + TExecute +>; export function defineTool< TOutputSchema extends StandardJSONSchemaV1, + TExecute extends ToolExecutor< + Record, + StandardJSONSchemaV1.InferOutput + > = ToolExecutor, StandardJSONSchemaV1.InferOutput>, >(definition: { description: ToolDefinition["description"]; inputSchema: JsonObject; outputSchema: TOutputSchema; - execute( - input: Record, - ctx: ToolContext, - ): - | Promise> - | StandardJSONSchemaV1.InferOutput - | AsyncIterable>; + execute: TExecute; approval?: ToolDefinition, unknown>["approval"]; toModelOutput?: ToolDefinition< unknown, StandardJSONSchemaV1.InferOutput >["toModelOutput"]; -}): ToolDefinition, StandardJSONSchemaV1.InferOutput>; -export function defineTool(definition: { +}): AuthoredToolDefinition< + Record, + StandardJSONSchemaV1.InferOutput, + TExecute +>; +export function defineTool< + TOutput, + TExecute extends ToolExecutor, TOutput> = ToolExecutor< + Record, + TOutput + >, +>(definition: { description: ToolDefinition["description"]; inputSchema: JsonObject; outputSchema?: JsonObject; - execute( - input: Record, - ctx: ToolContext, - ): Promise | TOutput | AsyncIterable; + execute: TExecute; approval?: ToolDefinition, unknown>["approval"]; - toModelOutput?: ToolDefinition["toModelOutput"]; -}): ToolDefinition, TOutput>; + toModelOutput?: ToolDefinition< + unknown, + NoInfer> + >["toModelOutput"]; +}): AuthoredToolDefinition< + Record, + ResolveToolExecutionOutput, + TExecute +>; export function defineTool( definition: ToolDefinition, ): ToolDefinition;