Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/precise-tool-executors.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/compatibility/tool/v7.ts
Original file line number Diff line number Diff line change
@@ -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 };
},
});
22 changes: 22 additions & 0 deletions packages/eve/extension-contracts/reports/tool/v8.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
18 changes: 17 additions & 1 deletion packages/eve/src/channel/cross-channel-receive.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 = () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/eve/src/channel/cross-channel-receive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface CrossChannelTargetHandle {
}

/** Selects another authored channel and one of its proactive targets. */
export type CrossChannelToFn = <TChannel extends ChannelReference>(
export type CrossChannelToFn = <TChannel extends ChannelReference<unknown>>(
channel: TChannel,
target: InferReceiveTarget<TChannel>,
) => CrossChannelTargetHandle;
Expand Down
2 changes: 1 addition & 1 deletion packages/eve/src/compiler/extension-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} },
Expand Down
18 changes: 18 additions & 0 deletions packages/eve/src/public/definitions/exact.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -61,6 +62,23 @@ describe("definition helper exact inputs", () => {
expectTypeOf(streamedTool).toMatchTypeOf<
ToolDefinition<Record<string, unknown>, { phase: string }>
>();
expectTypeOf<ReturnType<typeof streamedTool.execute>>().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<ReturnType<typeof ordinaryTool.execute>>().toEqualTypeOf<
Promise<{ ok: boolean }>
>();
});

it("keeps the public hook event map aligned with runtime stream events", () => {
Expand Down
76 changes: 45 additions & 31 deletions packages/eve/src/public/definitions/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ export interface ToolDefinition<TInput = unknown, TOutput = unknown> extends Pub
toModelOutput?: (output: TOutput) => ToolModelOutput | Promise<ToolModelOutput>;
}

type ToolOutputFromExecuteReturn<TReturn> =
TReturn extends Promise<infer TOutput>
? TOutput
: TReturn extends AsyncIterable<infer TOutput>
? TOutput
: TReturn;

type ToolDefinitionWithExecuteReturn<TInput, TOutput, TReturn> = ToolDefinition<TInput, TOutput> & {
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`
Expand All @@ -152,70 +163,73 @@ export interface ToolDefinition<TInput = unknown, TOutput = unknown> extends Pub
export function defineTool<
TInputSchema extends StandardJSONSchemaV1<unknown, unknown>,
TOutputSchema extends StandardJSONSchemaV1<unknown, unknown>,
TReturn extends
| Promise<StandardJSONSchemaV1.InferOutput<TOutputSchema>>
| StandardJSONSchemaV1.InferOutput<TOutputSchema>
| AsyncIterable<StandardJSONSchemaV1.InferOutput<TOutputSchema>>,
>(definition: {
description: ToolDefinition<unknown, unknown>["description"];
inputSchema: TInputSchema;
outputSchema: TOutputSchema;
execute(
input: StandardJSONSchemaV1.InferOutput<TInputSchema>,
ctx: ToolContext,
):
| Promise<StandardJSONSchemaV1.InferOutput<TOutputSchema>>
| StandardJSONSchemaV1.InferOutput<TOutputSchema>
| AsyncIterable<StandardJSONSchemaV1.InferOutput<TOutputSchema>>;
execute(input: StandardJSONSchemaV1.InferOutput<TInputSchema>, ctx: ToolContext): TReturn;
approval?: ToolDefinition<StandardJSONSchemaV1.InferOutput<TInputSchema>, unknown>["approval"];
toModelOutput?: ToolDefinition<
unknown,
StandardJSONSchemaV1.InferOutput<TOutputSchema>
>["toModelOutput"];
}): ToolDefinition<
}): ToolDefinitionWithExecuteReturn<
StandardJSONSchemaV1.InferOutput<TInputSchema>,
StandardJSONSchemaV1.InferOutput<TOutputSchema>
StandardJSONSchemaV1.InferOutput<TOutputSchema>,
TReturn
>;
export function defineTool<
TSchema extends StandardJSONSchemaV1<unknown, unknown>,
TOutput,
TReturn,
>(definition: {
description: ToolDefinition<unknown, unknown>["description"];
inputSchema: TSchema;
outputSchema?: JsonObject;
execute(
input: StandardJSONSchemaV1.InferOutput<TSchema>,
ctx: ToolContext,
): Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
execute(input: StandardJSONSchemaV1.InferOutput<TSchema>, ctx: ToolContext): TReturn;
approval?: ToolDefinition<StandardJSONSchemaV1.InferOutput<TSchema>, unknown>["approval"];
toModelOutput?: ToolDefinition<unknown, TOutput>["toModelOutput"];
}): ToolDefinition<StandardJSONSchemaV1.InferOutput<TSchema>, TOutput>;
toModelOutput?: ToolDefinition<unknown, ToolOutputFromExecuteReturn<TReturn>>["toModelOutput"];
}): ToolDefinitionWithExecuteReturn<
StandardJSONSchemaV1.InferOutput<TSchema>,
ToolOutputFromExecuteReturn<TReturn>,
TReturn
>;
export function defineTool<
TOutputSchema extends StandardJSONSchemaV1<unknown, unknown>,
TReturn extends
| Promise<StandardJSONSchemaV1.InferOutput<TOutputSchema>>
| StandardJSONSchemaV1.InferOutput<TOutputSchema>
| AsyncIterable<StandardJSONSchemaV1.InferOutput<TOutputSchema>>,
>(definition: {
description: ToolDefinition<unknown, unknown>["description"];
inputSchema: JsonObject;
outputSchema: TOutputSchema;
execute(
input: Record<string, unknown>,
ctx: ToolContext,
):
| Promise<StandardJSONSchemaV1.InferOutput<TOutputSchema>>
| StandardJSONSchemaV1.InferOutput<TOutputSchema>
| AsyncIterable<StandardJSONSchemaV1.InferOutput<TOutputSchema>>;
execute(input: Record<string, unknown>, ctx: ToolContext): TReturn;
approval?: ToolDefinition<Record<string, unknown>, unknown>["approval"];
toModelOutput?: ToolDefinition<
unknown,
StandardJSONSchemaV1.InferOutput<TOutputSchema>
>["toModelOutput"];
}): ToolDefinition<Record<string, unknown>, StandardJSONSchemaV1.InferOutput<TOutputSchema>>;
export function defineTool<TOutput>(definition: {
}): ToolDefinitionWithExecuteReturn<
Record<string, unknown>,
StandardJSONSchemaV1.InferOutput<TOutputSchema>,
TReturn
>;
export function defineTool<TReturn>(definition: {
description: ToolDefinition<unknown, unknown>["description"];
inputSchema: JsonObject;
outputSchema?: JsonObject;
execute(
input: Record<string, unknown>,
ctx: ToolContext,
): Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
execute(input: Record<string, unknown>, ctx: ToolContext): TReturn;
approval?: ToolDefinition<Record<string, unknown>, unknown>["approval"];
toModelOutput?: ToolDefinition<unknown, TOutput>["toModelOutput"];
}): ToolDefinition<Record<string, unknown>, TOutput>;
toModelOutput?: ToolDefinition<unknown, ToolOutputFromExecuteReturn<TReturn>>["toModelOutput"];
}): ToolDefinitionWithExecuteReturn<
Record<string, unknown>,
ToolOutputFromExecuteReturn<TReturn>,
TReturn
>;
export function defineTool<TInput = unknown, TOutput = unknown>(
definition: ToolDefinition<TInput, TOutput>,
): ToolDefinition<TInput, TOutput>;
Expand Down