Skip to content

feat(eve): background tasks - inert task mode (slice 1)#1190

Draft
ruiconti wants to merge 3 commits into
mainfrom
rui/background-tasks-slice1
Draft

feat(eve): background tasks - inert task mode (slice 1)#1190
ruiconti wants to merge 3 commits into
mainfrom
rui/background-tasks-slice1

Conversation

@ruiconti

Copy link
Copy Markdown
Contributor

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, the TaskNotification envelope, DEFAULT_NOTIFICATION_ROUTES (wake-worthy kinds only), the CreateTaskResult placeholder, 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, update requires input_required), cross-run reads, guarded notification fan-out, and the getTask/updateTask/cancelTask service.
  • Caller wiring — election in the dispatch step (placeholder at the call position, no child started), a task.* arm on the callback route that resumes the driver with kind: "deliver", routing-step discrimination (terminal → run a turn with the outcome as input; input_required → re-emit input.requested, no turn), and step-0 routing of late answers to updateTask before stale conversion.
  • harness/task-state.ts — the live-task index (taskId → actor run id) on session state, next to proxyInputRequests.

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: getWritable only 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 commandId ack. 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 — RuntimeActionRequest is in their public closure (found by bisecting against scripts/extension-capability-contracts.mjs). An inert slice shouldn't bump extension contracts, so elections are recorded per callId on PendingRuntimeActionBatch.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: taskNotifications stays undeclared on the public DeliverPayload type 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, so waitForRuntimeActionResults resolves the key instantly via initialResults and 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 suppresses subagent.completed — the work just started.

Invariants held

  • Call positions are terminalized at election; outcomes enter as new input (asserted on history in the integration test).
  • NextDriverAction untouched; the driver's wake vocabulary stays deliver-only. Discrimination lives in step bodies that run at latest.
  • Task ids are minted (task_ + UUID); endpoints and run ids never appear on streams.
  • Deliveries with an authenticated principal that mismatches the task's creator are dropped at routing; task-delivery turns run under the initiator.
  • Notification sends never throw; a 404 marks the endpoint dead, no retries.

Testing

execution/tasks/notifications.integration.test.ts drives the whole wire through a real workflowEntry run: 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 an input_required task 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 in service.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 the sync()/defer()/background() combinators. Known inert-mode edge: continuation-token rotation 404s a registered endpoint (dead-marked, notification dropped) — noted for the doc amendment.

ruiconti added 3 commits July 24, 2026 14:47
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>
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eve-docs Ready Ready Preview, Comment, Open in v0 Jul 24, 2026 6:52pm

@github-actions

Copy link
Copy Markdown
Contributor

Bundle + Package Summary: apps/fixtures/weather-agent

Key takeaways

  • Bundle warning: function payloads grew 13.1%.
  • Runtime delta: function payloads 11.68 MB -> 13.20 MB (+1.52 MB ⚠️); 1 changed payload.

❌ Bundle Warning: Action Will Fail

This action will fail because the bundle warning policy was exceeded. Add the acknowledge-bundle-warning label to acknowledge the regression and allow the check to pass without regenerating this report.

Area Warning Details
Runtime Total function bytes 11.68 MB -> 13.20 MB; +1.52 MB ⚠️ (13.1%) over limit 10.0%

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

ruiconti added a commit that referenced this pull request Jul 24, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant