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/eve-events-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

`eve/events` explicitly exports the v27 catalog, lifecycle tables, guards, selectors, and whole-line `foldEvents`; writer encoders, retention options, and extra folding helpers stay internal. Call labels use `label`, controls require delivery identities, and child deliveries retain per-message caller attribution while unnamed remote streams keep their call-order mapping.
5 changes: 5 additions & 0 deletions .changeset/local-tool-execution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Local tool calls execute after the model response ends, through the same approval policy, input validation, and telemetry as approved calls. Cancelling a task also stops a run that ignores cancellation, and ending a session stops the workflow tools its turn was waiting on.
5 changes: 5 additions & 0 deletions .changeset/message-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Sends, channel delivery, and schedules use eve-owned `MessageContent` instead of AI SDK `UserContent`; images are file parts, and Chat SDK uses `messageContentOf` instead of `messageToUserContent`. Workflow `ctx.agent().send()` also accepts text/file parts, but file data must be an HTTP(S) URL rather than inline bytes or base64.
5 changes: 5 additions & 0 deletions .changeset/observable-session-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Frontend snapshots expose `view`, `previews`, `messages`, and `agents` directly, with `idle | busy | resuming` status and separate errors; all lifecycle callbacks remain. Tool parts use the shared call `state`, optimistic answers last only through POST acknowledgement, and Stop still targets the observed or pending submission’s turn without withdrawing queued messages.
5 changes: 5 additions & 0 deletions .changeset/rebuild-extensions-v27.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Rebuild hook, channel, and schedule extensions for the v27 event contract, and rebuild tool extensions that require capability epoch 88 because `toolResultFrom` now accepts a settled call row instead of a v26 action result. Other previously supported tool, dynamic tool, connection, dynamic skill, dynamic instruction, and subagent epochs remain supported.
5 changes: 5 additions & 0 deletions .changeset/sign-in-privacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Built-in channels keep sign-in links and codes in private responses, or show a link-free status when a private response is unavailable; Teams shared cards reveal credentials only to the requester. Telegram's Authorize button opens the latest unresolved sign-in instead of a completed one.
5 changes: 5 additions & 0 deletions .changeset/usage-from-facts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Agent usage from workflow tool calls counts toward session token limits and `$eve.*` token tags even when it arrives after the turn stopped waiting on the call.
5 changes: 5 additions & 0 deletions .changeset/v27-channel-renderers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Slack's `renderers.taskCard` receives a `TaskCard` containing call rows and the session view instead of `TaskCardView`; its task, action, blocker, and status types are removed, and `callState`/`callLabel` from `eve/events` provide rendering state. Teams channel state replaces `pendingAuthActivityId` with `pendingAuthActivityIds`, one activity per connection.
5 changes: 5 additions & 0 deletions .changeset/v27-client-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Client `data` is now the conversation, with native `tool` and `interaction` parts instead of AI SDK `dynamic-tool` states; `toolResultFrom` accepts a settled call row. Custom reducers and the `EveAgentReducer`, `conversationReducer`, `defaultMessageReducer`, and `toolCallState` exports are removed: derive state from `view` or `events`, read saved events with `conversationOf(events)`, and replace `transcriptReducer(options)` with `transcriptOf(events, options)`.
5 changes: 5 additions & 0 deletions .changeset/v27-dev-event-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

`eve dev` validates each session line against the event contract before writing it, with an 8 MiB limit per line. Set `EVE_CHECK_SESSION_EVENTS=0` to disable validation or `=1` to enable it outside development.
5 changes: 5 additions & 0 deletions .changeset/v27-dynamic-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

`defineDynamic` handlers retain their keys but receive v27 facts: `session.started` and `turn.started` receive those facts, and `step.started` receives `model.requested` instead of the previous `sequence`, `stepIndex`, and `modelId` payload. Dynamic skill, tool, and model resolvers now reject handlers for events their slots cannot receive at build time.
5 changes: 5 additions & 0 deletions .changeset/v27-evals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

`eve/evals` adds `toolCallsOf(events)`, `turn.waitForToolCall(name)`, and event matchers for a fact's `scope`. `t.target.watchTurn(sessionId, { until })` can end a read without a turn boundary, and `session.compact()` returns when its context change settles.
5 changes: 5 additions & 0 deletions .changeset/v27-hook-and-channel-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Hooks and channel handlers receive v27 facts as `(event, ctx)`, with coordinates in `event.scope`, position and session tables in `ctx.position` and `ctx.view`, and channel-specific context in `ctx.channel` instead of `ctx.scope`. Channels can render once per committed line with `committed(line, ctx)`; removed v26 handler keys are rejected, and wildcard hooks no longer receive progress records.
5 changes: 5 additions & 0 deletions .changeset/v27-interactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Approvals, questions, sign-ins, and budget prompts share a nested `Interaction` projection and answers use `{ interactionId, optionId?, text? }`; approval and budget choices are `approve` / `deny`, and tool answers expose a minimized `Principal`. Typed replies answer only the displayed prompt, channels receive `onAnswers`, and CLI/MCP JSON projections omit private sign-in callback URLs.
5 changes: 5 additions & 0 deletions .changeset/v27-remote-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Remote agents speak protocol 3, and `child.opened` replaces `agent.started`. Protocol 1 and 2 callers still receive results, but turns requiring relayed approvals, questions, or sign-ins fail with an upgrade message; the eval `remoteUrl` field and matcher are removed.
5 changes: 5 additions & 0 deletions .changeset/v27-session-streams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Session streams now use v27 committed-fact and progress lines, with line-position cursors and event identities instead of v26 metadata; `streamControlVersion` and `streamIdleReconnectPolicy` are removed, and terminal streams emit `{"$eve":"stream.ended"}`. Checkpoints move to version 17, so sessions with an older checkpoint remain on their owning deployment instead of handing off to a v27 runtime.
5 changes: 5 additions & 0 deletions .changeset/v27-turns-wait-on-people.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

`turn.paused` now lists person-facing interactions in `awaiting`; turns waiting only on tasks keep running, and `turn.resumed` is removed. Text produced while waiting on tasks is narration rather than a final reply, and `eve invoke` ends a run that needs a person with an `input-required` status.
5 changes: 5 additions & 0 deletions .changeset/web-chat-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Web Chat renders reasoning and tool calls with Trace, places steering messages where the turn consumed them, and restores answered approvals without offering their buttons again. The chat, voice, and personal-agent templates follow the native session lifecycle and persist committed state rather than treating send acceptance as completion.
26 changes: 14 additions & 12 deletions apps/frameworks/tanstack/src/components/EveAgentConsole.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEveAgent, type EveDynamicToolPart, type EveMessagePart } from "eve/react";
import { useEveAgent, type EveToolPart, type EveMessagePart } from "eve/react";
import { useState } from "react";

function ToolBlock({
Expand All @@ -8,26 +8,26 @@ function ToolBlock({
}: Readonly<{
canRespond: boolean;
onRespond: (requestId: string, optionId: string) => void;
part: EveDynamicToolPart;
part: EveToolPart;
}>) {
const inputRequest = part.toolMetadata?.eve?.inputRequest;
const awaitingResponse =
inputRequest !== undefined && part.toolMetadata?.eve?.inputResponse === undefined;
const interaction = part.interaction;
const inputRequest = interaction?.request;
const awaitingResponse = interaction?.status === "open";

return (
<div className="tool">
<strong>{part.toolMetadata?.eve?.name ?? part.toolName}</strong> <em>{part.state}</em>
<strong>{part.label}</strong> <em>{part.outcome ?? part.status}</em>
<pre>{JSON.stringify(part.input, null, 2)}</pre>
{part.output !== undefined ? <pre>{JSON.stringify(part.output, null, 2)}</pre> : null}
{part.errorText ? <p className="error">{part.errorText}</p> : null}
{awaitingResponse ? (
{part.error ? <p className="error">{part.error.message}</p> : null}
{awaitingResponse && inputRequest !== undefined ? (
<div>
<p>{inputRequest.prompt}</p>
{(inputRequest.options ?? []).map((option) => (
<button
disabled={!canRespond}
key={option.id}
onClick={() => onRespond(inputRequest.requestId, option.id)}
onClick={() => onRespond(inputRequest.interactionId, option.id)}
type="button"
>
{option.label}
Expand All @@ -53,7 +53,7 @@ function PartView({
return <div>{part.text}</div>;
case "reasoning":
return <div className="reasoning">{part.text}</div>;
case "dynamic-tool":
case "tool":
return <ToolBlock canRespond={canRespond} onRespond={onRespond} part={part} />;
default:
return null;
Expand Down Expand Up @@ -91,8 +91,10 @@ export function EveAgentConsole() {
{message.parts.map((part, index) => (
<PartView
canRespond={!isInputDisabled}
key={part.type === "dynamic-tool" ? part.toolCallId : `${part.type}:${index}`}
onRespond={(requestId, optionId) => void agent.respond([{ optionId, requestId }])}
key={part.type === "tool" ? part.callId : `${part.type}:${index}`}
onRespond={(interactionId, optionId) =>
void agent.respond([{ interactionId, optionId }])
}
part={part}
/>
))}
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,9 @@ export default defineHook({

The remaining subscribers for the event still run. Then eve cancels the turn the same way [`session.cancel()`](./client/streaming) does: in-flight model and tool work is aborted, delegated child turns are cancelled, and the turn settles with `outcome: "cancelled"` and `cause: { hook }` naming the hook. No failure is recorded. A cancel from `turn.started` or `model.requested` takes effect before that model call. In a conversation, the next message starts a new turn. A delegated task reports the cancellation to its caller.

A plain-text message that answers a pending question, such as one from `ask_question`, is recorded as `message.received` before eve delivers it. Calling `ctx.cancel()` from that event's hook cancels the turn instead: the question resolves as `cancelled`, and the waiting tool call never receives the text.
A plain-text message that answers a pending question, such as one from `ask_question`, is recorded as `delivery.consumed` before eve delivers it. Calling `ctx.cancel()` from that event's hook cancels the turn instead: the question settles as `interrupted`, and the waiting tool call never receives the text.

The same goes for the `action.partial` a workflow tool reports while the turn waits on it: `ctx.cancel()` from its hook cancels the turn, and the run's result never reaches the model.
The same goes for the `call.progress` a workflow tool reports while the turn waits on it: `ctx.cancel()` from its hook cancels the turn, and the run's result never reaches the model.

`ctx.cancel()` returns `void` rather than a promise. The turn stops after the hook returns, so there is nothing to await. Call it before the handler's promise settles: eve ignores a call from work the handler does not await and logs a warning.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { waitForStore } from "@eve-e2e/config/frontend";
import { EveAgentStore } from "eve/client";
import { defineEval } from "eve/evals";
import { equals } from "eve/evals/expect";
Expand All @@ -13,6 +14,7 @@ export default defineEval({
resolveReconnectAuthorization = resolve;
});
let disconnectStream: (() => void) | undefined;
let awaitingForcedReconnect = false;
const store = new EveAgentStore({
host,
initialSession: session.state,
Expand All @@ -21,37 +23,51 @@ export default defineEval({
globalThis.fetch = async (input, init) => {
const url = new URL(input instanceof Request ? input.url : String(input));
if (url.origin !== host) return originalFetch(input, init);
const forcedReconnect = awaitingForcedReconnect;
const response = await t.target.fetch(`${url.pathname}${url.search}`, init);
if (!url.pathname.endsWith("/stream") || !response.ok || response.body === null) {
return response;
}
if (disconnectStream !== undefined) {
if (forcedReconnect) {
resolveReconnectAuthorization(new Headers(init?.headers).get("authorization"));
return response;
}
const transport = new TransformStream<Uint8Array, Uint8Array>({
start(controller) {
disconnectStream = () => controller.terminate();
disconnectStream = () => {
awaitingForcedReconnect = true;
controller.terminate();
};
},
});
return new Response(response.body.pipeThrough(transport), response);
};

try {
await store.send({
const first = await store.send({
message: "Reply with first.",
headers: { authorization: "Bearer old" },
signal: t.signal,
});
await waitForStore(
store,
(snapshot) => snapshot.view.deliveries[first.deliveryId]?.status === "settled",
t.signal,
);
await t.require(store.snapshot.error, equals(undefined));
const firstTurn = await t.target.watchTurn(session.sessionId).result();
firstTurn.expectOk();

await store.send({
const second = await store.send({
message: "Reply with second.",
headers: { authorization: "Bearer fresh" },
signal: t.signal,
});
await waitForStore(
store,
(snapshot) => snapshot.view.deliveries[second.deliveryId]?.status === "settled",
t.signal,
);
await t.require(store.snapshot.error, equals(undefined));

if (disconnectStream === undefined)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default defineEval({
: {};
const turn = await t.send(message, { headers });
turn.expectOk();
turn.event("message.completed", { count: 1 });
turn.event("content.completed", { count: 1, data: { kind: "text", phase: "reply" } });
const sessionPath = `/eve/v1/session/${encodeURIComponent(turn.sessionId)}`;
const readHistory = () =>
t.target.fetch(`${sessionPath}/stream?follow=false&startIndex=0`, {
Expand Down
2 changes: 1 addition & 1 deletion e2e/fixtures/agent-decide/evals/failure.eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default defineEval({
turn.eventsSatisfy("routing failure is reported", (events) =>
events.some(
(event) =>
event.type === "turn.settled" &&
event.type === "session.ended" &&
event.data.outcome === "failed" &&
event.data.error?.message.includes("Decision service unavailable"),
),
Expand Down
2 changes: 1 addition & 1 deletion e2e/fixtures/agent-mcp/evals/annotations.eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default defineEval({
"Alice asks the front desk to stop yesterday's kennel report run. MCP_CANCEL",
);
cancelled.expectOk();
cancelled.notEvent("input.requested");
cancelled.notEvent("interaction.opened");
cancelled.calledTool("loopback__agent_cancel", {
count: 1,
output: mentions(DESTRUCTIVE_DENIAL),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineEval({
turn.eventsSatisfy("the turn reports a missing dynamic model selection", (events) =>
events.some(
(event) =>
event.type === "turn.settled" &&
event.type === "session.ended" &&
event.data.outcome === "failed" &&
event.data.error?.message.includes("Dynamic model resolver returned no model"),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineEval({
turn.eventsSatisfy("the turn reports the resolver exception", (events) =>
events.some(
(event) =>
event.type === "turn.settled" &&
event.type === "session.ended" &&
event.data.outcome === "failed" &&
event.data.error?.message.includes("intentional resolver failure"),
),
Expand Down
29 changes: 29 additions & 0 deletions e2e/fixtures/agent-subagents/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { e2eAgentConfig } from "@eve-e2e/config";
import { latestTaskResult, playScript } from "@eve-e2e/config/mock-script";
import { defineAgent, defineDynamic } from "eve";
import { mockModel } from "eve/evals";

Expand Down Expand Up @@ -26,6 +27,31 @@ if (process.env.EVE_E2E_MODEL === "mock") {
process.env.EVE_MOCK_AUTHORED_MODELS = "1";
}

const DYNAMIC_SUBAGENT_PROBE = "E2E_DYNAMIC_SUBAGENT_CATALOG";
const dynamicSubagentProbe = mockModel({
modelId: "dynamic-subagent-catalog",
respond(request) {
if (!request.tools.some((tool) => tool.name === "conditional-marker")) {
throw new Error("The resolved dynamic subagent is missing from the model's tools.");
}
if (request.tools.some((tool) => tool.name === "omitted-marker")) {
throw new Error("A dynamic subagent that resolved to null was exposed to the model.");
}
return playScript(
request,
[
{
id: "dynamic-start",
name: "conditional-marker",
input: () => ({ message: "Return your marker." }),
},
{ id: "dynamic-wait", name: "eve__task_wait" },
],
(finished) => latestTaskResult(finished, "conditional-marker") ?? "No dynamic child result.",
);
},
});

const TOOL_FALSE_PROBE = "E2E_TOOL_FALSE_SUBAGENT";
const DISABLED_TOOL_PROBE = "E2E_DISABLED_SUBAGENT";
const hiddenSubagentProbe = mockModel({
Expand Down Expand Up @@ -141,6 +167,9 @@ export default defineAgent({
: message.content.map((part) => (part.type === "text" ? part.text : "")).join(""),
];
});
if (messages.some((message) => message.includes(DYNAMIC_SUBAGENT_PROBE))) {
return { model: dynamicSubagentProbe, modelContextWindowTokens: 1_000_000 };
}
if (
messages.some(
(message) =>
Expand Down
9 changes: 3 additions & 6 deletions e2e/fixtures/agent-subagents/evals/dynamic-subagent.eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ export default defineEval({
description:
"Dynamic subagents are advertised when their resolver returns an agent config and omitted when it returns nil.",
async test(t) {
const selected = await t.send("Call conditional-marker exactly once.");
const selected = await t.send("E2E_DYNAMIC_SUBAGENT_CATALOG");
selected.expectOk();
selected.calledSubagent("conditional-marker", { count: 1 });
selected.messageIncludes("DYNAMIC_SUBAGENT_ENABLED");

const omitted = await selected.session.send("Call omitted-marker exactly once.");

omitted.notEvent("task.started", { data: { name: "omitted-marker" } });
omitted.noFailedActions();
selected.notEvent("child.opened", { data: { name: "omitted-marker" } });
selected.noFailedActions();
},
});
Loading
Loading