Skip to content

feat(eve): prototype the per-session notification consumer#1194

Closed
ruiconti wants to merge 14 commits into
rui/remote-agent-callback-notificationsfrom
rui/notification-consumer-prototype
Closed

feat(eve): prototype the per-session notification consumer#1194
ruiconti wants to merge 14 commits into
rui/remote-agent-callback-notificationsfrom
rui/notification-consumer-prototype

Conversation

@ruiconti

Copy link
Copy Markdown
Contributor

Stacked on #1167 (base: rui/remote-agent-callback-notifications) — only the top three commits are this PR. Prototype for the plan in #1172 / issue #1170: validate the consumer design end to end and surface what the real implementation has to deal with. Draft on purpose.

What

  • workflow//eve//notificationConsumer — one companion run per session, started by the entry workflow with the driver's stream writable and serialized channel context, parked on <sessionId>:notify. Each delivery is one journaled step: invoke the channel's existing adapter event handler against the curated child event, append the wrapped subagent.event (now carrying childSessionId) to the session stream. The session workflow is never woken for notifications.
  • Producers ring it fire-and-forget. Callback metadata bakes a notifyUrl targeting the consumer hook; the callee posts notifications there (legacy url fallback for pre-consumer callers); the route rings the token and 202s once the payload is well-formed — a disposed consumer logs and drops.
  • fix(eve): OpenAPI baseUrl resolves from the live authored module. Found while making the fixture work without the eve/tools exposure: the compiled manifest bakes the build-time value, where the deployment's own origin doesn't exist yet, while auth/spec/operations already resolve live. Standalone fix, extractable.
  • Fixture drops the eve/tools exposure (reverts that commit from the base branch): auth triggers are now interactive OpenAPI connections fronting a bearer-gated /probe/credential route on the fixture's own deployment, with a custom AuthFn minting the user principal interactive auth requires. Existing public API only.

All three evals pass through the consumer — 34/34 gates, ~17s locally — including the new coverage that motivated everything: hitl-collision receives and completes an authorization notification while the turn is parked for input, the state where today's architecture silently drops it (resumeHook → 404).

What the prototype surfaced

  • Session-opening race: the consumer creates its hook on its first queue invocation, so a fast callee can ring before it exists. Bounded HookNotFoundError retry in the route covers it; hooks are creator-owned so entry-side pre-creation isn't available.
  • Stable-id trap: start(reference) silently creates a run that later dies WorkflowNotRegisteredError unless the function name is in STABLE_WORKFLOW_NAMES — that set is the whole contract between the bundler and the reference template.
  • Eval-side win: with delivery decoupled from turn state, the hitl eval reads the wrapped notification off the durable stream instead of blind-polling a derived hook URL, which also killed its ordering flake.

Known-red, deferred to the real change

  • childSessionId on SubagentChildEventStreamEvent trips the rule-36 capability-epoch ceremony on four contracts (dynamicTool, hook, dynamicSkill, dynamicInstructions) — additive/compatible, ceremony not run here, so guard:invariants fails on this branch.
  • Local subagent proxying still uses the legacy re-emit (the one-path migration is the plan's scope, not the prototype's).
  • Dispose is a poison-pill ring from the entry's finally; a crash before it leaks a parked run.
  • No changesets; not for merge as-is.

Unit suite 5368 passed, typecheck 33/33, lint clean.

ruiconti added 7 commits July 24, 2026 10:57
Producers append; consumers react. Each session gets one companion
reactor run parked on a notify hook: producers (the remote callback
route, the local subagent proxy, the callee's emit path) deliver
curated child events with one fire-and-forget resumeHook, and the
reactor appends the wrapped subagent.event to the session stream and
performs the side effects (channel rendering, callback forwarding) in
journaled steps. The session workflow is never woken for notifications,
which closes the parked-for-input delivery hole and unifies the local
and remote child-event paths into one shape.

Backed by #1170.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Per-lane wake accounting: the caller already pays a queue wake per
notification (resumeHook into the session workflow), so the reactor
there replaces a wake rather than adding one. The callee's outbound
forwarding is the only lane where a reactor would introduce a new
per-event wake, so v1 keeps the direct POST and merely defers it off
the emitting step's critical path (today the caller's RTT is awaited
inside the emit). Higher-frequency event types are admitted to the
lane only with cursor-based coalescing, so N events during one
invocation cost one wake.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
…tract

No new channel authoring surface. The reactor invokes the same adapter
event handler the session workflow's proxy step invokes for these
events today, so channels render notifications with zero changes; the
run doing the invoking is the only difference. Adapter state mutated
while handling a notification persists in the reactor's own run state,
which also resolves the edit-in-place rendering question without a
stateless-hook contract. The subagent.event wrapper stays a
stream-representation concern for followers and clients.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
The reactor is a framework-registered workflow function
(workflow//eve//notificationReactor) in the same compiled bundle as the
entry and turn workflows, executing as queue-driven invocations of the
caller's own deployment — no new compute surface. Started plainly by
the entry run (no latest-deployment routing): it holds the entry's
stream writable and channel context, so it is pinned to the entry's
deployment for the session's lifetime.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
…nsumers

Same design; the run is a consumer, and the doc now says so everywhere,
including the workflow id (workflow//eve//notificationConsumer) and the
file name.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
…tion

Lead the summary with the model rather than the bug: the durable
stream is the propagation backbone, producers append, and consumption
is a separately scheduled concern — followers already work this way,
and the notification consumer is the first non-follower consumer moved
onto it. The failure list stays as the evidence, and the lane is named
as the on-ramp for future event classes (progress, proxied HITL) and
for converging the remaining emit-coupled consumers.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Same design, a third shorter: cut repeated rationale and filler; every
section now states its point once.

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 8:40pm
eve-docs-1644 Ready Ready Preview, Comment, Open in v0 Jul 24, 2026 8:40pm
eve-docs-4759 Ready Ready Preview, Comment, Open in v0 Jul 24, 2026 8:40pm

@github-actions

Copy link
Copy Markdown
Contributor

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

Key takeaways

  • No notable deltas vs rui/remote-agent-callback-notifications (413ce48).

Delta vs rui/remote-agent-callback-notifications (413ce48)

Area Metric Baseline Current Delta
Package Packed tarball 4.05 MB 4.05 MB +830 B ⚠️
Package Unpacked publish size 14.72 MB 14.72 MB +4.8 kB ⚠️
Package Installed footprint 53.87 MB 53.88 MB +4.8 kB ⚠️
Package Published files 2748 2753 +5
Package Installed files 5988 5993 +5
Runtime Unique function payloads 2 2 0
Runtime Total function bytes 11.67 MB 11.69 MB +15.6 kB ⚠️
Runtime Public routes 10 10 0
Changed function payloads vs rui/remote-agent-callback-notifications (413ce48) (2)
Function Status Baseline Current Delta Route changes
functions/.well-known/workflow/v1/flow.func changed 7.27 MB 7.28 MB +13.1 kB ⚠️ none
functions/__server.func changed 4.41 MB 4.41 MB +2.5 kB ⚠️ none

eve init install

Metric Baseline Current Delta
Installed footprint 92.27 MB 92.28 MB +4.8 kB ⚠️
Installed packages 122 122 0
dependencies 4 4 0
devDependencies 2 2 0
Dependency package bytes 29.00 MB 29.00 MB +4.8 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-24T20:34:33.823Z
  • Route aliases: 10 public, 1 internal (11 total aliases)
  • Vercel routes in config: 11
  • Severity legend: 🔴 dominant/large, 🟠 notable, 🟡 watch, ⚪ small
Package Drill-Down

Package Details

  • Package: eve@0.27.3
  • Package directory: packages/eve
  • Tarball: 4.05 MB (eve-0.27.3.tgz)
  • Unpacked payload: 14.72 MB across 2753 published files
  • Installed footprint: 53.88 MB across 5993 installed files
  • Installed root package: 13.44 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.2%)
  • eve: 13.44 MB (24.9%)
  • ai: 6.43 MB (11.9%)
  • zod: 5.04 MB (9.4%)
  • 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.26 MB 76.5%
Installed footprint breakdown
Installed package size
🔴 @rolldown/binding-linux-x64-gnu [########################] 18.96 MB 35.2%
🔴 eve                             [#################.......] 13.44 MB 24.9%
🔴 ai                              [########................] 6.43 MB 11.9%
🔴 zod                             [######..................] 5.04 MB 9.4%
🟠 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.28 MB across 7861 installed files
  • Installed packages: 122 total (116 transitive-only)
  • dependencies: 4 direct packages totaling 29.00 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.44 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.44 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.3.tgz 13.44 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     [########################] 7.28 MB 62.3%
🟠 functions/__server.func                         [###############.........] 4.41 MB 37.7%

Top Function Payloads

🟠 functions/.well-known/workflow/v1/flow.func • 1 public route • 7.28 MB
Metric Value
Public routes /.well-known/workflow/v1/flow
Runtime nodejs24.x
Handler index.mjs
Payload 7.28 MB
Function files 7.28 MB across 40 files
Traced dependencies 0 B
Signal 🟠 Bundled file __eve_nitro_handler__.mjs is 2.72 MB (37.4%)

🟠 🔎 Dependency Analysis

📦 Bundled files:

Bundled file size
🟠 __eve_nitro_handler__.mjs       [########################] 2.72 MB 37.4%
🟡 _chunks/world-vercel.mjs        [########................] 903.2 kB 12.4%
🟡 _chunks/sandbox.mjs             [#######.................] 767.6 kB 10.5%
🟡 _libs/@ai-sdk/gateway+[...].mjs [####....................] 432.5 kB 5.9%
🟡 _chunks/token-util-B6qBs3-0.mjs [###.....................] 379.1 kB 5.2%
🟠 Other bundled files             [##################......] 2.08 MB 28.5%

🧾 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 • 9 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
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.1%)

🟠 🔎 Dependency Analysis

📦 Bundled files:

Bundled file size
🟠 _chunks/runtime-artifacts.mjs   [########################] 1.41 MB 32.1%
🟠 _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             [##########..............] 598.0 kB 13.6%

🧾 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.46 s -> 2.41 s (-52.7 ms) vs rui/remote-agent-callback-notifications (413ce48).
  • Timing is informational: shared GitHub runners are too variable for a hard timing budget.
Detailed phase timings vs `rui/remote-agent-callback-notifications (413ce48)`
Phase Baseline Current Delta
extension.check 1.1 ms 1.1 ms 0.0 ms
project.resolve 0.6 ms 0.6 ms 0.0 ms
workspace.create 0.8 ms 0.7 ms -0.1 ms
host.prepare 169.1 ms 141.4 ms -27.7 ms
vercel.service-prefix.resolve 2.0 ms 1.8 ms -0.2 ms
nitro.app.create 24.4 ms 24.3 ms -0.1 ms
nitro.app.cache.prepare 0.3 ms 0.2 ms -0.1 ms
nitro.app.prepare 1.0 ms 0.7 ms -0.3 ms
nitro.app.public-assets 0.7 ms 0.7 ms 0.0 ms
nitro.app.prerender 0.5 ms 0.5 ms 0.0 ms
nitro.app.bundle 665.3 ms 653.6 ms -11.7 ms
nitro.app.cache.write 0.4 ms 0.4 ms 0.0 ms
sandbox.prewarm 226.2 ms 241.1 ms +14.9 ms
nitro.flow.create 163.0 ms 156.7 ms -6.3 ms
nitro.flow.cache.prepare 0.2 ms 0.2 ms 0.0 ms
nitro.flow.prepare 0.7 ms 0.9 ms +0.2 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.03 s 993.7 ms -41.1 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 161.8 ms 181.6 ms +19.8 ms
agent-summary.emit 0.5 ms 0.5 ms 0.0 ms
nitro.app.close 0.1 ms 0.0 ms -0.1 ms
output.publish 3.6 ms 3.4 ms -0.2 ms
workspace.remove 5.7 ms 5.9 ms +0.2 ms

ruiconti added 7 commits July 24, 2026 16:37
…llbacks

Session callbacks now carry an event envelope discriminated by
event.status ("notification" | "termination"; "working" and
"input_required" are reserved and rejected). A remote callee forwards
its authorization.required/authorization.completed events to the caller
as notification callbacks, which the caller re-emits on its own stream
without resolving the pending call — the callback-URL analog of the
local subagent adapter's subagent-authorization-event proxying. A parent
that is itself a remote callee relays proxied child events one more hop
up its own callback URL.

Notification delivery is best-effort (an unreachable caller is logged on
the callee and never fails its turn); termination stays the single
durable resume path. The wire shape changed: mixed-version caller/callee
deployments reject each other's callbacks.

parseSessionCallback is renamed to parseCallbackMetadata to distinguish
the create-session callback metadata from the new callback events.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Models routinely emit outputSchema: {} for the optional per-call param
on the lowered subagent tool. Local delegation filters non-empty objects
(subagent-tool.ts) but remote dispatch forwarded the empty object, which
forced the callee into structured-output task completion and failed the
call with OUTPUT_SCHEMA_NOT_FULFILLED even when the callee produced a
correct prose result. Remote dispatch now applies the same non-empty
filter, falling back to the definition's declared schema.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
When one model step requested both an approval-gated tool call and a
runtime action (subagent or remote-agent call), the park classification
returned on the runtime-action batch before the input-request park ran,
silently dropping the approval: input.requested was never emitted, and
the action-result resume called the model with the gated call's tool_use
still unanswered — AI_MissingToolResultsError, failing the turn.

The classification now persists both batches (the input batch with empty
responseMessages, since the action batch owns the step's assistant
messages) and emits input.requested immediately. On the action-result
resume, executeStepBody's existing staged resolution re-parks on the
still-unanswered batch, committing the resolved action results to
history and closing the turn with the standard waiting boundary.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
… HITL collision

New agent-remote-callbacks fixture whose probe-remote subagent calls the
fixture's own deployment (WORKFLOW_LOCAL_BASE_URL locally, VERCEL_URL on
Vercel; loopback callback hosts are allowed by the SSRF guard), keeping
the suite self-contained while exercising the full remote dispatch and
session-callback wire.

Three evals:
- auth-propagation: the callee parks on requestAuthorization, the
  forwarded authorization.required surfaces on the caller's stream, the
  eval completes the webhookUrl with a ?code, and the code round-trips
  through the callee's resume into the caller's final reply.
- hitl-collision: one turn holds a pending release_gate approval and an
  in-flight remote call; the callee's auth completes (deterministic
  <childSessionId>:auth hook) while the caller is parked for input, and
  both resolve independently. Caught the dropped-approval harness bug.
- notification-propagation: the callee's local credential-probe subagent
  relays its authorization events two hops to the caller, each exactly
  once and in order, without resolving the pending call. Also caught the
  empty-outputSchema dispatch bug.

The hitl eval uses plain fetch for the hook: Client.fetch treats its
whole path argument as pathname, so a ?code= query would be
percent-encoded into the path and 404 the hook.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
The compiled manifest bakes the baseUrl evaluated during eve build,
where a deployment's own origin (dev-server port, VERCEL_URL) may not
exist yet. Runtime resolution already re-imports the authored module so
live values can reference ambient state — auth, spec, operations — but
kept the compile-time url copy. Prefer the re-imported module's baseUrl
when present, consistent with the module's own contract.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
One companion workflow run per session (workflow//eve//notificationConsumer,
stable-named beside the entry and turn workflows), started by the entry
with the driver's stream writable and serialized channel context, parked
on <sessionId>:notify. Each delivery runs one journaled step: invoke the
channel's existing adapter event handler against the curated child event
and append the wrapped subagent.event (now carrying childSessionId) to
the session stream.

Producers ring it fire-and-forget: callback metadata bakes a notifyUrl
targeting the consumer hook, the callee posts notifications there
(falling back to the legacy url for pre-consumer callers), and the
callback route rings the token with a brief HookNotFound retry for the
session-opening race — always 202 once well-formed, drop and log
otherwise. The session workflow is never woken for notifications, which
also delivers them while the turn is parked for input — previously a
silent drop. Termination is untouched.

Prototype: local subagent proxying still uses the legacy re-emit, and
the childSessionId protocol addition trips the rule-36 capability-epoch
ceremony, deferred to the real change.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
The fixture triggers authorization.required through existing public API
only. An interactive OpenAPI connection per hop (probe, nested-probe)
fronts a bearer-gated /probe/credential channel route on the fixture's
own deployment; completeAuthorization mints the bearer from the
callback's ?code, so the credential still proves the round trip. A
custom AuthFn on the eve channel projects the probe-remote bearer into
the user principal interactive authorization requires.

The connections read their self baseUrl through a property getter
resolved live at runtime (the compile-time copy bakes a placeholder) and
keep no cross-step token cache: every eval session shares one user
principal, so a module-level cache would bleed one eval's token into the
next session and suppress its authorization.required.

Evals assert the consumer's wrapped subagent.event instead of top-level
re-emits. hitl-collision completes the hook from the wrapped
notification itself — live-stream first, durable-stream poll when the
approval parks the turn before the notification lands — covering
parked-for-input delivery, which the pre-consumer architecture dropped.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
@ruiconti
ruiconti force-pushed the rui/notification-consumer-prototype branch from fa78d44 to 80cc51e Compare July 24, 2026 20:38
@ruiconti

Copy link
Copy Markdown
Contributor Author

Restacking: this lands before #1167, not after — replacement PR based on the research branch follows.

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