Skip to content

durable-harness/fx: Agent Runs from libfx hooks (#11), with handed-off and fenced turns kept open - #14

Closed
pranaygp wants to merge 5 commits into
chad/agent-runs-libraryfrom
pgp/agent-runs-fx-fixes
Closed

pranaygp wants to merge 5 commits into
chad/agent-runs-libraryfrom
pgp/agent-runs-fx-fixes

Conversation

@pranaygp

@pranaygp pranaygp commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

#11 (Chad's agentRunsHooks for libfx's own durable agents) with the fixes from my review of it, on top of chad/agent-runs-library (#10) as it is now.

What libfx 0.0.14 sends, measured on a bare createFxEngine:

Case Events
Fenced append journal.error { fenced: true } (null turn and execution ids), then turn.end { outcome: 'interrupted' }: no fenced outcome, no handoff
Handoff (deadline, lost lease, failed claim) tool.end { status: 'cancelled' }, then turn.end { outcome: 'interrupted', handoff: true }
User's cancel the same, without handoff

Changes in agentRunsHooks:

  • Handoff and fence (blocker): the turn stays open, and so do the tool calls the handoff cut off: a cancelled tool.end waits for the turn's end, so the rerun continues the same call instead of opening a second one. journal.error { fenced } marks the execution recording at that moment.
  • interrupted without a handoff is the user's cancel: it ends as cancelled, as does cancelled.
  • Executions: a turn's events must come from the execution that started it, so after a takeover in one process the old execution's end can't close the new one's steps or the turn. Steps and model calls are keyed by their index within the execution.
  • Eviction: a session's trace starts only on turn.start, and is dropped between turns once its queue drains. Journal receipts and a title call that outlives its turn make no entry.
  • NITs:
    • captureContent follows the capture decision when agentRuns() is registered before the agent is made (libfx reads it once, then), and stays true before that.
    • Failures go to agentRuns({ onError }) (hook.onError).
    • Turns take the session's persisted sequence, since libfx's hooks carry no turn number.
    • A cancelled model or tool call ends as cancelled rather than failed.
    • Not changed: reasoning tokens and the routed provider have no field in @vercel/agent-runs 0.0.1's model result.

Tests (test/fx-agent-runs.test.ts, 14 pass):

  • handoff and fence, each continued in another process with one tool span;
  • interrupted, cancelled and failed endings;
  • a takeover in one process;
  • turns numbered 0 and 1, no session kept between them or made by late events;
  • a private audience: no content anywhere, captureContent: false;
  • nothing recorded before registration.

The takeover test and six of the others fail on d2cc60d's hook.

Also run on this head: workspace typecheck; durable-harness (356 pass), harness-world (374 pass) and demo (25 pass) tests.

Left as-is: the libfx peer range still admits 0.0.13-dev builds, whose types have no FxHooks. That only matters for an app on such a build that type-checks node_modules.

🤖 Generated with Claude Code

…ts semantic hooks

Signed-off-by: Chad Hietala <chad.hietala@vercel.com>
A session's trace is found or made, and its event queued, synchronously in
onEvent, and dropped only when its turn finished with nothing queued
behind it. Deleting it at turn.end orphaned a next turn whose turn.start
was already queued: its later events went to a new, empty session and
were dropped, its completion among them.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 15:31
@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
durable-fx-example Ready Ready Preview, v0 Oct 16, 2026 3:42pm UTC
harness-on-world Ready Ready Preview, v0 Oct 16, 2026 3:42pm UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread packages/durable-harness/src/instrumentation/checkpoints.ts Outdated
const { outcome, error, handoff } = e.data;
// Handed to another worker, fenced out by one (libfx then ends it `interrupted`), yielded or paused:
// the execution that resumes it continues the turn, with its cut-off calls still open.
if (!handoff && !turn.fenced && outcome !== 'yielded' && outcome !== 'paused' && outcome !== 'fenced') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the replaced execution emits its fence receipt after a same-process takeover starts, the replacement's successful turn end is ignored and its root span remains unfinished.

Fix on Vercel

…execution and session is its own

libfx 0.0.14 ends a fenced execution `interrupted` after a `journal.error { fenced: true }` that names no turn or execution, and a handoff `interrupted` with `handoff: true`, after `tool.end` reports the call it cut off as cancelled (measured on a bare createFxEngine). So:

- A turn handed off or fenced out stays open, and its cut-off tool calls stay open until the turn's end, so the rerun continues the same call. `interrupted` with no handoff is the user's cancel and ends as `cancelled`.
- Events are scoped to the execution that started the turn: after a takeover in this process, the old execution's end doesn't close the new one's steps or the turn.
- A session's trace starts only with a turn's start, and is dropped between turns once its queue drains: journal receipts and calls that outlive their turn make none.
- With agentRuns() registered before the agent is made, `captureContent` follows the audience's capture decision (`hook.capture()`). Failures go to `hook.onError`, and turns take the session's persisted sequence.
- A cancelled model or tool call ends as `cancelled` rather than failed.
- harness-world: `SessionTracing.step()` keeps #11's `at` (the execution and its step index) on chad/agent-runs-library's interface.

Tests: handoff, fence, interrupted, cancelled and failed endings; a takeover in one process; turn numbering and eviction; a private audience; and no recording before registration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@pranaygp
pranaygp force-pushed the pgp/agent-runs-fx-fixes branch from 3921109 to cf8e06d Compare October 16, 2026 15:40
@pranaygp
pranaygp changed the base branch from chad/agent-runs-fx-hooks to chad/agent-runs-library October 16, 2026 15:40
@pranaygp pranaygp changed the title durable-harness/fx: handed-off and fenced turns stay open; executions and sessions scoped durable-harness/fx: Agent Runs from libfx hooks (#11), with handed-off and fenced turns kept open Oct 16, 2026
@pranaygp

Copy link
Copy Markdown
Collaborator Author

Superseded by Chad's cfeff14 on chad/agent-runs-fx-hooks, which fixes the same review items. I ran this PR's 14 tests, built on libfx 0.0.14's measured events (handoff, fence, the user's cancel, cancelled, failed, a takeover in one process, turn numbering and eviction, a private audience, nothing before registration), against cfeff14. 13 pass. The 14th expects captureContent: true before agentRuns() is registered; cfeff14 picks false and documents registering tracing first. That's a reasonable call, so nothing here is needed. Closing.

🤖 Posted by Claude Code

@pranaygp pranaygp closed this Oct 16, 2026
@pranaygp
pranaygp deleted the pgp/agent-runs-fx-fixes branch October 16, 2026 15:43

This branch was successfully deployed

2 active deployments
Preview – harness-on-world — cf8e06d4 Deployed Oct 16, 2026 by vercel[bot]
Preview – durable-fx-example — cf8e06d4 Deployed Oct 16, 2026 by vercel[bot]
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.

3 participants