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/owned-decision-contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

`decide`, both `auto()` helpers, and eval judges now use eve-owned decision types from `eve/ai`. Standalone `decide` takes `signal` instead of `abortSignal` and returns only `answers` and `usage`; AI SDK request hooks, internal options, and provider metadata are no longer public contracts.
5 changes: 5 additions & 0 deletions .changeset/owned-platform-contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Slack card, Twilio webhook, and Vercel sandbox settings now use eve-owned contracts. Create Vercel drives through `@vercel/sandbox` and mount by ID; import Next.js config types from `next` instead of the removed `EveNext*` aliases, and use the narrowed, documented workflow helpers.
26 changes: 26 additions & 0 deletions docs/concepts/upgrade-to-v27-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,32 @@ not individual events. The shared options include `follow` and `signal`; only th
`streamReconnectPolicy`. Client handles (`ClientSession`, `ClientAgentSession`, `ClientSessions`)
are type-only exports. Obtain them from `new Client()` and its methods instead of constructing them.

## Decision models

`decide` from `eve/ai` now accepts eve-owned `DecisionOptions` and returns `DecisionResult`:
`answers` and `usage` only. Replace the `abortSignal` request option with `signal`; SDK telemetry,
callbacks, `_internal`, headers, provider options, and response metadata are not public options or
results. Import question, state, answer, and model types from `eve/ai`, not AI SDK's experimental
aliases. Both `auto()` helpers and eval judges use the same `DecisionModel` contract; installed
provider instances still use the SDK's runtime model-version checks and authentication.
Eval diagnostic metadata continues to include provider response details internally.

## Owned platform types

Slack card builders now return eve-owned plain-data types. The upstream-only
`AdapterPostableMessage` and `Attachment` exports are removed; use Slack message options
and `SlackAttachment` instead. Chat SDK adapters still use the app's own `chat` classes
through `eve/channels/chat-sdk`.

`eve/sandbox/vercel` no longer re-exports `Drive`. Apps that create drives import it from
`@vercel/sandbox` and pass `{ drive: drive.driveId, mode: "read-write" }` or
`drive.snapshot()` as a mount. Sandbox creation and firewall settings use eve-owned types.

Next.js apps import `NextConfig` from `next`, not the removed `EveNext*` aliases.
Reusable eve config callbacks can use `WithEveConfigFunction` and `WithEveConfigInput`.
`eve/workflow-modules` now declares only the authored workflow helpers documented by eve,
not the entire vendored Workflow SDK.

## Clients and frontends

- **Frontend commands resolve on acceptance.** `EveAgentStore.send()` and the frontend hooks'
Expand Down
17 changes: 11 additions & 6 deletions docs/guides/decide.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,24 @@ export default defineTool({
},
},
},
abortSignal: ctx.abortSignal,
signal: ctx.abortSignal,
});
return { team: result.answers.team.choice };
},
});
```

The choice above is typed as `"billing" | "support"`. Each question appears under
its authored key in `result.answers`. Results also include token usage, warnings,
provider metadata, and response metadata. To use that choice to delegate while keeping specialist subagents out of the parent model's tools, see [Route to a hidden subagent with Jev](/docs/tools/workflows#route-to-a-hidden-subagent-with-jev).

`decide` accepts AI SDK decision options, including `maxRetries`, `headers`,
and `providerOptions`. Pass an `abortSignal` to cancel the request. Input and
its authored key in `result.answers`. Results also include normalized token usage in `usage`.
These are eve-owned contracts, independent of the AI SDK's experimental result metadata. To use that choice to delegate while keeping specialist subagents out of the parent model's tools, see [Route to a hidden subagent with Jev](/docs/tools/workflows#route-to-a-hidden-subagent-with-jev).

`decide` accepts `model`, `state`, `questions`, `maxRetries`, and `signal`.
Pass `signal: ctx.abortSignal` to cancel the request. Import `DecisionModel`, `DecisionState`,
`DecisionStatePart`, `DecisionInput`, `DecisionQuestion`, `DecisionAnswer`, `DecisionOptions`,
and `DecisionResult` from `eve/ai` to annotate reusable configs and handlers.
Model instances must implement a decision model specification supported by the installed AI SDK;
a matching `provider`, `modelId`, and `specificationVersion` alone is not an implementation.
A newer specification can typecheck but still be rejected by an older SDK at runtime. Input and
answer validation, retries, and provider errors follow AI SDK semantics.

You can also call `decide` outside a tool; it does not require an active eve
Expand Down
7 changes: 4 additions & 3 deletions docs/sandbox/vercel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,12 @@ const sandboxName = sandbox.name;

## Mount a Drive

A [Vercel Sandbox Drive](https://vercel.com/docs/sandbox/concepts/drives) stores files independently of a sandbox. Create or retrieve the Drive before calling `open()`, then pass it in `mounts` under an absolute path:
A [Vercel Sandbox Drive](https://vercel.com/docs/sandbox/concepts/drives) stores files independently of a sandbox. Install `@vercel/sandbox` if you create drives in your app. Create or retrieve the Drive before calling `open()`, then pass its ID and mount mode under an absolute path:

```ts
import { defineSandbox } from "eve/sandbox";
import { Drive, VercelSandbox } from "eve/sandbox/vercel";
import { VercelSandbox } from "eve/sandbox/vercel";
import { Drive } from "@vercel/sandbox";

const REGION = "iad1";

Expand All @@ -124,7 +125,7 @@ export default defineSandbox(async () => {

return environment.open({
mounts: {
"/data": drive,
"/data": { drive: drive.driveId, mode: "read-write" },
},
});
});
Expand Down
2 changes: 1 addition & 1 deletion docs/tools/workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ async function chooseTarget(
"use step";

const result = await decide({
abortSignal,
signal: abortSignal,
state: { task },
questions: {
route: {
Expand Down
2 changes: 1 addition & 1 deletion e2e/fixtures/agent-decide/agent/tools/decide-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default defineTool({
},
},
},
abortSignal: ctx.abortSignal,
signal: ctx.abortSignal,
});
return { choice: result.answers.route.choice, usage: result.usage };
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"kind": "eve-extension-capability-contract",
"capability": "dynamicTool",
"epoch": 87,
"sha256": "b49b94b2f891ea661733eb70a56d89dcbb55f81c625b25b2e23856f93675eca0",
"sha256": "f1fdc214384760419e3e08d17acad33e0d72c29c29effc36ebb643dc913a30de",
"exports": [
"DynamicToolEntry",
"DynamicToolEvents",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"kind": "eve-extension-capability-contract",
"capability": "subagent",
"epoch": 39,
"sha256": "7808eaaa7c0b837f1f22288c4a126e4714d0e57fb6b5ee4d2ec547dce4369c84",
"sha256": "1833f1925c6f8fc9f438deb819381d748609adfebc12736e587301f04fb7b6bb",
"exports": [
"AgentCompactionDefinition",
"AgentDefinition",
Expand Down
2 changes: 1 addition & 1 deletion packages/eve/extension-contracts/reports/tool/v93.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"kind": "eve-extension-capability-contract",
"capability": "tool",
"epoch": 93,
"sha256": "ba09dbf4d14666c0141425e2f9d641a5914907c3e512ed56e84c2375cb1162d4",
"sha256": "51586ffb8c30c324462effcd26dee0ee0c6d21aeab90e6a7da297fc3fa25d6ae",
"exports": [
"AgentRouterInput",
"AgentRouterTool",
Expand Down
12 changes: 11 additions & 1 deletion packages/eve/public-api/eve/ai.json
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
["decide"]
[
"DecisionAnswer",
"DecisionInput",
"DecisionModel",
"DecisionOptions",
"DecisionQuestion",
"DecisionResult",
"DecisionState",
"DecisionStatePart",
"decide"
]
2 changes: 0 additions & 2 deletions packages/eve/public-api/eve/channels/slack.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
"ANSWER_ACTION_PREFIX",
"Actions",
"ActionsElement",
"AdapterPostableMessage",
"Attachment",
"BlockKitBlock",
"Button",
"ButtonElement",
Expand Down
8 changes: 2 additions & 6 deletions packages/eve/public-api/eve/next.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
[
"EveNextConfig",
"EveNextConfigFunction",
"EveNextConfigInput",
"EveNextRewriteRule",
"EveNextRewriteSections",
"EveNextRewrites",
"WithEveAgentOptions",
"WithEveAgentsConfig",
"WithEveConfigFunction",
"WithEveConfigInput",
"WithEveOptions",
"withEve"
]
1 change: 0 additions & 1 deletion packages/eve/public-api/eve/sandbox/vercel.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
[
"Drive",
"VercelSandbox",
"VercelSandboxEnvironmentOptions",
"VercelSandboxMount",
Expand Down
29 changes: 21 additions & 8 deletions packages/eve/src/ai/decide.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ describe("decide", () => {
expectTypeOf(result.answers.urgent.probability).toEqualTypeOf<number>();
expectTypeOf(result.answers.priority.score).toEqualTypeOf<number>();
expect(result.usage).toEqual({ inputTokens: 42, outputTokens: 3, totalTokens: 45 });
expect(result.providerMetadata).toEqual({ fixture: { requestId: "request-1" } });
expect(Object.keys(result).sort()).toEqual(["answers", "usage"]);
expectTypeOf<Parameters<typeof decide>[0]>().not.toHaveProperty("_internal");
expectTypeOf<Parameters<typeof decide>[0]>().not.toHaveProperty("telemetry");
});

it("resolves an explicit model ID through the local connection", async () => {
Expand All @@ -75,16 +77,14 @@ describe("decide", () => {
expect(localDecisionModel).not.toHaveBeenCalled();
});

it("passes explicit model instances and request options through unchanged", async () => {
it("translates stable request options for explicit provider model instances", async () => {
const decider = decisionModel();
const abortSignal = new AbortController().signal;
await decide({
model: decider.model,
state,
questions,
abortSignal,
headers: { "x-request-id": "request-1" },
providerOptions: { fixture: { mode: "fast" } },
signal: abortSignal,
maxRetries: 0,
});

Expand All @@ -94,12 +94,25 @@ describe("decide", () => {
state: [{ type: "json", value: state }],
questions,
abortSignal,
headers: expect.objectContaining({ "x-request-id": "request-1" }),
providerOptions: { fixture: { mode: "fast" } },
}),
);
});

it("rejects identity-only objects and unsupported model versions before provider I/O", async () => {
await expect(
decide({
// @ts-expect-error Identity alone is not a provider implementation.
model: { provider: "fixture", modelId: "missing", specificationVersion: "v4" },
state,
questions,
}),
).rejects.toThrow("not just its identity");
const decider = decisionModel();
const newerModel = { ...decider.model, specificationVersion: "v-next" };
await expect(decide({ model: newerModel, state, questions })).rejects.toThrow(/version/i);
expect(decider.doDecide).not.toHaveBeenCalled();
});

it("preserves input and answer validation", async () => {
const decider = decisionModel();
await expect(decide({ model: decider.model, state, questions: {} })).rejects.toThrow();
Expand Down Expand Up @@ -128,7 +141,7 @@ describe("decide", () => {
model: decider.model,
state,
questions,
abortSignal: AbortSignal.abort(error),
signal: AbortSignal.abort(error),
}),
).rejects.toBe(error);
expect(decider.doDecide).not.toHaveBeenCalled();
Expand Down
41 changes: 15 additions & 26 deletions packages/eve/src/ai/decide.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import {
experimental_decide as decideWithAiSdk,
type Experimental_DecisionModel as DecisionModel,
type Experimental_DecisionQuestion as DecisionQuestion,
} from "ai";
import { decideWithMetadata } from "#ai/decision-adapter.js";
import type { DecisionOptions, DecisionQuestion, DecisionResult } from "#ai/decision-types.js";

import { ensureAiSdkWarningLogger } from "#instrumentation/ai-sdk-warnings.js";
import { localGatewayDecisionModel } from "#internal/model-auth/transport.js";

export const DEFAULT_DECISION_MODEL = "typesafe-ai/jev";

/** Decide typed questions about the state you pass to it using eve's model authentication. */
export function decide<const Questions extends Record<string, DecisionQuestion>>({
model = DEFAULT_DECISION_MODEL,
...options
}: Omit<Parameters<typeof decideWithAiSdk<Questions>>[0], "model"> & {
/** Decision model instance or ID. Defaults to TypeSafe Jev. */
model?: DecisionModel;
}) {
ensureAiSdkWarningLogger();
return decideWithAiSdk({
...options,
model:
typeof model === "string" && Reflect.get(globalThis, "AI_SDK_DEFAULT_PROVIDER") == null
? (localGatewayDecisionModel(model) ?? model)
: model,
});
/** Decide typed questions about shared state using eve's model authentication. */
export async function decide<const Questions extends Readonly<Record<string, DecisionQuestion>>>(
options: DecisionOptions<Questions>,
): Promise<DecisionResult<Questions>> {
const result = await decideWithMetadata(options);
return {
answers: result.answers as DecisionResult<Questions>["answers"],
usage: {
inputTokens: result.usage.inputTokens,
outputTokens: result.usage.outputTokens,
totalTokens: result.usage.totalTokens,
},
};
}
44 changes: 44 additions & 0 deletions packages/eve/src/ai/decision-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { experimental_decide as decideWithAiSdk } from "ai";
import type { DecisionOptions, DecisionQuestion } from "#ai/decision-types.js";
import { ensureAiSdkWarningLogger } from "#instrumentation/ai-sdk-warnings.js";
import { localGatewayDecisionModel } from "#internal/model-auth/transport.js";

export const DEFAULT_DECISION_MODEL = "typesafe-ai/jev";

/** Internal eval diagnostics retain SDK metadata without promising it to applications. */
export function decideWithMetadata<
const Questions extends Readonly<Record<string, DecisionQuestion>>,
>({
model = DEFAULT_DECISION_MODEL,
state,
questions,
maxRetries,
signal,
providerOptions,
}: DecisionOptions<Questions> & {
readonly providerOptions?: Parameters<typeof decideWithAiSdk>[0]["providerOptions"];
}) {
ensureAiSdkWarningLogger();
if (
typeof model !== "string" &&
typeof Reflect.get(model, "doDecide") !== "function" &&
typeof Reflect.get(model, "doEvaluate") !== "function"
) {
throw new TypeError(
"decide requires a provider decision model implementation, not just its identity.",
);
}
// The SDK validates model versions, questions, state, and answers. The cast
// adapts eve's read-only JSON and provider-instance boundary to that SDK version.
return decideWithAiSdk({
model:
typeof model === "string" && Reflect.get(globalThis, "AI_SDK_DEFAULT_PROVIDER") == null
? (localGatewayDecisionModel(model) ?? model)
: model,
state,
questions,
maxRetries,
abortSignal: signal,
providerOptions,
} as Parameters<typeof decideWithAiSdk>[0]);
}
Loading
Loading