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

Add opt-in `experimental.taskEventDelivery` for agents that orchestrate background work. Task notifications remain chronological, compaction restores task identities, and queued child requests are serviced between parent steps without requiring the parent to end its turn.
12 changes: 12 additions & 0 deletions docs/subagents/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ description: "Delegate work to root-agent copies or declared specialists with th

eve supports two ways to delegate work: the root-only built-in `agent` tool, which starts or continues a copy of the root agent, and declared subagents, which are specialists with their own directories. Use a subagent to run independent work in parallel, narrow the available tools, or give a task to a specialist.

## Agent-directed task delivery

Set `experimental: { taskEventDelivery: true }` in the parent agent definition to let its instructions govern how it handles background results. This disables the default launch acknowledgement and cohort reporting instructions, and stops injecting the changing task inventory into model requests. Existing task receipts and notifications remain in conversation history; task ownership, cancellation, approvals, and notification delivery are unchanged.

With this option, the runtime services queued child requests between parent model steps. The parent does not need to end its turn to start a child, but requests arriving during an in-flight model or tool step wait for a safe step boundary. User messages and task-result notifications still follow their normal turn-delivery policy.

Tell the parent to continue useful work after delegation, consume results as they arrive, and distinguish finishing its current turn from completing the user's request. Background notification turns may intentionally deliver nothing when there is nothing new to report. Child sessions and structured-output sessions still require an output.

After compaction, eve adds a point-in-time index of task IDs, names, and terminal or unsettled statuses. It does not duplicate result bodies or private executor credentials. The compaction summary retains relevant results and artifact paths. Keep large findings in shared artifacts and return their locations in task results. Later notifications supersede the recovery snapshot.

The option defaults to off. It reduces changing prompt context but does not guarantee provider cache hits.

## The built-in `agent` tool

The root session receives `agent` by default. The model calls it to delegate a task to a new copy of the root agent or continue an existing copy:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod";
import { defineDynamic, defineTool } from "#public/tools/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineTool({
description: "Read a report.",
inputSchema: z.object({ report: z.string() }),
execute: ({ report }) => ({ report }),
}),
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineAgent } from "#public/definitions/agent.js";

export default defineAgent({
model: "openai/gpt-5.5",
description: "Inspect a report.",
});
8 changes: 8 additions & 0 deletions packages/eve/extension-contracts/compatibility/tool/v30.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from "zod";
import { defineTool } from "#public/tools/index.js";

export default defineTool({
description: "Read a report.",
inputSchema: z.object({ report: z.string() }),
execute: ({ report }) => ({ report }),
});
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicTool/v30.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicTool",
"epoch": 30,
"sha256": "fc1240ef5a9d41023d6847bf856505a4e8ec9ac135e343b17677e4b413629c61",
"exports": [
"DynamicToolEntry",
"DynamicToolEvents",
"DynamicToolResult",
"DynamicToolSet",
"defineDynamic"
]
}
20 changes: 20 additions & 0 deletions packages/eve/extension-contracts/reports/subagent/v10.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"kind": "eve-extension-capability-contract",
"capability": "subagent",
"epoch": 10,
"sha256": "947460b56cab11573a22ee71731121356bde6073c46bc33c15a245e3ca476ec2",
"exports": [
"AgentCompactionDefinition",
"AgentDefinition",
"AgentModelDefinition",
"AgentStaticModelDefinition",
"DefinedAgent",
"DynamicLocalSubagentDefinition",
"DynamicSubagentDefinition",
"RemoteAgentDefinition",
"RemoteAgentDefinitionInput",
"defineAgent",
"defineDynamic",
"defineRemoteAgent"
]
}
20 changes: 20 additions & 0 deletions packages/eve/extension-contracts/reports/tool/v31.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"kind": "eve-extension-capability-contract",
"capability": "tool",
"epoch": 31,
"sha256": "6e33c63a9a14307ea6456f75dc7c25eca93b99d45e980484a162ce583cdb5e60",
"exports": [
"defaultWebSearch",
"defineTool",
"defineWorkflowTool",
"disableTool",
"experimental_workflow",
"isDisabledToolSentinel",
"isExperimentalWorkflowToolDefinition",
"isWebSearchToolDefinition",
"toolOutput",
"toolOutputPart",
"toolResultFrom",
"webSearch"
]
}
14 changes: 8 additions & 6 deletions packages/eve/src/compiler/extension-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ interface ExtensionCapabilityContract {
const EXTENSION_CAPABILITY_CONTRACTS = {
extension: { current: 1, supported: [1], dropped: {} },
tool: {
current: 30,
supported: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 28, 29, 30],
current: 31,
supported: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 28, 29, 30, 31],
dropped: {
14: "TaskExec.delegated was removed; migrate to workflow-backed background tools",
15: "TaskExec replaces stageEffect with send",
Expand All @@ -42,8 +42,10 @@ const EXTENSION_CAPABILITY_CONTRACTS = {
},
},
dynamicTool: {
current: 29,
supported: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 28, 29],
current: 30,
supported: [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 28, 29, 30,
],
dropped: {
21: "Message and reasoning append events now expose deltas instead of cumulative snapshots.",
23: "TaskExec.delegated was removed; migrate to workflow-backed background tools",
Expand All @@ -68,8 +70,8 @@ const EXTENSION_CAPABILITY_CONTRACTS = {
},
},
subagent: {
current: 9,
supported: [3, 4, 6, 7, 8, 9],
current: 10,
supported: [3, 4, 6, 7, 8, 9, 10],
dropped: {
1: "Persistent subagent sessions are now the default and the experimental opt-in was removed",
2: "Persistent subagent sessions are now the default and the experimental opt-in was removed",
Expand Down
11 changes: 11 additions & 0 deletions packages/eve/src/compiler/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ import {
} from "#compiler/validate-artifact.js";

describe("compiled agent manifest v48", () => {
it.each([undefined, false, true])(
"round-trips the task event delivery opt-in (%s)",
async (taskEventDelivery) => {
const { manifest } = await compileFromMemory({
model: "openai/gpt-5.4",
agent: { model: "openai/gpt-5.4", experimental: { taskEventDelivery } },
});
const parsed = compiledAgentManifestSchema.parse(JSON.parse(JSON.stringify(manifest)));
expect(parsed.config.experimental?.taskEventDelivery).toBe(taskEventDelivery);
},
);
it("round-trips a real compiled graph through the serialized schema", async () => {
const { manifest } = await compileFromMemory({
limits: { maxTokenCostUsdPerSession: 1.5 },
Expand Down
2 changes: 2 additions & 0 deletions packages/eve/src/compiler/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ const compiledAgentConfigBaseFields = {
description: z.string().optional(),
experimental: z
.object({
taskEventDelivery: z.boolean().optional(),
instrumentationProviders: z.boolean().optional(),
workflow: compiledAgentWorkflowDefinitionSchema.optional(),
})
Expand Down Expand Up @@ -1209,6 +1210,7 @@ function cloneCompiledAgentDefinition(config: CompiledAgentDefinition): Compiled
config.experimental === undefined
? undefined
: {
taskEventDelivery: config.experimental.taskEventDelivery,
instrumentationProviders: config.experimental.instrumentationProviders,
workflow:
config.experimental.workflow === undefined
Expand Down
4 changes: 4 additions & 0 deletions packages/eve/src/compiler/normalize-agent-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ function normalizeExperimentalDefinition(

const compiledExperimental: Mutable<NonNullable<CompiledAgentDefinition["experimental"]>> = {};

if (experimental.taskEventDelivery !== undefined) {
compiledExperimental.taskEventDelivery = experimental.taskEventDelivery;
}

if (experimental.instrumentationProviders !== undefined) {
compiledExperimental.instrumentationProviders = experimental.instrumentationProviders;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe("turn workflow wire migrations", () => {
).toEqual({
capabilities: undefined,
completionToken: "turn-token",
driverCapabilities: { cancelledTurnSettle: true, turnInbox: true },
driverCapabilities: { bufferedDeliveries: true, cancelledTurnSettle: true, turnInbox: true },
mode: "conversation",
stepInput: {
input: delivery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface TurnWorkflowInputBase {
*/
readonly driverCapabilities?: {
readonly turnInbox?: true;
readonly bufferedDeliveries?: true;
readonly cancelledTurnSettle?: true;
};
readonly mode: RunMode;
Expand Down Expand Up @@ -108,7 +109,7 @@ export function createTurnWorkflowInput(input: TurnWorkflowDispatchInput): TurnW
return {
capabilities: input.capabilities,
completionToken: input.completionToken,
driverCapabilities: { cancelledTurnSettle: true, turnInbox: true },
driverCapabilities: { bufferedDeliveries: true, cancelledTurnSettle: true, turnInbox: true },
initialCancellation: input.initialCancellation,
initialStep: input.initialStep,
mode: input.mode,
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/execution/durable-session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export interface DurableSession {
readonly workflowMaxSubagents?: number;
readonly agent: {
readonly system: string;
readonly taskEventDelivery?: boolean;
};
readonly compaction?: {
readonly lastKnownInputTokens?: number;
Expand Down
18 changes: 18 additions & 0 deletions packages/eve/src/execution/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ describe("createCompactionConfig", () => {
});

describe("createSession", () => {
it.each([undefined, false, true])(
"carries task event delivery through session hydration (%s)",
(taskEventDelivery) => {
const turnAgent = createTestTurnAgent({ taskEventDelivery });
const session = createSession({
continuationToken: "root-token",
sessionId: "sess-root",
turnAgent,
});
expect(session.agent.taskEventDelivery).toBe(taskEventDelivery);
expect(projectToDurableSession(session).agent.taskEventDelivery).toBe(taskEventDelivery);
const hydrated = hydrateDurableSession({
durable: projectToDurableSession(session),
turnAgent,
});
expect(hydrated.agent.taskEventDelivery).toBe(taskEventDelivery);
},
);
it("creates a session with correct agent configuration", () => {
const outputSchema = { properties: { title: { type: "string" } }, type: "object" } as const;
const session = createSession({
Expand Down
10 changes: 8 additions & 2 deletions packages/eve/src/execution/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ function createSessionAgent(
const base = {
compactionModelReference: turnAgent.compactionModel,
reasoning: turnAgent.reasoning,
taskEventDelivery: turnAgent.taskEventDelivery,
system,
tools,
};
Expand Down Expand Up @@ -200,7 +201,7 @@ export function mintSubagentContinuationToken(suffix?: string): string {
*/
export function projectToDurableSession(session: HarnessSession): DurableSession {
const durable: {
agent: { system: string };
agent: DurableSession["agent"];
compaction?: {
lastKnownInputTokens?: number;
lastKnownPromptMessageCount?: number;
Expand All @@ -216,7 +217,12 @@ export function projectToDurableSession(session: HarnessSession): DurableSession
taskId?: string;
workflowMaxSubagents?: number;
} = {
agent: { system: session.agent.system },
agent: {
system: session.agent.system,
...(session.agent.taskEventDelivery !== undefined
? { taskEventDelivery: session.agent.taskEventDelivery }
: {}),
},
continuationToken: session.continuationToken,
history: session.history,
sessionId: session.sessionId,
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/execution/turn-control-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type TurnControlPayload =
readonly inboxToken: string;
readonly kind: "turn-delivery-request";
readonly requestId: string;
readonly bufferedOnly?: boolean;
}
| { readonly kind: "turn-delivery-accepted"; readonly requestId: string }
| { readonly kind: "turn-delivery-cancelled"; readonly requestId: string };
Expand Down
24 changes: 23 additions & 1 deletion packages/eve/src/execution/turn-control-receiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,26 @@ describe("TurnControlReceiver", () => {
createHookMock.mockReset();
});

it("answers an empty between-step poll without consuming queued user messages", async () => {
installControlHook([
{ ...deliveryRequest("poll-1"), bufferedOnly: true },
{ kind: "turn-delivery-accepted", requestId: "poll-1" },
parkResult(),
]);
const message: DeliverHookPayload = { kind: "deliver", payloads: [{ message: "follow-up" }] };
const buffered = [message];
await runReceiver(buffered);
expect(forwardTurnDeliveryStep).toHaveBeenCalledWith({
inboxToken: "turn-inbox",
payload: {
kind: "driver-delivery",
requestId: "poll-1",
delivery: { kind: "deliver", payloads: [] },
},
});
expect(buffered).toEqual([message]);
});

it("forwards a buffered delivery and consumes it once the turn accepts", async () => {
const delivery: DeliverHookPayload = {
kind: "deliver",
Expand Down Expand Up @@ -318,7 +338,9 @@ function runReceiver(
return receiver.waitForAction().finally(() => receiver.dispose());
}

function deliveryRequest(requestId: string): TurnControlPayload {
function deliveryRequest(
requestId: string,
): Extract<TurnControlPayload, { kind: "turn-delivery-request" }> {
return {
continuationToken: "http:test",
inboxToken: "turn-inbox",
Expand Down
4 changes: 4 additions & 0 deletions packages/eve/src/execution/turn-control-receiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ export class TurnControlReceiver {
await this.commandInbox.rekeyContinuation(request.continuationToken);

let delivery = this.takeInputResponseDelivery();
// A between-step poll must not park the model waiting for future input.
if (delivery === undefined && request.bufferedOnly === true) {
delivery = { kind: "deliver", payloads: [] };
}
while (delivery === undefined) {
const winner = await Promise.race([
this.getControlPromise().then((value) => ({ kind: "control" as const, value })),
Expand Down
Loading
Loading