diff --git a/.changeset/precise-tool-executors.md b/.changeset/precise-tool-executors.md new file mode 100644 index 000000000..fb1cd2765 --- /dev/null +++ b/.changeset/precise-tool-executors.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Preserve each tool executor's concrete return type through `defineTool`, so non-streaming tools no longer appear to return an async iterable. Allow `ctx.to()` to infer closed receive-target interfaces such as Slack's without requiring an index signature. 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..847355ad1 --- /dev/null +++ b/packages/eve/extension-contracts/compatibility/tool/v7.ts @@ -0,0 +1,13 @@ +import { defineTool } from "#public/tools/index.js"; + +export default defineTool({ + description: "Stream report progress.", + inputSchema: { type: "object", properties: {} }, + async *execute(_input, ctx) { + yield { callId: ctx.callId, phase: "collecting" }; + yield { callId: ctx.callId, phase: "complete" }; + }, + toModelOutput(output) { + return { type: "text", value: output.phase }; + }, +}); 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..b03620c85 --- /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": "dcafc8090f640b967b808d0e6cbe6a61462edd1147ccec5902b7ebafc9fd67ed", + "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..7bc21fb2c 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, SlackReceiveTarget } from "#public/channels/slack/slackChannel.js"; function makeRuntime(): Runtime { return { @@ -71,6 +76,17 @@ function makeChannel(name: string): { } describe("createCrossChannelToFn", () => { + it("accepts a Slack channel whose receive target is a closed interface", () => { + const typeOnlyCalls = (to: CrossChannelToFn, slack: SlackChannel) => { + const target: SlackReceiveTarget = { channelId: "C1" }; + to(slack, target); + // @ts-expect-error Slack receive targets require a channel id. + to(slack, {}); + }; + + expect(typeOnlyCalls).toBeTypeOf("function"); + }); + it("requires an eve channel reference at compile time", () => { const fn = createCrossChannelToFn(makeRuntime(), []); const invalidCalls = () => { 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..dce266d47 100644 --- a/packages/eve/src/public/definitions/exact.test.ts +++ b/packages/eve/src/public/definitions/exact.test.ts @@ -1,5 +1,6 @@ import { describe, expect, expectTypeOf, it } from "vitest"; +import { z } from "#compiled/zod/index.js"; import type { UnstampedMessageStreamEvent } from "#protocol/message.js"; import { defineAgent, defineDynamic } from "#public/definitions/agent.js"; import { defineRemoteAgent } from "#public/definitions/remote-agent.js"; @@ -61,6 +62,23 @@ describe("definition helper exact inputs", () => { expectTypeOf(streamedTool).toMatchTypeOf< ToolDefinition, { phase: string }> >(); + expectTypeOf>().toEqualTypeOf< + AsyncGenerator<{ phase: string }, void, unknown> + >(); + }); + + it("preserves ordinary async tool executor return types", () => { + const ordinaryTool = defineTool({ + description: "React to a message.", + inputSchema: z.object({ reaction: z.string() }), + async execute(input) { + return { ok: input.reaction.length > 0 }; + }, + }); + + expectTypeOf>().toEqualTypeOf< + Promise<{ ok: boolean }> + >(); }); 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..449b67641 100644 --- a/packages/eve/src/public/definitions/tool.ts +++ b/packages/eve/src/public/definitions/tool.ts @@ -141,6 +141,17 @@ export interface ToolDefinition extends Pub toModelOutput?: (output: TOutput) => ToolModelOutput | Promise; } +type ToolOutputFromExecuteReturn = + TReturn extends Promise + ? TOutput + : TReturn extends AsyncIterable + ? TOutput + : TReturn; + +type ToolDefinitionWithExecuteReturn = ToolDefinition & { + execute(input: TInput, ctx: ToolContext): TReturn; +}; + /** * Defines a tool configuration, used both for static tools (default export * from `agent/tools/*.ts`) and as the entry wrapper inside `defineDynamic` @@ -152,70 +163,73 @@ export interface ToolDefinition extends Pub export function defineTool< TInputSchema extends StandardJSONSchemaV1, TOutputSchema extends StandardJSONSchemaV1, + TReturn extends + | Promise> + | StandardJSONSchemaV1.InferOutput + | AsyncIterable>, >(definition: { description: ToolDefinition["description"]; inputSchema: TInputSchema; outputSchema: TOutputSchema; - execute( - input: StandardJSONSchemaV1.InferOutput, - ctx: ToolContext, - ): - | Promise> - | StandardJSONSchemaV1.InferOutput - | AsyncIterable>; + execute(input: StandardJSONSchemaV1.InferOutput, ctx: ToolContext): TReturn; approval?: ToolDefinition, unknown>["approval"]; toModelOutput?: ToolDefinition< unknown, StandardJSONSchemaV1.InferOutput >["toModelOutput"]; -}): ToolDefinition< +}): ToolDefinitionWithExecuteReturn< StandardJSONSchemaV1.InferOutput, - StandardJSONSchemaV1.InferOutput + StandardJSONSchemaV1.InferOutput, + TReturn >; export function defineTool< TSchema extends StandardJSONSchemaV1, - TOutput, + TReturn, >(definition: { description: ToolDefinition["description"]; inputSchema: TSchema; outputSchema?: JsonObject; - execute( - input: StandardJSONSchemaV1.InferOutput, - ctx: ToolContext, - ): Promise | TOutput | AsyncIterable; + execute(input: StandardJSONSchemaV1.InferOutput, ctx: ToolContext): TReturn; approval?: ToolDefinition, unknown>["approval"]; - toModelOutput?: ToolDefinition["toModelOutput"]; -}): ToolDefinition, TOutput>; + toModelOutput?: ToolDefinition>["toModelOutput"]; +}): ToolDefinitionWithExecuteReturn< + StandardJSONSchemaV1.InferOutput, + ToolOutputFromExecuteReturn, + TReturn +>; export function defineTool< TOutputSchema extends StandardJSONSchemaV1, + TReturn extends + | Promise> + | StandardJSONSchemaV1.InferOutput + | AsyncIterable>, >(definition: { description: ToolDefinition["description"]; inputSchema: JsonObject; outputSchema: TOutputSchema; - execute( - input: Record, - ctx: ToolContext, - ): - | Promise> - | StandardJSONSchemaV1.InferOutput - | AsyncIterable>; + execute(input: Record, ctx: ToolContext): TReturn; approval?: ToolDefinition, unknown>["approval"]; toModelOutput?: ToolDefinition< unknown, StandardJSONSchemaV1.InferOutput >["toModelOutput"]; -}): ToolDefinition, StandardJSONSchemaV1.InferOutput>; -export function defineTool(definition: { +}): ToolDefinitionWithExecuteReturn< + Record, + StandardJSONSchemaV1.InferOutput, + TReturn +>; +export function defineTool(definition: { description: ToolDefinition["description"]; inputSchema: JsonObject; outputSchema?: JsonObject; - execute( - input: Record, - ctx: ToolContext, - ): Promise | TOutput | AsyncIterable; + execute(input: Record, ctx: ToolContext): TReturn; approval?: ToolDefinition, unknown>["approval"]; - toModelOutput?: ToolDefinition["toModelOutput"]; -}): ToolDefinition, TOutput>; + toModelOutput?: ToolDefinition>["toModelOutput"]; +}): ToolDefinitionWithExecuteReturn< + Record, + ToolOutputFromExecuteReturn, + TReturn +>; export function defineTool( definition: ToolDefinition, ): ToolDefinition;