Skip to content
Draft
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/tidy-tools-cross-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Preserve authored tool executor returns and closed cross-channel receive-target types.
26 changes: 26 additions & 0 deletions packages/eve/extension-contracts/compatibility/tool/v7.ts
Original file line number Diff line number Diff line change
@@ -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<NormalizeInput, NormalizeOutput> = 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;
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": "427ca5a2b844c76496ed33ab0cddfbd1b0de1c9da2483ff8c16a7d0df3f85d37",
"exports": [
"defineBashTool",
"defineGlobTool",
"defineGrepTool",
"defineReadFileTool",
"defineTool",
"defineWriteFileTool",
"disableTool",
"experimental_workflow",
"isDisabledToolSentinel",
"isExperimentalWorkflowToolDefinition",
"toolOutput",
"toolOutputPart",
"toolResultFrom",
"webSearch"
]
}
13 changes: 12 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 } from "#public/channels/slack/slackChannel.js";

function makeRuntime(): Runtime {
return {
Expand Down Expand Up @@ -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;
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
88 changes: 88 additions & 0 deletions packages/eve/src/public/definitions/exact.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -61,6 +62,93 @@ describe("definition helper exact inputs", () => {
expectTypeOf(streamedTool).toMatchTypeOf<
ToolDefinition<Record<string, unknown>, { 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<Promise<{ report: string }>>();

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<Record<string, unknown>, { 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<Record<string, unknown>, { report: string }>
>();

const broadlyTypedTool = defineTool<string, string>({
description: "Normalize text.",
inputSchema: { type: "string" },
async execute(input) {
return input.toUpperCase();
},
});

expectTypeOf(broadlyTypedTool.execute("hello", null as never)).toEqualTypeOf<
Promise<string> | string | AsyncIterable<string>
>();
});

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

type ToolExecutionResult<TOutput> = Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
type ToolExecutor<TInput, TOutput> = (
input: TInput,
ctx: ToolContext,
) => ToolExecutionResult<TOutput>;
type InferToolExecutionResult<TResult> =
TResult extends AsyncIterable<infer TOutput> ? TOutput : Awaited<TResult>;
type InferToolExecutionOutput<TExecute extends (...args: never[]) => unknown> =
InferToolExecutionResult<ReturnType<TExecute>>;
type ResolveToolExecutionOutput<
TOutput,
TExecute extends (...args: never[]) => unknown,
> = unknown extends TOutput ? InferToolExecutionOutput<TExecute> : TOutput;
type AuthoredToolDefinition<TInput, TOutput, TExecute extends (...args: never[]) => unknown> = Omit<
ToolDefinition<TInput, TOutput>,
"execute"
> & {
execute(input: TInput, ctx: ToolContext): ReturnType<TExecute>;
};

/**
* 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 +172,92 @@ export interface ToolDefinition<TInput = unknown, TOutput = unknown> extends Pub
export function defineTool<
TInputSchema extends StandardJSONSchemaV1<unknown, unknown>,
TOutputSchema extends StandardJSONSchemaV1<unknown, unknown>,
TExecute extends ToolExecutor<
StandardJSONSchemaV1.InferOutput<TInputSchema>,
StandardJSONSchemaV1.InferOutput<TOutputSchema>
> = ToolExecutor<
StandardJSONSchemaV1.InferOutput<TInputSchema>,
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: TExecute;
approval?: ToolDefinition<StandardJSONSchemaV1.InferOutput<TInputSchema>, unknown>["approval"];
toModelOutput?: ToolDefinition<
unknown,
StandardJSONSchemaV1.InferOutput<TOutputSchema>
>["toModelOutput"];
}): ToolDefinition<
}): AuthoredToolDefinition<
StandardJSONSchemaV1.InferOutput<TInputSchema>,
StandardJSONSchemaV1.InferOutput<TOutputSchema>
StandardJSONSchemaV1.InferOutput<TOutputSchema>,
TExecute
>;
export function defineTool<
TSchema extends StandardJSONSchemaV1<unknown, unknown>,
TOutput,
TExecute extends ToolExecutor<StandardJSONSchemaV1.InferOutput<TSchema>, TOutput> = ToolExecutor<
StandardJSONSchemaV1.InferOutput<TSchema>,
TOutput
>,
>(definition: {
description: ToolDefinition<unknown, unknown>["description"];
inputSchema: TSchema;
outputSchema?: JsonObject;
execute(
input: StandardJSONSchemaV1.InferOutput<TSchema>,
ctx: ToolContext,
): Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
execute: TExecute;
approval?: ToolDefinition<StandardJSONSchemaV1.InferOutput<TSchema>, unknown>["approval"];
toModelOutput?: ToolDefinition<unknown, TOutput>["toModelOutput"];
}): ToolDefinition<StandardJSONSchemaV1.InferOutput<TSchema>, TOutput>;
toModelOutput?: ToolDefinition<
unknown,
NoInfer<ResolveToolExecutionOutput<TOutput, TExecute>>
>["toModelOutput"];
}): AuthoredToolDefinition<
StandardJSONSchemaV1.InferOutput<TSchema>,
ResolveToolExecutionOutput<TOutput, TExecute>,
TExecute
>;
export function defineTool<
TOutputSchema extends StandardJSONSchemaV1<unknown, unknown>,
TExecute extends ToolExecutor<
Record<string, unknown>,
StandardJSONSchemaV1.InferOutput<TOutputSchema>
> = ToolExecutor<Record<string, unknown>, 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: TExecute;
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: {
}): AuthoredToolDefinition<
Record<string, unknown>,
StandardJSONSchemaV1.InferOutput<TOutputSchema>,
TExecute
>;
export function defineTool<
TOutput,
TExecute extends ToolExecutor<Record<string, unknown>, TOutput> = ToolExecutor<
Record<string, unknown>,
TOutput
>,
>(definition: {
description: ToolDefinition<unknown, unknown>["description"];
inputSchema: JsonObject;
outputSchema?: JsonObject;
execute(
input: Record<string, unknown>,
ctx: ToolContext,
): Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
execute: TExecute;
approval?: ToolDefinition<Record<string, unknown>, unknown>["approval"];
toModelOutput?: ToolDefinition<unknown, TOutput>["toModelOutput"];
}): ToolDefinition<Record<string, unknown>, TOutput>;
toModelOutput?: ToolDefinition<
unknown,
NoInfer<ResolveToolExecutionOutput<TOutput, TExecute>>
>["toModelOutput"];
}): AuthoredToolDefinition<
Record<string, unknown>,
ResolveToolExecutionOutput<TOutput, TExecute>,
TExecute
>;
export function defineTool<TInput = unknown, TOutput = unknown>(
definition: ToolDefinition<TInput, TOutput>,
): ToolDefinition<TInput, TOutput>;
Expand Down
Loading