diff --git a/.changeset/owned-decision-contracts.md b/.changeset/owned-decision-contracts.md new file mode 100644 index 0000000000..26aaf27a15 --- /dev/null +++ b/.changeset/owned-decision-contracts.md @@ -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. diff --git a/.changeset/owned-platform-contracts.md b/.changeset/owned-platform-contracts.md new file mode 100644 index 0000000000..3b3b85d438 --- /dev/null +++ b/.changeset/owned-platform-contracts.md @@ -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. diff --git a/docs/concepts/upgrade-to-v27-events.md b/docs/concepts/upgrade-to-v27-events.md index ac436f0ee7..e0ab8d0a74 100644 --- a/docs/concepts/upgrade-to-v27-events.md +++ b/docs/concepts/upgrade-to-v27-events.md @@ -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' diff --git a/docs/guides/decide.md b/docs/guides/decide.md index 6bf9eb9bc9..18c79fb373 100644 --- a/docs/guides/decide.md +++ b/docs/guides/decide.md @@ -158,7 +158,7 @@ export default defineTool({ }, }, }, - abortSignal: ctx.abortSignal, + signal: ctx.abortSignal, }); return { team: result.answers.team.choice }; }, @@ -166,11 +166,16 @@ export default defineTool({ ``` 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 diff --git a/docs/sandbox/vercel.mdx b/docs/sandbox/vercel.mdx index 718006e26b..d6f31c6b54 100644 --- a/docs/sandbox/vercel.mdx +++ b/docs/sandbox/vercel.mdx @@ -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"; @@ -124,7 +125,7 @@ export default defineSandbox(async () => { return environment.open({ mounts: { - "/data": drive, + "/data": { drive: drive.driveId, mode: "read-write" }, }, }); }); diff --git a/docs/tools/workflows.mdx b/docs/tools/workflows.mdx index 3da11a12ab..b77644307f 100644 --- a/docs/tools/workflows.mdx +++ b/docs/tools/workflows.mdx @@ -620,7 +620,7 @@ async function chooseTarget( "use step"; const result = await decide({ - abortSignal, + signal: abortSignal, state: { task }, questions: { route: { diff --git a/e2e/fixtures/agent-decide/agent/tools/decide-request.ts b/e2e/fixtures/agent-decide/agent/tools/decide-request.ts index e73cf25ab9..f731bc4ec3 100644 --- a/e2e/fixtures/agent-decide/agent/tools/decide-request.ts +++ b/e2e/fixtures/agent-decide/agent/tools/decide-request.ts @@ -27,7 +27,7 @@ export default defineTool({ }, }, }, - abortSignal: ctx.abortSignal, + signal: ctx.abortSignal, }); return { choice: result.answers.route.choice, usage: result.usage }; }, diff --git a/packages/eve/extension-contracts/reports/dynamicTool/v87.json b/packages/eve/extension-contracts/reports/dynamicTool/v87.json index 6b836d593f..ec687e72f9 100644 --- a/packages/eve/extension-contracts/reports/dynamicTool/v87.json +++ b/packages/eve/extension-contracts/reports/dynamicTool/v87.json @@ -2,7 +2,7 @@ "kind": "eve-extension-capability-contract", "capability": "dynamicTool", "epoch": 87, - "sha256": "b49b94b2f891ea661733eb70a56d89dcbb55f81c625b25b2e23856f93675eca0", + "sha256": "f1fdc214384760419e3e08d17acad33e0d72c29c29effc36ebb643dc913a30de", "exports": [ "DynamicToolEntry", "DynamicToolEvents", diff --git a/packages/eve/extension-contracts/reports/subagent/v39.json b/packages/eve/extension-contracts/reports/subagent/v39.json index 0bff1b0354..ff1f9a98b6 100644 --- a/packages/eve/extension-contracts/reports/subagent/v39.json +++ b/packages/eve/extension-contracts/reports/subagent/v39.json @@ -2,7 +2,7 @@ "kind": "eve-extension-capability-contract", "capability": "subagent", "epoch": 39, - "sha256": "7808eaaa7c0b837f1f22288c4a126e4714d0e57fb6b5ee4d2ec547dce4369c84", + "sha256": "1833f1925c6f8fc9f438deb819381d748609adfebc12736e587301f04fb7b6bb", "exports": [ "AgentCompactionDefinition", "AgentDefinition", diff --git a/packages/eve/extension-contracts/reports/tool/v93.json b/packages/eve/extension-contracts/reports/tool/v93.json index 23f05ef29e..c59e6cf9a0 100644 --- a/packages/eve/extension-contracts/reports/tool/v93.json +++ b/packages/eve/extension-contracts/reports/tool/v93.json @@ -2,7 +2,7 @@ "kind": "eve-extension-capability-contract", "capability": "tool", "epoch": 93, - "sha256": "ba09dbf4d14666c0141425e2f9d641a5914907c3e512ed56e84c2375cb1162d4", + "sha256": "51586ffb8c30c324462effcd26dee0ee0c6d21aeab90e6a7da297fc3fa25d6ae", "exports": [ "AgentRouterInput", "AgentRouterTool", diff --git a/packages/eve/public-api/eve/ai.json b/packages/eve/public-api/eve/ai.json index 9c76ae60f1..27c56a3e00 100644 --- a/packages/eve/public-api/eve/ai.json +++ b/packages/eve/public-api/eve/ai.json @@ -1 +1,11 @@ -["decide"] +[ + "DecisionAnswer", + "DecisionInput", + "DecisionModel", + "DecisionOptions", + "DecisionQuestion", + "DecisionResult", + "DecisionState", + "DecisionStatePart", + "decide" +] diff --git a/packages/eve/public-api/eve/channels/slack.json b/packages/eve/public-api/eve/channels/slack.json index 79f8f4d79d..b02c032750 100644 --- a/packages/eve/public-api/eve/channels/slack.json +++ b/packages/eve/public-api/eve/channels/slack.json @@ -2,8 +2,6 @@ "ANSWER_ACTION_PREFIX", "Actions", "ActionsElement", - "AdapterPostableMessage", - "Attachment", "BlockKitBlock", "Button", "ButtonElement", diff --git a/packages/eve/public-api/eve/next.json b/packages/eve/public-api/eve/next.json index f285764a63..0c235e8247 100644 --- a/packages/eve/public-api/eve/next.json +++ b/packages/eve/public-api/eve/next.json @@ -1,12 +1,8 @@ [ - "EveNextConfig", - "EveNextConfigFunction", - "EveNextConfigInput", - "EveNextRewriteRule", - "EveNextRewriteSections", - "EveNextRewrites", "WithEveAgentOptions", "WithEveAgentsConfig", + "WithEveConfigFunction", + "WithEveConfigInput", "WithEveOptions", "withEve" ] diff --git a/packages/eve/public-api/eve/sandbox/vercel.json b/packages/eve/public-api/eve/sandbox/vercel.json index dc532b4d9e..16f7556a0b 100644 --- a/packages/eve/public-api/eve/sandbox/vercel.json +++ b/packages/eve/public-api/eve/sandbox/vercel.json @@ -1,5 +1,4 @@ [ - "Drive", "VercelSandbox", "VercelSandboxEnvironmentOptions", "VercelSandboxMount", diff --git a/packages/eve/src/ai/decide.integration.test.ts b/packages/eve/src/ai/decide.integration.test.ts index d4fc9cc4d9..cdc8467747 100644 --- a/packages/eve/src/ai/decide.integration.test.ts +++ b/packages/eve/src/ai/decide.integration.test.ts @@ -53,7 +53,9 @@ describe("decide", () => { expectTypeOf(result.answers.urgent.probability).toEqualTypeOf(); expectTypeOf(result.answers.priority.score).toEqualTypeOf(); 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[0]>().not.toHaveProperty("_internal"); + expectTypeOf[0]>().not.toHaveProperty("telemetry"); }); it("resolves an explicit model ID through the local connection", async () => { @@ -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, }); @@ -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(); @@ -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(); diff --git a/packages/eve/src/ai/decide.ts b/packages/eve/src/ai/decide.ts index 9627631d62..04d04fd0be 100644 --- a/packages/eve/src/ai/decide.ts +++ b/packages/eve/src/ai/decide.ts @@ -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>({ - model = DEFAULT_DECISION_MODEL, - ...options -}: Omit>[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>>( + options: DecisionOptions, +): Promise> { + const result = await decideWithMetadata(options); + return { + answers: result.answers as DecisionResult["answers"], + usage: { + inputTokens: result.usage.inputTokens, + outputTokens: result.usage.outputTokens, + totalTokens: result.usage.totalTokens, + }, + }; } diff --git a/packages/eve/src/ai/decision-adapter.ts b/packages/eve/src/ai/decision-adapter.ts new file mode 100644 index 0000000000..96ee2e8e85 --- /dev/null +++ b/packages/eve/src/ai/decision-adapter.ts @@ -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>, +>({ + model = DEFAULT_DECISION_MODEL, + state, + questions, + maxRetries, + signal, + providerOptions, +}: DecisionOptions & { + readonly providerOptions?: Parameters[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[0]); +} diff --git a/packages/eve/src/ai/decision-types.ts b/packages/eve/src/ai/decision-types.ts new file mode 100644 index 0000000000..7d1d244006 --- /dev/null +++ b/packages/eve/src/ai/decision-types.ts @@ -0,0 +1,89 @@ +// Question and answer shapes adapted from AI SDK 7.0.137 (Apache-2.0). +// eve owns these contracts; AI SDK remains the runtime validation boundary. +import type { HistoryMessage } from "#shared/history-message.js"; +import type { JsonObject, JsonValue } from "#shared/json.js"; + +/** A model ID or an installed provider's decision model instance. + * The installed AI SDK validates the model specification at runtime. + * An identity object without a decision implementation is not a model. + */ +export type DecisionModel = + | string + | ({ + readonly provider: string; + readonly modelId: string; + readonly specificationVersion: string; + } & ( + | { readonly doDecide: (...args: never[]) => unknown } + | { readonly doEvaluate: (...args: never[]) => unknown } + )); + +/** Text, an object, or ordered multimodal parts shared by every question. */ +export type DecisionState = string | JsonObject | readonly DecisionStatePart[]; +type UserContent = Extract["content"]; +/** One text, file, or JSON part of shared decision state. */ +export type DecisionStatePart = + | Extract[number], { type: "text" | "file" }> + | { readonly type: "json"; readonly value: JsonValue }; + +/** Text or JSON used to describe a question or rubric entry. */ +export type DecisionInput = string | JsonObject | readonly JsonValue[]; +/** One independently answered choice, ordered score, or probability question. */ +export type DecisionQuestion = + | { + readonly type: "choice"; + readonly instructions: DecisionInput; + readonly criteria: Readonly>; + } + | { + readonly type: "score"; + readonly instructions: DecisionInput; + readonly criteria: readonly (DecisionInput | null)[]; + } + | { + readonly type: "boolean"; + readonly instructions: DecisionInput; + readonly criteria?: { + readonly true?: DecisionInput | null; + readonly false?: DecisionInput | null; + }; + }; + +/** Answer inferred from the authored question and its choice keys. */ +export type DecisionAnswer = Q extends { + type: "choice"; + criteria: infer Criteria; +} + ? { + readonly type: "choice"; + readonly choice: Extract; + readonly probabilities?: Readonly, number>>; + } + : Q extends { type: "score" } + ? { + readonly type: "score"; + readonly score: number; + readonly probabilities?: Readonly>; + } + : { readonly type: "boolean"; readonly probability: number }; + +/** Typed answers and normalized token usage; provider metadata stays internal. */ +export interface DecisionResult>> { + readonly answers: { readonly [Id in keyof Questions]: DecisionAnswer }; + readonly usage: { + readonly inputTokens: number | undefined; + readonly outputTokens: number | undefined; + readonly totalTokens: number | undefined; + }; +} + +/** Stable options accepted by eve's standalone decision API. */ +export interface DecisionOptions>> { + /** Defaults to TypeSafe Jev, using eve's normal model authentication. */ + readonly model?: DecisionModel; + readonly state: DecisionState; + readonly questions: Questions; + /** Maximum retries for transient provider failures. Defaults to 2. */ + readonly maxRetries?: number; + readonly signal?: AbortSignal; +} diff --git a/packages/eve/src/ai/package.scenario.test.ts b/packages/eve/src/ai/package.scenario.test.ts index 3c5e263c66..5b248e04d7 100644 --- a/packages/eve/src/ai/package.scenario.test.ts +++ b/packages/eve/src/ai/package.scenario.test.ts @@ -35,7 +35,7 @@ export default defineTool({ const result = await decide({ state: { incident: "Alice needs an export summary." }, questions: { category: { type: "choice", instructions: "Choose a category.", criteria: { routine: "Routine work", investigation: "Investigation" } } }, - abortSignal: ctx.abortSignal, + signal: ctx.abortSignal, }); return result.answers.category.choice; }, diff --git a/packages/eve/src/evals/judge.ts b/packages/eve/src/evals/judge.ts index 5751b8dd9b..64acd24b4d 100644 --- a/packages/eve/src/evals/judge.ts +++ b/packages/eve/src/evals/judge.ts @@ -1,4 +1,4 @@ -import { decide } from "#ai/decide.js"; +import { decideWithMetadata } from "#ai/decision-adapter.js"; import { runUntilAborted } from "#evals/abort.js"; import type { AssertionCollector } from "#evals/assertions/collector.js"; import { formatDiagnosticValue, toDiagnosticMetadataValue } from "#evals/diagnostics.js"; @@ -69,7 +69,7 @@ export function buildJudgeContext(deps: JudgeDeps): JudgeContext { return [id, decisionQuestion]; }), ); - const result = await decide({ + const result = await decideWithMetadata({ model, // The AI SDK reads a bare array as state parts, so JSON arrays travel as one JSON part. state: isJsonArray(capturedState) @@ -77,7 +77,7 @@ export function buildJudgeContext(deps: JudgeDeps): JudgeContext { : capturedState, questions: decisionQuestions, providerOptions: modelOptions?.providerOptions, - abortSignal: deps.signal, + signal: deps.signal, }); return { result, state: capturedState, questions: capturedQuestions }; })(), @@ -138,7 +138,7 @@ function formatJudgeDetail(state: JudgeInput, question: JudgeQuestion, answer: u function judgeScore( question: JudgeQuestion, - answer: Awaited>["answers"][string], + answer: Awaited>["answers"][string], ): number { switch (answer.type) { case "boolean": diff --git a/packages/eve/src/evals/types.ts b/packages/eve/src/evals/types.ts index 55ade6679b..bc59e86eee 100644 --- a/packages/eve/src/evals/types.ts +++ b/packages/eve/src/evals/types.ts @@ -4,7 +4,7 @@ import type { RequestStatus } from "#protocol/session-projection/request-status. import type { ChildOpened } from "#protocol/session-events/families/child.js"; import type { SessionStreamEvent } from "#protocol/session-event.js"; import type { TokenUsage } from "#shared/token-usage.js"; -import type { Experimental_DecisionModel as DecisionModel } from "ai"; +import type { DecisionModel } from "#ai/decision-types.js"; import type { StandardSchemaV1 } from "#compiled/@standard-schema/spec/index.js"; import type { ClientAgentSession } from "#client/agent-session.js"; diff --git a/packages/eve/src/execution/tools/agent-router-workflow.ts b/packages/eve/src/execution/tools/agent-router-workflow.ts index 250efddad7..7caa7c4144 100644 --- a/packages/eve/src/execution/tools/agent-router-workflow.ts +++ b/packages/eve/src/execution/tools/agent-router-workflow.ts @@ -33,7 +33,7 @@ async function chooseTarget( if (names.length === 1) return names[0]!; const result = await decide({ - abortSignal, + signal: abortSignal, state: { message }, questions: { route: { diff --git a/packages/eve/src/models/auto.ts b/packages/eve/src/models/auto.ts index 88c3e8b943..1d8a4ba18a 100644 --- a/packages/eve/src/models/auto.ts +++ b/packages/eve/src/models/auto.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto"; -import { type Experimental_DecisionModel as DecisionModel } from "ai"; +import type { DecisionModel } from "#ai/decision-types.js"; import { loadContext } from "#context/container.js"; import { ContextKey } from "#context/key.js"; @@ -17,7 +17,8 @@ import type { PublicAgentStaticModelDefinition, } from "#shared/agent-definition.js"; -import { DEFAULT_DECISION_MODEL, decide } from "#ai/decide.js"; +import { decide } from "#ai/decide.js"; +import { DEFAULT_DECISION_MODEL } from "#ai/decision-adapter.js"; type AutoModelSelection = | PublicAgentStaticModelDefinition @@ -234,7 +235,7 @@ export function auto>>( criteria, }, }, - abortSignal: ctx.abortSignal, + signal: ctx.abortSignal, }); ctx.abortSignal?.throwIfAborted(); diff --git a/packages/eve/src/public/ai/index.ts b/packages/eve/src/public/ai/index.ts index e9f0af6c61..29d0a1584b 100644 --- a/packages/eve/src/public/ai/index.ts +++ b/packages/eve/src/public/ai/index.ts @@ -1 +1,11 @@ export { decide } from "#ai/decide.js"; +export type { + DecisionAnswer, + DecisionInput, + DecisionModel, + DecisionOptions, + DecisionQuestion, + DecisionResult, + DecisionState, + DecisionStatePart, +} from "#ai/decision-types.js"; diff --git a/packages/eve/src/public/channels/slack/api.ts b/packages/eve/src/public/channels/slack/api.ts index ffd2398fcc..15a16ec183 100644 --- a/packages/eve/src/public/channels/slack/api.ts +++ b/packages/eve/src/public/channels/slack/api.ts @@ -26,7 +26,7 @@ import { type SlackFileUpload, type SlackMessageOptions, } from "#compiled/@chat-adapter/slack/api.js"; -import { isCardElement, type CardElement, type FileUpload } from "#compiled/chat/index.js"; +import { isCardElement, type CardElement, type FileUpload } from "#public/channels/slack/cards.js"; import { createLogger, logError } from "#internal/logging.js"; import { cardToBlocks, cardToFallbackText } from "#public/channels/slack/blocks.js"; diff --git a/packages/eve/src/public/channels/slack/blocks.test.ts b/packages/eve/src/public/channels/slack/blocks.test.ts index 7125160bb4..f6cbfbd082 100644 --- a/packages/eve/src/public/channels/slack/blocks.test.ts +++ b/packages/eve/src/public/channels/slack/blocks.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, expectTypeOf, it } from "vitest"; import { Actions, @@ -14,12 +14,15 @@ import { Field, Image, TextInput, -} from "#compiled/chat/index.js"; +} from "#public/channels/slack/cards.js"; +import type { CardElement } from "#public/channels/slack/cards.js"; +import type { CardElement as SdkCardElement } from "#compiled/chat/index.js"; import { cardToBlocks, cardToFallbackText } from "#public/channels/slack/blocks.js"; describe("cardToBlocks", () => { it("renders a text input as its label, like the Slack adapter", () => { + expectTypeOf().toEqualTypeOf(); const blocks = cardToBlocks( Card({ children: [Section([TextInput({ id: "reason", label: "Reason" })])] }), ); diff --git a/packages/eve/src/public/channels/slack/blocks.ts b/packages/eve/src/public/channels/slack/blocks.ts index 43b8c2afad..b64c7e937d 100644 --- a/packages/eve/src/public/channels/slack/blocks.ts +++ b/packages/eve/src/public/channels/slack/blocks.ts @@ -1,5 +1,9 @@ import { cardToSlackBlocks, type SlackCardChild } from "#compiled/@chat-adapter/slack/blocks.js"; -import { cardChildToFallbackText, type CardChild, type CardElement } from "#compiled/chat/index.js"; +import { + cardChildToFallbackText, + type CardChild, + type CardElement, +} from "#public/channels/slack/cards.js"; /** A Slack Block Kit block emitted by {@link cardToBlocks}. */ export type BlockKitBlock = Record; diff --git a/packages/eve/src/public/channels/slack/cards.ts b/packages/eve/src/public/channels/slack/cards.ts new file mode 100644 index 0000000000..c6b7d40cdf --- /dev/null +++ b/packages/eve/src/public/channels/slack/cards.ts @@ -0,0 +1,702 @@ +/** eve-owned Slack card data contracts. Chat SDK classes retain their identity at eve/channels/chat-sdk. */ +import * as chat from "#compiled/chat/index.js"; +declare const _Actions: ActionsComponent; +interface ActionsComponent { + ( + children: (ButtonElement | LinkButtonElement | SelectElement | RadioSelectElement)[], + ): ActionsElement; + (props: ContainerProps): ChatElement; +} +interface ButtonElement { + actionType?: "action" | "modal"; + callbackUrl?: string; + disabled?: boolean; + id: string; + label: string; + style?: ButtonStyle; + tooltip?: string; + type: "button"; + value?: string; + visibleTo?: string[]; +} +type ButtonStyle = "primary" | "danger" | "default"; +interface LinkButtonElement { + id?: string; + label: string; + style?: ButtonStyle; + tooltip?: string; + type: "link-button"; + url: string; + visibleTo?: string[]; +} +interface SelectElement { + dispatchAction?: boolean; + id: string; + initialOption?: string; + label: string; + optional?: boolean; + options: SelectOptionElement[]; + placeholder?: string; + type: "select"; +} +interface SelectOptionElement { + description?: string; + label: string; + value: string; +} +interface RadioSelectElement { + dispatchAction?: boolean; + id: string; + initialOption?: string; + label: string; + optional?: boolean; + options: SelectOptionElement[]; + type: "radio_select"; +} +interface ActionsElement { + children: (ButtonElement | LinkButtonElement | SelectElement | RadioSelectElement)[]; + type: "actions"; +} +interface ContainerProps { + children?: unknown; +} +type ChatElement = + | CardJSXElement + | CardElement + | TextElement + | ButtonElement + | LinkButtonElement + | LinkElement + | ImageElement + | DividerElement + | ActionsElement + | SectionElement + | FieldsElement + | FieldElement + | ModalElement + | TextInputElement + | DateInputElement + | NumberInputElement + | SelectElement + | ExternalSelectElement + | SelectOptionElement + | RadioSelectElement + | TableElement + | ChartElement; +interface CardJSXElement

{ + $typeof: symbol; + children: unknown[]; + props: P; + type: CardComponentFunction; +} +type CardJSXProps = + | CardProps + | TextProps + | ButtonProps + | LinkButtonProps + | CardLinkProps + | ImageProps + | FieldProps + | ContainerProps + | DividerProps + | ModalProps + | TextInputProps + | DateInputProps + | NumberInputProps + | SelectProps + | ExternalSelectProps + | SelectOptionProps + | TableProps + | ChartProps; +interface CardProps { + children?: unknown; + imageUrl?: string; + subtitle?: string; + title?: string; + width?: CardWidth; +} +type CardWidth = "default" | "full"; +interface TextProps { + children?: string | number | (string | number | undefined)[]; + style?: TextStyle; +} +type TextStyle = "plain" | "bold" | "muted"; +interface ButtonProps { + actionType?: "action" | "modal"; + callbackUrl?: string; + children?: string | number | (string | number | undefined)[]; + disabled?: boolean; + id: string; + label?: string; + style?: ButtonStyle; + tooltip?: string; + value?: string; + visibleTo?: string[]; +} +interface LinkButtonProps { + children?: string | number | (string | number | undefined)[]; + id?: string; + label?: string; + style?: ButtonStyle; + tooltip?: string; + url: string; + visibleTo?: string[]; +} +interface CardLinkProps { + children?: string | number | (string | number | undefined)[]; + label?: string; + url: string; +} +interface ImageProps { + alt?: string; + url: string; +} +interface FieldProps { + label: string; + value: string; +} +type DividerProps = Record; +interface ModalProps { + callbackId: string; + callbackUrl?: string; + children?: unknown; + closeLabel?: string; + notifyOnClose?: boolean; + privateMetadata?: string; + submitLabel?: string; + title: string; +} +interface TextInputProps { + id: string; + initialValue?: string; + label: string; + maxLength?: number; + multiline?: boolean; + optional?: boolean; + placeholder?: string; +} +interface DateInputProps { + id: string; + initialValue?: string; + label: string; + optional?: boolean; + placeholder?: string; +} +interface NumberInputProps { + decimal?: boolean; + id: string; + initialValue?: number; + label: string; + max?: number; + min?: number; + optional?: boolean; + placeholder?: string; +} +interface SelectProps { + children?: unknown; + dispatchAction?: boolean; + id: string; + initialOption?: string; + label: string; + optional?: boolean; + placeholder?: string; +} +interface ExternalSelectProps { + id: string; + initialOption?: { + label: string; + value: string; + }; + label: string; + minQueryLength?: number; + optional?: boolean; + placeholder?: string; +} +interface SelectOptionProps { + description?: string; + label: string; + value: string; +} +type TableProps = TableOptions; +interface TableOptions { + align?: TableAlignment[]; + caption?: string; + gridLines?: boolean; + gridStyle?: TableGridStyle; + headers: string[]; + pageSize?: number; + rows: string[][]; + verticalAlign?: TableVerticalAlignment; + widths?: number[]; +} +type TableAlignment = "left" | "center" | "right"; +type TableGridStyle = "default" | "emphasis" | "accent" | "good" | "attention" | "warning"; +type TableVerticalAlignment = "top" | "center" | "bottom"; +interface ChartProps { + chart: ChartDefinition; + title: string; +} +type ChartDefinition = PieChartDefinition | SeriesChartDefinition; +interface PieChartDefinition { + segments: ChartSegment[]; + type: "pie"; +} +interface ChartSegment { + label: string; + value: number; +} +interface SeriesChartDefinition { + categories: string[]; + series: ChartSeries[]; + type: "area" | "bar" | "line"; + xLabel?: string; + yLabel?: string; +} +interface ChartSeries { + data: ChartDataPoint[]; + name: string; +} +interface ChartDataPoint { + label: string; + value: number; +} +type CardComponentFunction = + | typeof _Card + | typeof Text + | typeof _Button + | typeof _LinkButton + | typeof _CardLink + | typeof _Image + | typeof _Field + | typeof _Divider + | typeof _Section + | typeof _ActionsInternal + | typeof _Fields + | typeof _Modal + | typeof _TextInput + | typeof DateInput + | typeof NumberInput + | typeof _Select + | typeof _ExternalSelect + | typeof _RadioSelect + | typeof _SelectOption + | typeof _Table + | typeof Chart; +declare function _Card(options?: CardOptions): CardElement; +interface CardOptions { + children?: CardChild[]; + imageUrl?: string; + subtitle?: string; + title?: string; + width?: CardWidth; +} +type CardChild = + | TextElement + | ImageElement + | DividerElement + | ActionsElement + | SectionElement + | FieldsElement + | LinkElement + | TableElement + | ChartElement + | TextInputElement; +interface TextElement { + content: string; + style?: TextStyle; + type: "text"; +} +interface ImageElement { + alt?: string; + type: "image"; + url: string; +} +interface DividerElement { + type: "divider"; +} +interface SectionElement { + children: CardChild[]; + type: "section"; +} +interface FieldsElement { + children: FieldElement[]; + type: "fields"; +} +interface FieldElement { + label: string; + type: "field"; + value: string; +} +interface LinkElement { + label: string; + type: "link"; + url: string; +} +interface TableElement extends TableOptions { + type: "table"; +} +interface ChartElement { + chart: ChartDefinition; + title: string; + type: "chart"; +} +interface TextInputElement { + id: string; + initialValue?: string; + label: string; + maxLength?: number; + multiline?: boolean; + optional?: boolean; + placeholder?: string; + type: "text_input"; +} +interface CardElement { + children: CardChild[]; + imageUrl?: string; + subtitle?: string; + title?: string; + type: "card"; + width?: CardWidth; +} +declare function Text( + content: string, + options?: { + style?: TextStyle; + }, +): TextElement; +declare function _Button(options: ButtonOptions): ButtonElement; +interface ButtonOptions { + actionType?: "action" | "modal"; + callbackUrl?: string; + disabled?: boolean; + id: string; + label: string; + style?: ButtonStyle; + tooltip?: string; + value?: string; + visibleTo?: string[]; +} +declare function _LinkButton(options: LinkButtonOptions): LinkButtonElement; +interface LinkButtonOptions { + id?: string; + label: string; + style?: ButtonStyle; + tooltip?: string; + url: string; + visibleTo?: string[]; +} +declare function _CardLink(options: { url: string; label: string }): LinkElement; +declare function _Image(options: { url: string; alt?: string }): ImageElement; +declare function _Field(options: { label: string; value: string }): FieldElement; +declare function _Divider(): DividerElement; +declare function _Section(children: CardChild[]): SectionElement; +declare function _ActionsInternal( + children: (ButtonElement | LinkButtonElement | SelectElement | RadioSelectElement)[], +): ActionsElement; +declare function _Fields(children: FieldElement[]): FieldsElement; +declare function _Modal(options: ModalOptions): ModalElement; +interface ModalOptions { + callbackId: string; + callbackUrl?: string; + children?: ModalChild[]; + closeLabel?: string; + notifyOnClose?: boolean; + privateMetadata?: string; + submitLabel?: string; + title: string; +} +type ModalChild = + | TextInputElement + | DateInputElement + | NumberInputElement + | SelectElement + | ExternalSelectElement + | RadioSelectElement + | TextElement + | FieldsElement; +interface DateInputElement { + id: string; + initialValue?: string; + label: string; + optional?: boolean; + placeholder?: string; + type: "date_input"; +} +interface NumberInputElement { + decimal?: boolean; + id: string; + initialValue?: number; + label: string; + max?: number; + min?: number; + optional?: boolean; + placeholder?: string; + type: "number_input"; +} +interface ExternalSelectElement { + id: string; + initialOption?: SelectOptionElement; + label: string; + minQueryLength?: number; + optional?: boolean; + placeholder?: string; + type: "external_select"; +} +interface ModalElement { + callbackId: string; + callbackUrl?: string; + children: ModalChild[]; + closeLabel?: string; + notifyOnClose?: boolean; + privateMetadata?: string; + submitLabel?: string; + title: string; + type: "modal"; +} +declare function _TextInput(options: TextInputOptions): TextInputElement; +interface TextInputOptions { + id: string; + initialValue?: string; + label: string; + maxLength?: number; + multiline?: boolean; + optional?: boolean; + placeholder?: string; +} +declare function DateInput(options: DateInputOptions): DateInputElement; +interface DateInputOptions { + id: string; + initialValue?: string; + label: string; + optional?: boolean; + placeholder?: string; +} +declare function NumberInput(options: NumberInputOptions): NumberInputElement; +interface NumberInputOptions { + decimal?: boolean; + id: string; + initialValue?: number; + label: string; + max?: number; + min?: number; + optional?: boolean; + placeholder?: string; +} +declare function _Select(options: SelectOptions): SelectElement; +interface SelectOptions { + dispatchAction?: boolean; + id: string; + initialOption?: string; + label: string; + optional?: boolean; + options: SelectOptionElement[]; + placeholder?: string; +} +declare function _ExternalSelect(options: ExternalSelectOptions): ExternalSelectElement; +interface ExternalSelectOptions { + id: string; + initialOption?: SelectOptionElement; + label: string; + minQueryLength?: number; + optional?: boolean; + placeholder?: string; +} +declare function _RadioSelect(options: RadioSelectOptions): RadioSelectElement; +interface RadioSelectOptions { + dispatchAction?: boolean; + id: string; + initialOption?: string; + label: string; + optional?: boolean; + options: SelectOptionElement[]; +} +declare function _SelectOption(options: { + label: string; + value: string; + description?: string; +}): SelectOptionElement; +declare function _Table(options: TableOptions): TableElement; +declare function Chart(options: ChartOptions): ChartElement; +interface ChartOptions { + chart: ChartDefinition; + title: string; +} +declare const _ButtonInternal: ButtonComponent; +interface ButtonComponent { + (options: ButtonOptions): ButtonElement; + (props: ButtonProps): ChatElement; +} +declare const _CardInternal: CardComponent; +interface CardComponent { + (options?: CardOptions): CardElement; + (props: CardProps): ChatElement; +} +declare const _CardLinkInternal: CardLinkComponent; +interface CardLinkComponent { + (options: { url: string; label: string }): LinkElement; + (props: CardLinkProps): ChatElement; +} +declare const _CardText: TextComponent; +interface TextComponent { + ( + content: string, + options?: { + style?: TextStyle; + }, + ): TextElement; + (props: TextProps): ChatElement; +} +declare const _DividerInternal: DividerComponent; +interface DividerComponent { + (): DividerElement; + (props: DividerProps): ChatElement; +} +declare const _ExternalSelectInternal: ExternalSelectComponent; +interface ExternalSelectComponent { + (options: ExternalSelectOptions): ExternalSelectElement; + (props: ExternalSelectProps): ChatElement; +} +declare const _FieldInternal: FieldComponent; +interface FieldComponent { + (options: { label: string; value: string }): FieldElement; + (props: FieldProps): ChatElement; +} +declare const _FieldsInternal: FieldsComponent; +interface FieldsComponent { + (children: FieldElement[]): FieldsElement; + (props: ContainerProps): ChatElement; +} +declare const _ImageInternal: ImageComponent; +interface ImageComponent { + (options: { url: string; alt?: string }): ImageElement; + (props: ImageProps): ChatElement; +} +declare const _LinkButtonInternal: LinkButtonComponent; +interface LinkButtonComponent { + (options: LinkButtonOptions): LinkButtonElement; + (props: LinkButtonProps): ChatElement; +} +declare const _ModalInternal: ModalComponent; +interface ModalComponent { + (options: ModalOptions): ModalElement; + (props: ModalProps): ChatElement; +} +declare const _RadioSelectInternal: RadioSelectComponent; +interface RadioSelectComponent { + (options: RadioSelectOptions): RadioSelectElement; + (props: SelectProps): ChatElement; +} +declare const _SectionInternal: SectionComponent; +interface SectionComponent { + (children: CardChild[]): SectionElement; + (props: ContainerProps): ChatElement; +} +declare const _SelectInternal: SelectComponent; +interface SelectComponent { + (options: SelectOptions): SelectElement; + (props: SelectProps): ChatElement; +} +declare const _SelectOptionInternal: SelectOptionComponent; +interface SelectOptionComponent { + (options: { label: string; value: string; description?: string }): SelectOptionElement; + (props: SelectOptionProps): ChatElement; +} +declare const _TableInternal: typeof _Table; +declare const _TextInputInternal: TextInputComponent; +interface TextInputComponent { + (options: TextInputOptions): TextInputElement; + (props: TextInputProps): ChatElement; +} +declare const _cardChildToFallbackText: typeof _cardChildToFallbackTextInternal; +declare function _cardChildToFallbackTextInternal(child: CardChild): string | null; +declare const _isCardElement: typeof _isCardElementInternal; +declare function _isCardElementInternal(value: unknown): value is CardElement; +interface FileUpload { + data: Buffer | Blob | ArrayBuffer; + filename: string; + mimeType?: string; +} +interface PostableCard { + aiGenerated?: boolean; + card: CardElement; + fallbackText?: string; + files?: FileUpload[]; +} + +export type { + ActionsElement, + ButtonElement, + ButtonOptions, + ButtonProps, + ButtonStyle, + CardChild, + CardElement, + CardJSXElement, + CardJSXProps, + CardLinkProps, + CardOptions, + CardProps, + ChatElement, + ContainerProps, + DividerElement, + DividerProps, + ExternalSelectElement, + ExternalSelectOptions, + ExternalSelectProps, + FieldElement, + FieldProps, + FieldsElement, + FileUpload, + ImageElement, + ImageProps, + LinkButtonElement, + LinkButtonOptions, + LinkButtonProps, + LinkElement, + ModalChild, + ModalElement, + ModalOptions, + ModalProps, + PostableCard, + RadioSelectElement, + RadioSelectOptions, + SectionElement, + SelectElement, + SelectOptionElement, + SelectOptionProps, + SelectOptions, + SelectProps, + TableAlignment, + TableElement, + TableOptions, + TextElement, + TextInputElement, + TextInputOptions, + TextInputProps, + TextProps, + TextStyle, +}; +export const Actions: typeof _Actions = chat.Actions as typeof _Actions; +export const Button: typeof _ButtonInternal = chat.Button as typeof _ButtonInternal; +export const Card: typeof _CardInternal = chat.Card as typeof _CardInternal; +export const CardLink: typeof _CardLinkInternal = chat.CardLink as typeof _CardLinkInternal; +export const CardText: typeof _CardText = chat.CardText as typeof _CardText; +export const Divider: typeof _DividerInternal = chat.Divider as typeof _DividerInternal; +export const ExternalSelect: typeof _ExternalSelectInternal = + chat.ExternalSelect as typeof _ExternalSelectInternal; +export const Field: typeof _FieldInternal = chat.Field as typeof _FieldInternal; +export const Fields: typeof _FieldsInternal = chat.Fields as typeof _FieldsInternal; +export const Image: typeof _ImageInternal = chat.Image as typeof _ImageInternal; +export const LinkButton: typeof _LinkButtonInternal = chat.LinkButton as typeof _LinkButtonInternal; +export const Modal: typeof _ModalInternal = chat.Modal as typeof _ModalInternal; +export const RadioSelect: typeof _RadioSelectInternal = + chat.RadioSelect as typeof _RadioSelectInternal; +export const Section: typeof _SectionInternal = chat.Section as typeof _SectionInternal; +export const Select: typeof _SelectInternal = chat.Select as typeof _SelectInternal; +export const SelectOption: typeof _SelectOptionInternal = + chat.SelectOption as typeof _SelectOptionInternal; +export const Table: typeof _TableInternal = chat.Table as typeof _TableInternal; +export const TextInput: typeof _TextInputInternal = chat.TextInput as typeof _TextInputInternal; +export const cardChildToFallbackText: typeof _cardChildToFallbackText = + chat.cardChildToFallbackText as typeof _cardChildToFallbackText; +export const isCardElement: typeof _isCardElement = chat.isCardElement as typeof _isCardElement; diff --git a/packages/eve/src/public/channels/slack/index.ts b/packages/eve/src/public/channels/slack/index.ts index e5b8d2c002..4bac789ed0 100644 --- a/packages/eve/src/public/channels/slack/index.ts +++ b/packages/eve/src/public/channels/slack/index.ts @@ -105,8 +105,8 @@ export { } from "#public/channels/slack/hitl.js"; /** - * Card builders and element types re-exported from the vendored chat - * SDK module. These are pure data factories. They return plain typed + * Card builders and eve-owned element types. The vendored factories remain an + * implementation detail. These are pure data factories. They return plain typed * objects that the channel passes to {@link cardToBlocks} at post time. * The chat SDK runtime (`Chat`, `Thread`, `Adapter`, etc.) is not * imported and not reachable through this entry point. @@ -132,12 +132,10 @@ export { TextInput, cardChildToFallbackText, isCardElement, -} from "#compiled/chat/index.js"; +} from "#public/channels/slack/cards.js"; export type { ActionsElement, - AdapterPostableMessage, - Attachment, ButtonElement, ButtonOptions, ButtonProps, @@ -188,4 +186,4 @@ export type { TextInputProps, TextProps, TextStyle, -} from "#compiled/chat/index.js"; +} from "#public/channels/slack/cards.js"; diff --git a/packages/eve/src/public/channels/slack/slackChannel.ts b/packages/eve/src/public/channels/slack/slackChannel.ts index a4fde8ba80..8abd3ca3fa 100644 --- a/packages/eve/src/public/channels/slack/slackChannel.ts +++ b/packages/eve/src/public/channels/slack/slackChannel.ts @@ -18,7 +18,7 @@ import type { Delivery } from "#shared/delivery.js"; import { setChannelBuildMetadata } from "#channel/compiled-channel.js"; import type { SessionAuthContext, TurnPolicy } from "#channel/types.js"; import type { VercelConnectMetadata } from "#shared/vercel-connect-metadata.js"; -import type { CardElement } from "#compiled/chat/index.js"; +import type { CardElement } from "#public/channels/slack/cards.js"; import type { SessionContext } from "#public/definitions/callback-context.js"; import type { ChannelContinuationOps } from "#public/definitions/channel.js"; diff --git a/packages/eve/src/public/channels/twilio/verify.ts b/packages/eve/src/public/channels/twilio/verify.ts index 5329b0120b..47e6385738 100644 --- a/packages/eve/src/public/channels/twilio/verify.ts +++ b/packages/eve/src/public/channels/twilio/verify.ts @@ -1,22 +1,21 @@ import { createHmac } from "node:crypto"; -import { - resolveTwilioCredential, - TwilioApiError, - type TwilioCredential, -} from "#compiled/@chat-adapter/twilio/api.js"; +import { resolveTwilioCredential, TwilioApiError } from "#compiled/@chat-adapter/twilio/api.js"; import { twilioSignatureBase, verifyTwilioRequest as verifyPrimitive, TwilioWebhookVerificationError, - type TwilioVerifiedRequest, - type TwilioWebhookUrl, } from "#compiled/@chat-adapter/twilio/webhook.js"; /** Twilio auth token, materialized directly or from an async secret provider. */ -export type TwilioAuthToken = TwilioCredential; +export type TwilioAuthToken = string | (() => string | Promise); + +export type TwilioWebhookUrl = string | ((request: Request) => string | Promise); -export type { TwilioVerifiedRequest, TwilioWebhookUrl }; +export interface TwilioVerifiedRequest { + readonly body: string; + readonly params: URLSearchParams; +} /** Options for verifying Twilio inbound webhooks. */ export interface TwilioVerifyOptions { diff --git a/packages/eve/src/public/next/index.integration.test.ts b/packages/eve/src/public/next/index.integration.test.ts index 19c55d620e..ec70882d37 100644 --- a/packages/eve/src/public/next/index.integration.test.ts +++ b/packages/eve/src/public/next/index.integration.test.ts @@ -29,7 +29,12 @@ vi.mock("node:child_process", () => ({ spawn: spawnMock, })); -import { withEve, type EveNextConfig, type EveNextRewriteSections } from "./index.js"; +import { withEve } from "./index.js"; +import type { NextConfig as EveNextConfig } from "next"; +type EveNextRewriteSections = Extract< + Awaited>>, + { beforeFiles?: unknown; afterFiles?: unknown; fallback?: unknown } +>; interface TestConfig extends EveNextConfig { readonly basePath?: string; diff --git a/packages/eve/src/public/next/index.test.ts b/packages/eve/src/public/next/index.test.ts index aa6ee31852..27cd7a6472 100644 --- a/packages/eve/src/public/next/index.test.ts +++ b/packages/eve/src/public/next/index.test.ts @@ -45,7 +45,12 @@ vi.mock("#internal/nitro/host/workspace-extensions.js", () => ({ const { buildWorkspaceExtensions } = await import("#internal/nitro/host/workspace-extensions.js"); -import { withEve, type EveNextConfig, type EveNextRewriteSections } from "./index.js"; +import { withEve } from "./index.js"; +import type { NextConfig as EveNextConfig } from "next"; +type EveNextRewriteSections = Extract< + Awaited>>, + { beforeFiles?: unknown; afterFiles?: unknown; fallback?: unknown } +>; const EVE_NEXT_SERVICE_PREFIX = "/_eve_internal/eve"; diff --git a/packages/eve/src/public/next/index.ts b/packages/eve/src/public/next/index.ts index 1ec462de72..ce4b22a17c 100644 --- a/packages/eve/src/public/next/index.ts +++ b/packages/eve/src/public/next/index.ts @@ -35,18 +35,18 @@ type NextRewrites = Awaited>>; /** * Next.js rewrite rule that {@link withEve} emits. */ -export type EveNextRewriteRule = ArrayElement; +type EveNextRewriteRule = ArrayElement; /** * Resolved return type of a Next.js `rewrites` function: an array of rules, or * the sectioned `{ beforeFiles, afterFiles, fallback }` object. */ -export type EveNextRewrites = NextRewrites; +type EveNextRewrites = NextRewrites; /** * Sectioned Next.js rewrite rules. */ -export type EveNextRewriteSections = Extract< +type EveNextRewriteSections = Extract< NextRewrites, { readonly afterFiles?: EveNextRewriteRule[]; @@ -57,16 +57,16 @@ export type EveNextRewriteSections = Extract< /** * Alias of Next.js's `NextConfig`, the config object form {@link withEve} - * accepts (the other being {@link EveNextConfigFunction}). + * accepts (the other being {@link WithEveConfigFunction}). */ -export type EveNextConfig = NextConfig; +type EveNextConfig = NextConfig; /** * Structural shape of a Next.js config function: receives the build `phase` and * a `context` containing `defaultConfig`, and returns a config (or a promise of * one). This is the form {@link withEve} returns. */ -export type EveNextConfigFunction = ( +export type WithEveConfigFunction = ( phase: string, context: { readonly defaultConfig: TConfig; @@ -76,8 +76,8 @@ export type EveNextConfigFunction /** * Next.js config input that {@link withEve} accepts. */ -export type EveNextConfigInput = - | EveNextConfigFunction +export type WithEveConfigInput = + | WithEveConfigFunction | TConfig; /** @@ -299,7 +299,7 @@ function isRewriteSections(rewrites: EveNextRewrites): rewrites is EveNextRewrit } async function resolveNextConfig( - configOrFunction: EveNextConfigInput, + configOrFunction: WithEveConfigInput, phase: string, context: { readonly defaultConfig: TConfig; @@ -431,9 +431,9 @@ async function buildAgentWorkspaceExtensions( * to the origin serving the eve service namespace. */ export function withEve( - configOrFunction: EveNextConfigInput, + configOrFunction: WithEveConfigInput, options: WithEveOptions = {}, -): EveNextConfigFunction { +): WithEveConfigFunction { const nextRoot = process.cwd(); const devServerTimeoutMs = resolveDevServerTimeout(options.devServerTimeoutMs, EVE_NEXT_HOST); assertValidWithEveOptions(options); diff --git a/packages/eve/src/public/sandbox/vercel-sandbox.ts b/packages/eve/src/public/sandbox/vercel-sandbox.ts index d9a83246b2..e0a3c554a8 100644 --- a/packages/eve/src/public/sandbox/vercel-sandbox.ts +++ b/packages/eve/src/public/sandbox/vercel-sandbox.ts @@ -1,17 +1,5 @@ -import type * as Vercel from "#compiled/@vercel/sandbox/index.js"; import type { SandboxNetworkOptions } from "#shared/sandbox-network-policy.js"; -type VercelCreateOptions = NonNullable[0]>; - -type VercelSandboxInternalCreateOptions = { - readonly [key: `__${string}`]: unknown; -}; - -type VercelSandboxAuthorCreateOptions = T extends unknown - ? Omit & - VercelSandboxInternalCreateOptions - : never; - /** * Options accepted by Vercel sandbox environment constructors. Forwarded to Vercel * Sandbox creation for every fresh sandbox the framework creates @@ -47,16 +35,54 @@ type VercelSandboxAuthorCreateOptions = T extends unknown * snapshot, force a template rebuild (e.g. by changing the sandbox * definition so its template key changes). */ -export type VercelSandboxCreateOptions = VercelSandboxAuthorCreateOptions; +export type VercelSandboxCreateOptions = SandboxNetworkOptions & { + readonly [key: `__${string}`]: unknown; + readonly token?: string; + readonly teamId?: string; + readonly projectId?: string; + readonly fetch?: typeof fetch; + readonly ports?: number[]; + readonly timeout?: number; + readonly resources?: { vcpus: number }; + readonly env?: Record; + readonly tags?: Record; + readonly region?: string; + readonly failoverRegions?: string[]; + readonly snapshotExpiration?: number; + readonly keepLastSnapshots?: { count: number; expiration?: number; deleteEvicted?: boolean }; +} & ( + | { + image?: string; + source?: + | { + type: "git"; + url: string; + username?: never; + password?: never; + depth?: number; + revision?: string; + } + | { + type: "git"; + url: string; + username: string; + password: string; + depth?: number; + revision?: string; + } + | { type: "tarball"; url: string }; + } + | { image?: never; source: { type: "snapshot"; snapshotId: string } } + ); /** Access mode for a Drive mounted into a Vercel Sandbox. */ -export type VercelSandboxMountMode = Vercel.SandboxMountMode; +export type VercelSandboxMountMode = "read-write" | "snapshot"; /** A Drive mounted at one absolute path in a Vercel Sandbox. */ -export type VercelSandboxMount = Vercel.SandboxMounts[string]; +export type VercelSandboxMount = { readonly drive: string; readonly mode: VercelSandboxMountMode }; /** Drive mounts keyed by absolute sandbox path. */ -export type VercelSandboxMounts = Vercel.SandboxMounts; +export type VercelSandboxMounts = Record; /** Options resolved when eve creates a fresh live session sandbox. */ export interface VercelSandboxMountOptions { diff --git a/packages/eve/src/public/sandbox/vercel.test.ts b/packages/eve/src/public/sandbox/vercel.test.ts index c73432936e..6f5ce7fddf 100644 --- a/packages/eve/src/public/sandbox/vercel.test.ts +++ b/packages/eve/src/public/sandbox/vercel.test.ts @@ -1,13 +1,12 @@ import { describe, expect, expectTypeOf, it } from "vitest"; import type { SessionContext } from "#context/session-context.js"; import type { RuntimeSandboxSession } from "#public/definitions/sandbox.js"; -import { Drive, VercelSandbox, type VercelSandboxSession } from "#public/sandbox/vercel.js"; +import { VercelSandbox, type VercelSandboxSession } from "#public/sandbox/vercel.js"; describe("VercelSandbox", () => { it("creates environments", () => { const environment = VercelSandbox.environment({ resources: { vcpus: 4 } }); expect(environment.provider).toBe("vercel"); expectTypeOf(environment.open).toBeFunction(); - expectTypeOf(Drive.getOrCreate).toBeFunction(); }); it("preserves its session capabilities through getSandbox", () => { diff --git a/packages/eve/src/public/sandbox/vercel.ts b/packages/eve/src/public/sandbox/vercel.ts index 7c9aee6e2b..11eaa15b8c 100644 --- a/packages/eve/src/public/sandbox/vercel.ts +++ b/packages/eve/src/public/sandbox/vercel.ts @@ -14,7 +14,6 @@ export { type VercelSandboxEnvironmentOptions, type VercelSandboxRuntimeOptions, } from "#sandbox/providers/vercel.js"; -export { Drive } from "#compiled/@vercel/sandbox/index.js"; export type { VercelSandboxMount, VercelSandboxMountMode, diff --git a/packages/eve/src/public/workflow-modules.d.ts b/packages/eve/src/public/workflow-modules.d.ts index 82d4d40438..dc97f9ef09 100644 --- a/packages/eve/src/public/workflow-modules.d.ts +++ b/packages/eve/src/public/workflow-modules.d.ts @@ -3,9 +3,15 @@ * so an installed `workflow` package takes precedence. */ declare module "workflow" { - export * from "#internal/workflow/index.js"; + export { + createHook, + createWebhook, + sleep, + FatalError, + getWorkflowMetadata, + } from "#internal/workflow/index.js"; } declare module "workflow/api" { - export * from "#internal/workflow/runtime.js"; + export { start, getRun, resumeHook, getHookByToken } from "#internal/workflow/runtime.js"; } diff --git a/packages/eve/src/shared/agent-definition.ts b/packages/eve/src/shared/agent-definition.ts index f1abfab8c0..198504b55b 100644 --- a/packages/eve/src/shared/agent-definition.ts +++ b/packages/eve/src/shared/agent-definition.ts @@ -1,4 +1,4 @@ -import type { CallSettings, LanguageModel } from "ai"; +import type { LanguageModel } from "ai"; import type { JsonObject } from "#shared/json.js"; import type { ModuleSourceRef } from "#shared/source-ref.js"; import { @@ -47,7 +47,14 @@ export const ANTHROPIC_PROMPT_CACHE_TTLS: readonly AnthropicPromptCacheTtl[] = [ * Provider-agnostic reasoning effort forwarded to the AI SDK model call. * The AI SDK's `max` level is not supported yet: eve's manifest and agent info list the levels. */ -export type AgentReasoningDefinition = Exclude, "max">; +export type AgentReasoningDefinition = + | "provider-default" + | "none" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh"; /** * How an agent's model is reached at runtime, decided at compile time from the diff --git a/packages/eve/src/shared/sandbox-network-policy.ts b/packages/eve/src/shared/sandbox-network-policy.ts index 548b4ebff6..d5e4e1cfc9 100644 --- a/packages/eve/src/shared/sandbox-network-policy.ts +++ b/packages/eve/src/shared/sandbox-network-policy.ts @@ -1,9 +1,7 @@ -import type * as Vercel from "#compiled/@vercel/sandbox/index.js"; - /** * Firewall network policy applied to a live sandbox session. * - * eve-owned alias of the provider network-policy shape. Use it to restrict + * eve-owned firewall policy. Use it to restrict * egress (`"deny-all"`, an allow-list) or to broker credentials onto * outgoing requests. A per-domain `transform` injects headers at the * firewall so secrets never enter the sandbox process: @@ -24,7 +22,64 @@ import type * as Vercel from "#compiled/@vercel/sandbox/index.js"; * `"deny-all"` policies. just-bash environments do not expose * `setNetworkPolicy` because their network policy is fixed at sandbox creation. */ -export type SandboxNetworkPolicy = Vercel.NetworkPolicy; + +type NetworkTransformer = { + headers?: Record; +}; + +type NetworkPolicyMatcher = + | { + exact?: string; + } + | { + startsWith?: string; + } + | { + regex?: string; + }; + +type NetworkPolicyKeyValueMatcher = { + key?: NetworkPolicyMatcher; + + value?: NetworkPolicyMatcher; +}; + +type NetworkPolicyMatch = { + path?: NetworkPolicyMatcher; + + method?: string[]; + + queryString?: NetworkPolicyKeyValueMatcher[]; + + headers?: NetworkPolicyKeyValueMatcher[]; +}; + +type NetworkPolicyRule = { + match?: NetworkPolicyMatch; +} & ( + | { + transform: NetworkTransformer[]; + forwardURL?: never; + } + | { + transform?: never; + + forwardURL: string; + } +); + +export type SandboxNetworkPolicy = + | "allow-all" + | "deny-all" + | { + allow?: string[] | Record; + + subnets?: { + allow?: string[]; + + deny?: string[]; + }; + }; /** Initial network configuration shared by network-capable sandbox providers. */ export interface SandboxNetworkOptions { diff --git a/packages/eve/src/tools/approval/policies.test.ts b/packages/eve/src/tools/approval/policies.test.ts index 9b8590fff7..ea097564c0 100644 --- a/packages/eve/src/tools/approval/policies.test.ts +++ b/packages/eve/src/tools/approval/policies.test.ts @@ -50,7 +50,7 @@ describe("dynamic tool approval helpers", () => { await expect(auto()(context)).resolves.toBe(expected); expect(decide).toHaveBeenCalledWith( expect.objectContaining({ - abortSignal: expect.any(AbortSignal), + signal: expect.any(AbortSignal), maxRetries: 0, model: "typesafe-ai/jev", state: { action: { input: context.toolInput, tool: "deploy" } }, diff --git a/packages/eve/src/tools/approval/policies.ts b/packages/eve/src/tools/approval/policies.ts index 8e6d7414ca..1e2878c3b0 100644 --- a/packages/eve/src/tools/approval/policies.ts +++ b/packages/eve/src/tools/approval/policies.ts @@ -1,4 +1,4 @@ -import type { Experimental_DecisionModel as DecisionModel } from "ai"; +import type { DecisionModel } from "#ai/decision-types.js"; import type { ApprovalContext, ApprovalPolicy } from "#approval/definition.js"; import { decide } from "#ai/decide.js"; @@ -54,7 +54,7 @@ async function autoApproval( }, }, }, - abortSignal, + signal: abortSignal, maxRetries: 0, }); return result.answers.permission.choice === "clear" ? "approved" : "user-approval"; diff --git a/packages/eve/src/tracing/eve/otel-declaration.ts b/packages/eve/src/tracing/eve/otel-declaration.ts index 7cdc442989..d1942e6449 100644 --- a/packages/eve/src/tracing/eve/otel-declaration.ts +++ b/packages/eve/src/tracing/eve/otel-declaration.ts @@ -1,11 +1,4 @@ -import type { - MetricReader, - PropagatorOrName, - SamplerOrName, - SpanExporter, - SpanProcessor, - SpanProcessorOrName, -} from "#compiled/@vercel/otel/index.js"; +import type { MetricReader, SpanExporter, SpanProcessor } from "#compiled/@vercel/otel/index.js"; import { PROVIDER, type InstrumentationProvider } from "#instrumentation/provider.js"; import type { InstrumentationModelStartedInput } from "#public/instrumentation/index.js"; @@ -16,6 +9,25 @@ import { vercelRuntimeSpanProcessor } from "#tracing/eve/vercel-runtime-span-exp import type { OtelConfiguration } from "#tracing/eve/otel-configuration.js"; import { createLogger, formatError } from "#internal/logging.js"; const log = createLogger("tracing.export"); +type PropagatorSetting = + | { inject(...args: never[]): void; extract(...args: never[]): unknown; fields(): string[] } + | "auto" + | "none" + | "tracecontext" + | "baggage"; + +/** A `Sampler`, or one of the names `@vercel/otel` resolves for you. */ +type SamplerSetting = + | { shouldSample(...args: never[]): unknown; toString(): string } + | "auto" + | "always_off" + | "always_on" + | "parentbased_always_off" + | "parentbased_always_on" + | "parentbased_traceidratio" + | "traceidratio"; + +type SpanProcessorSetting = SpanProcessor | "auto"; import type { TraceCapturePolicy } from "#shared/trace-policy.js"; export type { TraceCaptureContext, @@ -87,9 +99,9 @@ export interface OtelOptions { * along with your exporters. To thin one backend only, drop spans in a * processor. */ - readonly sampler?: SamplerOrName; + readonly sampler?: SamplerSetting; /** Composed into one propagator. All inject; the first to extract wins. Defaults to `auto`. */ - readonly propagators?: readonly PropagatorOrName[]; + readonly propagators?: readonly PropagatorSetting[]; /** * OpenTelemetry `Instrumentation` instances passed through to * `registerOTel` before the server entry loads. Disabled by default. @@ -148,7 +160,7 @@ export interface OtelIntegration extends InstrumentationProvider { readonly [OTEL_INTEGRATION]: true; readonly metricReaders: readonly MetricReader[]; readonly runtimeContext?: (input: InstrumentationModelStartedInput) => JsonObject | undefined; - readonly spanProcessors: readonly SpanProcessorOrName[]; + readonly spanProcessors: readonly SpanProcessorSetting[]; } /** @@ -284,7 +296,7 @@ export interface CollectedOtel { * @internal */ export function collectOtelPipeline(values: readonly unknown[]): CollectedOtel { - const spanProcessors: SpanProcessorOrName[] = []; + const spanProcessors: SpanProcessorSetting[] = []; const metricReaders: MetricReader[] = []; const runtimeContextResolvers: RuntimeContextResolver[] = []; let declaration: OtelDeclaration | undefined; diff --git a/research/jev-decision-models.md b/research/jev-decision-models.md index 4a0d32e81b..824fccdbfc 100644 --- a/research/jev-decision-models.md +++ b/research/jev-decision-models.md @@ -1,7 +1,7 @@ --- issue: "TBD (maintainer-requested research; no matching issue found)" status: implemented -last_updated: "2026-10-06" +last_updated: "2026-10-17" --- # Decision model routing in eve @@ -10,9 +10,9 @@ last_updated: "2026-10-06" Expose automatic model selection as `auto` from `eve/models` and standalone `decide` from `eve/ai`. Build both on AI SDK's `experimental_decide` API. -`decide` accepts the SDK's state, typed questions, and request options, with an -optional model defaulting to `typesafe-ai/jev`. It returns the SDK's typed answers -and response metadata. +`decide` accepts eve-owned state, typed questions, `maxRetries`, and `signal`, with an +optional model defaulting to `typesafe-ai/jev`. It returns typed `answers` and normalized `usage`. +The pre-1.0 cleanup owns these public shapes rather than exposing experimental SDK signatures. `auto` calls the shared `decide` wrapper. Model strings use the configured AI SDK default provider; without an override, the wrapper resolves eve's local @@ -164,8 +164,9 @@ direct provider behavior. ## Scope boundaries eve owns the `decide` wrapper and its default model and authentication behavior. -AI SDK owns decision schemas, validation, retries, errors, and result metadata. -The integration adds no TypeSafe-specific `decide` function, direct HTTP client, +AI SDK owns runtime validation, retries, errors, and provider result metadata. eve owns its +public question, state, answer, options, and result types; eval diagnostics retain SDK metadata +internally. The integration adds no TypeSafe-specific `decide` function, direct HTTP client, provider fallback, confidence threshold, or built-in agent-callable decision tool. No live Jev inference or independent quality, latency, or cost benchmark was run