feat(eve): background tasks - inert task mode (slice 1)#1190
Conversation
Slice 1 of the background-tasks plan (#1084): the MCP-aligned task record (Task/DetailedTask discriminated on status, ttlMs nullable, input_required carrying InputRequests inline), the TaskNotification envelope with DEFAULT_NOTIFICATION_ROUTES narrowed to wake-worthy kinds, and the CreateTaskResult placeholder projection. readBackgroundElection is the single seam later slices extend; the internal env gate (EVE_INTERNAL_BACKGROUND_TASK_ELECTION) exists only so integration coverage can exercise the creation path before any public elector ships. Signed-off-by: Rui Conti <ruiconti@gmail.com>
…-out Each background task is a dedicated durable workflow run that owns its record single-writer: transitions arrive as commands on the actor's hook, legality (terminal-is-final, cancelled sticky, update requires input_required) is applied in one pure function, and every applied transition appends a full snapshot to the actor's own stream and fans out one POST per routed endpoint. The actor exists because workflow-run streams are write-local but read-global: getWritable only targets the current run, so a store the dispatch step, the routing step, the turn step, and external executors can all write to must be a run they can all resumeHook. Readers use cross-run tail reads; command senders poll for their commandId ack. Notification delivery is best-effort by contract: a gone subscriber (HTTP 404 from the callback route) is marked dead and never retried, and no delivery failure ever fails the task — unlike the terminal session callback, which throws to hand retry to the orchestrator. Signed-off-by: Rui Conti <ruiconti@gmail.com>
…se wiring Wires inert task mode through both roles end-to-end: - Election is read in the dispatch step from the pending batch's taskElections map (per-callId, gated by the tool definition's internal taskSupport flag). Elections ride the batch — session- internal state — not the action request, whose type is part of the extension capability contracts. An elected call starts no child: the dispatch step creates the task, registers the session driver's callback URL as the wake endpoint, and pre-seeds the CreateTaskResult placeholder so the in-turn wait resolves instantly and the turn can end with the work still running. The placeholder suppresses subagent.completed — the work just started. - A task notification travels as an ordinary deliver payload carrying the reserved taskNotifications field (undeclared on the public DeliverPayload type; concat-merged on coalesce; stripped from public ingress and from adapter-visible payloads). The callback route grew a task.* arm that resumes the token with kind "deliver" so a parked driver wakes — its wake vocabulary stays deliver-only and NextDriverAction stays closed. - The routing step discriminates per notification: terminal outcomes stay on the remainder and run a turn that projects the outcome as new input (the call position was terminalized at election); input_required is consumed in place, re-emitting the record's live requests as the session's own input.requested without waking the model; progress and non-wake kinds are consumed silently; unknown tasks and cross-principal authenticated deliveries are dropped. - Step-0 late responses: answers matching a live input_required task's requests route to updateTask before stale conversion — the original action can resume instead of degrading to advisory text. Task deliveries run under the session's initiator principal. The live-task index (taskId → actor run id) lives on session state next to proxyInputRequests; its only writers — dispatch at election, turnStep at terminal consumption — sit on the threaded path. Signed-off-by: Rui Conti <ruiconti@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Bundle + Package Summary:
|
| Area | Warning | Details |
|---|---|---|
| Runtime | Total function bytes | 11.68 MB -> 13.20 MB; +1.52 MB |
Delta vs main (5841c14)
| Area | Metric | Baseline | Current | Delta |
|---|---|---|---|---|
| Package | Packed tarball | 4.06 MB | 4.07 MB | +15.9 kB |
| Package | Unpacked publish size | 14.74 MB | 14.80 MB | +60.6 kB |
| Package | Installed footprint | 53.90 MB | 53.96 MB | +60.6 kB |
| Package | Published files | 2757 | 2779 | +22 |
| Package | Installed files | 5997 | 6019 | +22 |
| Runtime | Unique function payloads | 2 | 2 | 0 |
| Runtime | Total function bytes | 11.68 MB | 13.20 MB | +1.52 MB |
| Runtime | Public routes | 11 | 11 | 0 |
Changed function payloads vs main (5841c14) (2)
| Function | Status | Baseline | Current | Delta | Route changes |
|---|---|---|---|---|---|
functions/.well-known/workflow/v1/flow.func |
changed | 7.27 MB | 8.79 MB | +1.52 MB |
none |
functions/__server.func |
changed | 4.41 MB | 4.41 MB | +3.0 kB |
none |
eve init install
| Metric | Baseline | Current | Delta |
|---|---|---|---|
| Installed footprint | 92.30 MB | 92.36 MB | +60.6 kB |
| Installed packages | 122 | 122 | 0 |
| dependencies | 4 | 4 | 0 |
| devDependencies | 2 | 2 | 0 |
| Dependency package bytes | 29.02 MB | 29.08 MB | +60.6 kB |
| devDependency package bytes | 5.04 MB | 5.04 MB | 0 B ➖ |
Build Metadata
- Preset:
vercel - Nitro:
nitro@3.0.260610-beta - Output directory:
apps/fixtures/weather-agent/.vercel/output - Build metadata timestamp: 2026-07-24T18:52:27.049Z
- Route aliases: 11 public, 1 internal (12 total aliases)
- Vercel routes in config: 12
- Severity legend: 🔴 dominant/large, 🟠 notable, 🟡 watch, ⚪ small
Package Drill-Down
Package Details
- Package:
eve@0.27.5 - Package directory:
packages/eve - Tarball: 4.07 MB (
eve-0.27.5.tgz) - Unpacked payload: 14.80 MB across 2779 published files
- Installed footprint: 53.96 MB across 6019 installed files
- Installed root package: 13.52 MB
- Installed dependencies: 40.44 MB
- Runtime dependencies: 1
- Peer dependencies: 5 (4 optional)
Installed footprint is measured from an isolated temporary npm install of the packed tarball.
Heavy installed dependencies
@rolldown/binding-linux-x64-gnu: 18.96 MB (35.1%)eve: 13.52 MB (25.1%)ai: 6.43 MB (11.9%)zod: 5.04 MB (9.3%)nitro: 2.41 MB (4.5%)
Publish payload breakdown
Published file size
🟠 dist/src/compiled/experimental-ai-sdk-code-mo... [###.....................] 1.51 MB 10.2%
🟡 dist/src/compiled/@vercel/sandbox/index.js [#.......................] 632.4 kB 4.3%
🟡 dist/src/compiled/_chunks/workflow/undici-C2Z... [#.......................] 502.4 kB 3.4%
🟡 dist/src/compiled/@chat-adapter/slack/index.js [#.......................] 440.5 kB 3.0%
🟡 dist/src/compiled/@vercel/oidc/index.js [#.......................] 379.5 kB 2.6%
🔴 Other published files [########################] 11.34 MB 76.6%
Installed footprint breakdown
Installed package size
🔴 @rolldown/binding-linux-x64-gnu [########################] 18.96 MB 35.1%
🔴 eve [#################.......] 13.52 MB 25.1%
🔴 ai [########................] 6.43 MB 11.9%
🔴 zod [######..................] 5.04 MB 9.3%
🟠 nitro [###.....................] 2.41 MB 4.5%
🟡 @ai-sdk/provider-utils [#.......................] 852.0 kB 1.6%
🔴 Other installed packages [#########...............] 6.75 MB 12.5%
Runtime dependencies (1)
| Package | Range | Notes |
|---|---|---|
nitro |
3.0.260610-beta |
Peer dependencies (5)
| Package | Range | Notes |
|---|---|---|
@opentelemetry/api |
^1.0.0 |
optional peer |
ai |
catalog: |
|
braintrust |
^3.0.0 |
optional peer |
just-bash |
^3.0.0 |
optional peer |
microsandbox |
^0.5.0 |
optional peer |
eve init install drill-down
eve init install details
- Command:
eve init my-agent - Package manager:
npm - Installed footprint: 92.36 MB across 7887 installed files
- Installed packages: 122 total (116 transitive-only)
- dependencies: 4 direct packages totaling 29.08 MB
- devDependencies: 2 direct packages totaling 5.04 MB
- Other transitive package files: 58.24 MB
Installed footprint is measured from an isolated temporary eve init my-agent using the current packed eve tarball.
Heavy installed dependencies
@typescript/typescript-linux-x64: 27.95 MB (30.3%)@rolldown/binding-linux-x64-gnu: 18.96 MB (20.5%)eve: 13.52 MB (14.6%)zod: 9.00 MB (9.7%)ai: 6.43 MB (7.0%)
Installed footprint breakdown
Installed package size
🔴 @typescript/typescript-linux-x64 [########################] 27.95 MB 30.3%
🔴 @rolldown/binding-linux-x64-gnu [################........] 18.96 MB 20.5%
🔴 eve [############............] 13.52 MB 14.6%
🔴 zod [########................] 9.00 MB 9.7%
🔴 ai [######..................] 6.43 MB 7.0%
🟠 @types/node [##......................] 2.54 MB 2.8%
🔴 Other installed packages [############............] 13.97 MB 15.1%
dependencies (4)
| Package | Range | Installed size | Share |
|---|---|---|---|
@vercel/connect |
0.4.2 |
135.8 kB | 0.1% |
ai |
^7.0.34 |
6.43 MB | 7.0% |
eve |
file:eve-0.27.5.tgz |
13.52 MB | 14.6% |
zod |
4.4.3 |
9.00 MB | 9.7% |
devDependencies (2)
| Package | Range | Installed size | Share |
|---|---|---|---|
@types/node |
24.x |
2.54 MB | 2.8% |
typescript |
7.0.2 |
2.50 MB | 2.7% |
Function Drill-Down
Payload Size Graph
Unique function payload size and share of total
🔴 functions/.well-known/workflow/v1/flow.func [########################] 8.79 MB 66.6%
🟠 functions/__server.func [############............] 4.41 MB 33.4%
Top Function Payloads
🔴 functions/.well-known/workflow/v1/flow.func • 1 public route • 8.79 MB
| Metric | Value |
|---|---|
| Public routes | /.well-known/workflow/v1/flow |
| Runtime | nodejs24.x |
| Handler | index.mjs |
| Payload | 8.79 MB |
| Function files | 8.79 MB across 40 files |
| Traced dependencies | 0 B |
| Signal | 🔴 Bundled file __eve_nitro_handler__.mjs is 4.23 MB (48.1%) |
🔴 🔎 Dependency Analysis
📦 Bundled files:
Bundled file size
🔴 __eve_nitro_handler__.mjs [########################] 4.23 MB 48.1%
🟡 _chunks/world-vercel.mjs [#####...................] 903.2 kB 10.3%
🟡 _chunks/sandbox.mjs [####....................] 767.6 kB 8.7%
🟡 _libs/@ai-sdk/gateway+[...].mjs [##......................] 432.5 kB 4.9%
🟡 _chunks/token-util-B6qBs3-0.mjs [##......................] 379.1 kB 4.3%
🟠 Other bundled files [############............] 2.08 MB 23.7%
🧾 Vercel Config
{
"handler": "index.mjs",
"launcherType": "Nodejs",
"shouldAddHelpers": false,
"supportsResponseStreaming": true,
"runtime": "nodejs24.x",
"environment": {
"WORKFLOW_PRECONDITION_GUARD": "1",
"NODE_OPTIONS": "--experimental-require-module"
},
"maxDuration": "max",
"experimentalTriggers": [
{
"type": "queue/v2beta",
"topic": "__eve776561746865722d6167656e74_wkf_workflow_*",
"consumer": "default",
"retryAfterSeconds": 5,
"initialDelaySeconds": 0
}
]
}🟠 functions/__server.func • 10 public routes, 1 internal alias • 4.41 MB
| Metric | Value |
|---|---|
| Public routes | //eve/v1/callback/[token]/eve/v1/connections/[name]/callback/[token]/eve/v1/health/eve/v1/info/eve/v1/session/eve/v1/session/[sessionId]/eve/v1/session/[sessionId]/cancel/eve/v1/session/[sessionId]/stream/eve/v1/session/reset |
| Internal aliases | /__server |
| Runtime | nodejs24.x |
| Handler | index.mjs |
| Payload | 4.41 MB |
| Function files | 4.41 MB across 30 files |
| Traced dependencies | 0 B |
| Signal | 🟠 Bundled file _chunks/runtime-artifacts.mjs is 1.41 MB (32.0%) |
🟠 🔎 Dependency Analysis
📦 Bundled files:
Bundled file size
🟠 _chunks/runtime-artifacts.mjs [########################] 1.41 MB 32.0%
🟠 _chunks/world-vercel.mjs [###############.........] 900.3 kB 20.4%
🟠 _chunks/sandbox.mjs [#############...........] 767.6 kB 17.4%
🟡 _chunks/token-util-B6qBs3-0.mjs [######..................] 379.1 kB 8.6%
🟡 _chunks/dist-BX517Nmz.mjs [######..................] 348.7 kB 7.9%
🟡 Other bundled files [##########..............] 603.9 kB 13.7%
🧾 Vercel Config
{
"handler": "index.mjs",
"launcherType": "Nodejs",
"shouldAddHelpers": false,
"supportsResponseStreaming": true,
"runtime": "nodejs24.x"
}Build Timing: e2e/fixtures/agent-tools-sandbox
This is an informational timing measurement inside eve build, from preflight through publication. Output-size measurement and profile writing are excluded.
Build mode: deployable Vercel build with sandbox template prewarm included.
- Build pipeline: 2.41 s -> 3.06 s (+641.5 ms) vs
main (5841c14). - Timing is informational: shared GitHub runners are too variable for a hard timing budget.
Detailed phase timings vs `main (5841c14)`
| Phase | Baseline | Current | Delta |
|---|---|---|---|
extension.check |
1.0 ms | 1.0 ms | 0.0 ms |
project.resolve |
0.5 ms | 0.5 ms | 0.0 ms |
workspace.create |
0.8 ms | 0.7 ms | -0.1 ms |
host.prepare |
176.8 ms | 280.8 ms | +104.0 ms |
vercel.service-prefix.resolve |
1.7 ms | 1.6 ms | -0.1 ms |
nitro.app.create |
23.9 ms | 23.3 ms | -0.6 ms |
nitro.app.cache.prepare |
0.2 ms | 0.2 ms | 0.0 ms |
nitro.app.prepare |
0.8 ms | 0.7 ms | -0.1 ms |
nitro.app.public-assets |
0.7 ms | 0.7 ms | 0.0 ms |
nitro.app.prerender |
0.5 ms | 0.4 ms | -0.1 ms |
nitro.app.bundle |
673.2 ms | 671.0 ms | -2.2 ms |
nitro.app.cache.write |
0.3 ms | 0.3 ms | 0.0 ms |
sandbox.prewarm |
183.7 ms | 162.0 ms | -21.7 ms |
nitro.flow.create |
157.0 ms | 384.7 ms | +227.7 ms |
nitro.flow.cache.prepare |
0.2 ms | 0.2 ms | 0.0 ms |
nitro.flow.prepare |
0.7 ms | 0.7 ms | 0.0 ms |
nitro.flow.public-assets |
0.2 ms | 0.2 ms | 0.0 ms |
nitro.flow.prerender |
0.0 ms | 0.0 ms | 0.0 ms |
nitro.flow.bundle |
1.02 s | 1.10 s | +74.5 ms |
nitro.flow.cache.write |
0.3 ms | 0.3 ms | 0.0 ms |
nitro.flow.close |
0.2 ms | 0.2 ms | 0.0 ms |
workflow.emit |
160.7 ms | 420.0 ms | +259.3 ms |
agent-summary.emit |
0.5 ms | 0.5 ms | 0.0 ms |
nitro.app.close |
0.1 ms | 0.1 ms | 0.0 ms |
output.publish |
3.1 ms | 3.3 ms | +0.2 ms |
workspace.remove |
4.8 ms | 5.2 ms | +0.4 ms |
Implementation of Slice 1 (#1190) showed session state cannot own the record: it is threaded through step results, while transitions must be writable from paths holding no threadable state — the routing step while parked, the callback route, a detached executor. Each record is now owned by a dedicated durable task run (single-writer actor, command hook in, snapshots out, cross-run tail reads); the caller keeps a live-task index on session state and the recorded election rides the pending action batch, keeping the extension capability contracts untouched. Also notes the inert-mode consumer-orphaning edge under continuation-token rotation. Signed-off-by: Rui Conti <ruiconti@gmail.com>
Part of #1084. Implements Slice 1 of
research/background-tasks.md: the task contract and both protocols (caller and callee) land end-to-end with no public electors. Nothing user-facing changes; the creation path is exercised through an internal env gate, with unit and integration coverage only.What
runtime/tasks/— the MCP-aligned contract:Task/DetailedTask, theTaskNotificationenvelope,DEFAULT_NOTIFICATION_ROUTES(wake-worthy kinds only), theCreateTaskResultplaceholder, and the election seam Slices 2/3 extend.execution/tasks/— the callee side: a per-task actor run owning the record, a pure transition function (terminal-is-final, cancelled sticky,updaterequiresinput_required), cross-run reads, guarded notification fan-out, and thegetTask/updateTask/cancelTaskservice.task.*arm on the callback route that resumes the driver withkind: "deliver", routing-step discrimination (terminal → run a turn with the outcome as input;input_required→ re-emitinput.requested, no turn), and step-0 routing of late answers toupdateTaskbefore stale conversion.harness/task-state.ts— the live-task index (taskId → actor run id) on session state, next toproxyInputRequests.The task actor
The doc says records "live on the durable session state". That doesn't survive contact with the runtime: session state is threaded through step results, and the routing step — which handles notifications while the session is parked — returns only
{ remainder }to the pinned driver. Transitions must also be writable from the callback path and (Slice 2) a child session. Workflow-run streams are write-local/read-global:getWritableonly targets the current run,getRun(id)only reads.So each task is a small dedicated workflow run: it loops on its own hook, applies each resumed command under the legality rules, appends a full snapshot to its own stream, fans out one POST per routed endpoint, and returns on terminal. Everyone else either tail-reads the stream or sends a command and polls for its
commandIdack. Single-writer legality and ordered transition bursts fall out for free, and it matches the extension's receiver-owned record. The research doc needs a follow-up amendment for this (on the #1085 branch, not here).Election rides the batch, not the request
First cut added
task?: { ttlMs? }to the action-request schemas. That silently changed four extension-capability contract epochs —RuntimeActionRequestis in their public closure (found by bisecting againstscripts/extension-capability-contracts.mjs). An inert slice shouldn't bump extension contracts, so elections are recorded percallIdonPendingRuntimeActionBatch.taskElections, which is session-internal. Authored-tool election (Slice 3) will change the public schema and take the epoch bump deliberately.Same reasoning for the wire field:
taskNotificationsstays undeclared on the publicDeliverPayloadtype and rides its index signature — it's framework-owned, stripped at public ingress and from adapter-visible payloads."Does not register the action key" — mechanism
The doc's key-withholding is implemented as a pre-seeded result: the dispatch step returns the
CreateTaskResult-shaped result for elected actions, sowaitForRuntimeActionResultsresolves the key instantly viainitialResultsand the turn ends with the work still running. Same observable contract, no epilogue/resolver surgery. Precedent: the recursive-agent-guard synthetic result. The placeholder also suppressessubagent.completed— the work just started.Invariants held
NextDriverActionuntouched; the driver's wake vocabulary staysdeliver-only. Discrimination lives in step bodies that run at latest.task_+ UUID); endpoints and run ids never appear on streams.Testing
execution/tasks/notifications.integration.test.tsdrives the whole wire through a realworkflowEntryrun: mock model elects via the internal gate, the test plays the executor by resuming the actor's hook, and notification POSTs go through the real callback route handler (loopback fetch stub). The sharpest assertion: after answering aninput_requiredtask in the parent conversation, exactly one model turn runs in the whole exchange — a stale-converted answer would produce two. Actor semantics (sticky cancellation, post-terminal reads, 404 dead-marking) are covered inservice.integration.test.ts; everything else is unit-tested.Out of scope (Slice 2 per the doc)
Session-done cancellation of live tasks, descendant abort propagation,
task.*lifecycle events on the parent stream, and thesync()/defer()/background()combinators. Known inert-mode edge: continuation-token rotation 404s a registered endpoint (dead-marked, notification dropped) — noted for the doc amendment.