Skip to content
Closed
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/background-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Add experimental background tasks for local and remote subagents. With `experimental.tasks` enabled, subagent calls return durable task receipts; parents can inspect, continue, or cancel work while lifecycle notifications and human-input requests arrive asynchronously. Remote child streams are exposed through an authenticated parent-origin proxy so clients never receive remote credentials.
2 changes: 2 additions & 0 deletions docs/tools/human-in-the-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ semantics.

The run picks back up exactly where it parked. Because the pause is durable, nothing is held in memory while it waits — the process can restart and the parked turn survives.

When a background subagent requests input, eve emits the same `input.requested` event on its parent session. Answering through that parent session routes the response directly to the blocked child without invoking the parent model.

For approval requests, unrelated follow-up text does not deny the tool call. eve keeps the approval pending and holds that text until the approval is answered, then replays it as the next message in the session.

See [Sessions, runs & streaming](/docs/concepts/sessions-runs-and-streaming) for the full event and resume contract that this builds on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,7 @@ export default defineEval({

const probe = `CREATE-ONCE-PROBE-${crypto.randomUUID()}`;
const liveProbe = t.target.watchTurn(first.sessionId, { startIndex: firstTurn.events.length });
await continueSession(
t.target,
PRINCIPAL_A,
first.sessionId,
first.continuationToken,
probe,
);
await continueSession(t.target, PRINCIPAL_A, first.sessionId, first.continuationToken, probe);
const probeTurn = await liveProbe.result();
probeTurn.expectOk();
probeTurn.event("message.received", { count: 1, data: { message: probe } });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineDynamic, defineInstructions } from "#public/instructions/index.js";

export default defineDynamic({
events: {
"session.started": (_event, ctx) =>
defineInstructions({
markdown: `Use session ${ctx.session.id} when correlating evidence.`,
}),
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineDynamic, defineInstructions } from "#public/instructions/index.js";

export default defineDynamic({
events: {
"session.started": (_event, ctx) =>
defineInstructions({
markdown: `Use session ${ctx.session.id} when correlating evidence.`,
}),
},
});
11 changes: 11 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicSkill/v7.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineDynamic, defineSkill } from "#public/skills/index.js";

export default defineDynamic({
events: {
"turn.started": (_event, ctx) =>
defineSkill({
description: `Review evidence for session ${ctx.session.id}.`,
markdown: "# Evidence review\n\nCheck every claim against its source.",
}),
},
});
11 changes: 11 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicSkill/v8.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineDynamic, defineSkill } from "#public/skills/index.js";

export default defineDynamic({
events: {
"turn.started": (_event, ctx) =>
defineSkill({
description: `Review evidence for session ${ctx.session.id}.`,
markdown: "# Evidence review\n\nCheck every claim against its source.",
}),
},
});
12 changes: 12 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicTool/v10.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineDynamic, defineTool } from "#public/tools/index.js";

export default defineDynamic({
events: {
"session.started": (_event, ctx) =>
defineTool({
description: "Return the active session identifier.",
inputSchema: { type: "object", properties: {} },
execute: () => ({ sessionId: ctx.session.id }),
}),
},
});
12 changes: 12 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicTool/v11.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineDynamic, defineTool } from "#public/tools/index.js";

export default defineDynamic({
events: {
"session.started": (_event, ctx) =>
defineTool({
description: "Return the active session identifier.",
inputSchema: { type: "object", properties: {} },
execute: () => ({ sessionId: ctx.session.id }),
}),
},
});
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/compatibility/hook/v8.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineHook } from "#public/hooks/index.js";

export default defineHook({
events: {
"subagent.completed"(event, ctx) {
console.info("subagent completed", {
output: event.data.output,
sessionId: ctx.session.id,
subagentName: event.data.subagentName,
});
},
},
});
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/compatibility/hook/v9.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineHook } from "#public/hooks/index.js";

export default defineHook({
events: {
"subagent.called"(event, ctx) {
console.info("subagent called", {
childSessionId: event.data.childSessionId,
sessionId: ctx.session.id,
subagentName: event.data.name,
});
},
},
});
12 changes: 12 additions & 0 deletions packages/eve/extension-contracts/compatibility/tool/v7.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineTool } from "#public/tools/index.js";

export default defineTool({
description: "Summarize a completed task.",
inputSchema: { type: "object", properties: {} },
async execute(_input, ctx) {
return { callId: ctx.callId, summary: "Task completed." };
},
toModelOutput(output) {
return { type: "text", value: output.summary };
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicInstructions",
"epoch": 8,
"sha256": "2f160e8da5e5e9c019164a36e29520fca45ff3be63399eee91e235bb268c56ea",
"exports": ["defineDynamic"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicInstructions",
"epoch": 9,
"sha256": "8b36b0c2b148b52c18bc2eb87b1c29b0987c379836c88c6410a68b1dfde44a54",
"exports": ["defineDynamic"]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicSkill/v8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicSkill",
"epoch": 8,
"sha256": "2f160e8da5e5e9c019164a36e29520fca45ff3be63399eee91e235bb268c56ea",
"exports": ["defineDynamic"]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicSkill/v9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicSkill",
"epoch": 9,
"sha256": "8b36b0c2b148b52c18bc2eb87b1c29b0987c379836c88c6410a68b1dfde44a54",
"exports": ["defineDynamic"]
}
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicTool/v11.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicTool",
"epoch": 11,
"sha256": "137b1e3f0031f508e4902966823a9d00444c234c94bdaaa2ad772bfe14c364e0",
"exports": [
"DynamicToolEntry",
"DynamicToolEvents",
"DynamicToolResult",
"DynamicToolSet",
"defineDynamic"
]
}
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicTool/v12.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicTool",
"epoch": 12,
"sha256": "2be81075d7dbf585be7854384ea21409f53e02f00d432d32d0d2fd6bc54ff79f",
"exports": [
"DynamicToolEntry",
"DynamicToolEvents",
"DynamicToolResult",
"DynamicToolSet",
"defineDynamic"
]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/hook/v10.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "hook",
"epoch": 10,
"sha256": "9339bef30e59e9be24afd8c0b126a337f14747dd3f4314bdb306c2526c663d86",
"exports": ["defineHook"]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/hook/v9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "hook",
"epoch": 9,
"sha256": "6b46bf605ee89c3f7ce3a14f0ce5e0247fd03f9af9f1fd8fc15f2c7fdef58676",
"exports": ["defineHook"]
}
22 changes: 22 additions & 0 deletions packages/eve/extension-contracts/reports/tool/v8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"kind": "eve-extension-capability-contract",
"capability": "tool",
"epoch": 8,
"sha256": "d2c29a2f29a00db5c0ec65f5abf95c4ac57f2a55b6df034122c8e2dbe98c8406",
"exports": [
"defineBashTool",
"defineGlobTool",
"defineGrepTool",
"defineReadFileTool",
"defineTool",
"defineWriteFileTool",
"disableTool",
"experimental_workflow",
"isDisabledToolSentinel",
"isExperimentalWorkflowToolDefinition",
"toolOutput",
"toolOutputPart",
"toolResultFrom",
"webSearch"
]
}
1 change: 1 addition & 0 deletions packages/eve/src/channel/session-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const sessionCallbackSchema = z
.object({
callId: z.string().min(1),
subagentName: z.string().min(1),
taskId: z.string().min(1).optional(),
token: z.string().min(1),
url: z.string().min(1),
})
Expand Down
2 changes: 1 addition & 1 deletion packages/eve/src/channel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export interface TurnCaller {
readonly taskId?: string;
readonly replyTo:
| { readonly kind: "hook"; readonly token: string }
| { readonly kind: "callback"; readonly url: string };
| { readonly kind: "callback"; readonly token: string; readonly url: string };
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/eve/src/cli/dev/tui/tool-presentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ describe("presentTool", () => {
grep: { pattern: "useEve" },
load_skill: { skill: "commit" },
read_file: { filePath: "/workspace/a.ts" },
task_cancel: { taskIds: ["task_abc"] },
task_peek: { taskIds: ["task_abc"] },
task_send: { message: "Continue with the next region.", taskId: "task_abc" },
task_sleep: { seconds: 30 },
todo: { todos: [] },
web_fetch: { url: "https://example.com" },
web_search: { query: "eve framework" },
Expand Down
49 changes: 49 additions & 0 deletions packages/eve/src/cli/dev/tui/tool-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,37 @@ const BUILTIN_TOOL_COPY: Readonly<Record<string, BuiltinToolCopy>> = {
singularNoun: "file",
pluralNoun: "files",
},
task_cancel: {
verb: "Cancel",
pastVerb: "Cancelled",
argKey: "taskIds",
extractItem: taskIdsArg,
singularNoun: "task",
pluralNoun: "tasks",
},
task_peek: {
verb: "Check",
pastVerb: "Checked",
argKey: "taskIds",
extractItem: taskIdsArg,
singularNoun: "task",
pluralNoun: "tasks",
},
task_send: {
verb: "Send",
pastVerb: "Sent",
argKey: "taskId",
singularNoun: "task",
pluralNoun: "tasks",
},
task_sleep: {
verb: "Pause",
pastVerb: "Paused",
argKey: "seconds",
extractItem: sleepSecondsArg,
singularNoun: "pause",
pluralNoun: "pauses",
},
web_fetch: {
verb: "Fetch",
pastVerb: "Fetched",
Expand Down Expand Up @@ -359,6 +390,24 @@ function webSearchActionArg(input: unknown): string | undefined {
* renders verbatim in aggregated rows, so a model-controlled value must lose
* its terminal controls here, not at the render call sites.
*/
/** Joins a `taskIds: string[]` argument into one salient line. */
function taskIdsArg(input: unknown): string | undefined {
if (input === null || typeof input !== "object" || Array.isArray(input)) return undefined;
const value = (input as Record<string, unknown>).taskIds;
if (!Array.isArray(value)) return undefined;
const ids = value.filter((id): id is string => typeof id === "string");
if (ids.length === 0) return undefined;
return salientLine(ids.join(", "));
}

/** Formats a `seconds: number` argument as a duration. */
function sleepSecondsArg(input: unknown): string | undefined {
if (input === null || typeof input !== "object" || Array.isArray(input)) return undefined;
const value = (input as Record<string, unknown>).seconds;
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
return `${String(value)}s`;
}

function salientArg(input: unknown, key: string): string | undefined {
if (input === null || typeof input !== "object" || Array.isArray(input)) return undefined;
const value = (input as Record<string, unknown>)[key];
Expand Down
39 changes: 39 additions & 0 deletions packages/eve/src/client/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,4 +967,43 @@ describe("ClientSession", () => {
streamIndex: 2,
});
});

it("hands the cursor from an aborted idle follow to the next send", async () => {
const streamStartIndices: Array<string | null> = [];
vi.spyOn(globalThis, "fetch").mockImplementation(async (request, init) => {
if ((init?.method ?? "GET") === "POST") return createAcceptedResponse();
const url =
typeof request === "string" ? request : request instanceof URL ? request.href : request.url;
streamStartIndices.push(new URL(url).searchParams.get("startIndex"));
return createStreamResponse([
{ type: "turn.started", data: {} },
{
type: "session.waiting",
data: { continuationToken: "eve:test", wait: "next-user-message" },
},
]);
});
const session = createSession({
continuationToken: "eve:test",
sessionId: "session_1",
streamIndex: 0,
});
const idleAbort = new AbortController();

vi.useFakeTimers();
try {
for await (const event of session.stream({ signal: idleAbort.signal })) {
if (event.type === "session.waiting") idleAbort.abort();
}

expect(session.state.streamIndex).toBe(2);
expect(await collectEventTypes(await session.send("follow up"))).toEqual([
"turn.started",
"session.waiting",
]);
} finally {
vi.useRealTimers();
}
expect(streamStartIndices).toEqual([null, "2"]);
});
});
10 changes: 5 additions & 5 deletions packages/eve/src/compiler/extension-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ interface ExtensionCapabilityContract {

const EXTENSION_CAPABILITY_CONTRACTS = {
extension: { current: 1, supported: [1], dropped: {} },
tool: { current: 7, supported: [1, 2, 3, 4, 5, 6, 7], dropped: {} },
dynamicTool: { current: 10, supported: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dropped: {} },
tool: { current: 8, supported: [1, 2, 3, 4, 5, 6, 7, 8], dropped: {} },
dynamicTool: { current: 12, supported: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], dropped: {} },
connection: { current: 2, supported: [1, 2], dropped: {} },
hook: { current: 8, supported: [1, 2, 3, 4, 5, 6, 7, 8], dropped: {} },
hook: { current: 10, supported: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dropped: {} },
skill: { current: 1, supported: [1], dropped: {} },
dynamicSkill: { current: 7, supported: [1, 2, 3, 4, 5, 6, 7], dropped: {} },
dynamicSkill: { current: 9, supported: [1, 2, 3, 4, 5, 6, 7, 8, 9], dropped: {} },
instructions: { current: 1, supported: [1], dropped: {} },
dynamicInstructions: { current: 7, supported: [1, 2, 3, 4, 5, 6, 7], dropped: {} },
dynamicInstructions: { current: 9, supported: [1, 2, 3, 4, 5, 6, 7, 8, 9], dropped: {} },
config: { current: 1, supported: [1], dropped: {} },
state: { current: 2, supported: [1, 2], dropped: {} },
} as const satisfies Record<string, ExtensionCapabilityContract>;
Expand Down
Loading