Repository navigation
durable-harness: Agent Runs tracing on @vercel/agent-runs - #10
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This comment has been minimized.
This comment has been minimized.
|
Fixed the reported capture-policy issue in 95b24ed. Both resume-only paths now re-enter the saved turn with the current capture decision before returning handles or settling restored tools. The library narrows the turn and its descendants, so newly private model/tool content is not checkpointed or exported. Missing checkpoints still return no turn. Regression coverage includes public-to-private/unknown recovery on warm and fresh workers, restricted model/tool content in checkpoints and exports, and memoized kernel tool settlement. Also updated Validation: 12 tracing/recovery tests plus the scripted pi-durable integration test passed; durable-harness and cli-check typechecks, package build, distribution checks, generic public hygiene, and changeset checks passed. The Linux-only relay caller check was not exercised on macOS. @vercel-security-reviewer please recheck the finding against this commit. |
pranaygp
left a comment
There was a problem hiding this comment.
Reviewed with three passes: durability of the tracing core, privacy and packaging, and #11's fx hooks. The design is right: one turn span across invocations, content decided where it's recorded, and nothing loaded without agentRuns(). The three earlier fixes in 0dc07a5 hold.
Requesting changes for:
- Blocker (privacy): a resumed turn keeps the capture decision it started with, in the kernel and in pi-durable. This confirms the security review's finding and is wider than it reported.
- Should fix before merge:
- the proxy buffers every request body with no cap, even without tracing;
- drives can hang on unbounded checkpoint I/O;
- a tracing error can fail a model call;
- a continued turn is traced under a queued follow-up;
- a failed checkpoint read looks like no checkpoint;
- MCP tool tokens are kept in checkpoint streams.
Tests: cli-check and the canary now record content for every audience, so apart from one scripted check, no harness path tests a private or unknown session any more. Could expectToolTurn run each harness again with the default capture and unknown audience, and assert the marker is in no span and no _agent-runs_ stream line?
CI's test failure here was the harness-sandbox relay race, which this PR doesn't touch; I re-ran it.
|
Review complete. |
|
@chadhietala thanks, 95b24ed fixes the resume capture blocker in both paths. I've marked those threads. To get #10 and #11 in for launch without colliding on your branches, I'm taking the rest of the review on stacked branches, each with a PR into yours:
Keep pushing to your branches as you like; I'll merge yours into mine. If you've already started on any of these, reply here and I'll drop it. 🤖 Posted by Claude Code |
|
Addressed the review in ff4eb4b, d9e4484, and 3963827 (privacy recovery was already fixed in 95b24ed).
Validation: 39 tracing/recovery/packaging tests, the 14-case scripted matrix, 32 process gateway/model tests, eight workspace typechecks, package build, declaration resolution with agent-runs disabled, check-dist, and generic public hygiene passed. The broader durable-harness suite timed out; isolating public-sandbox.test.ts shows four OpenCode cases failing because the installed @opencode/core cannot resolve @effect/platform-node (same on both branches). Linux relay caller validation remains CI-only. The deletion-only worktree was not touched. Requesting re-review. |
pranaygp
left a comment
There was a problem hiding this comment.
Thanks for ff4eb4b, d9e4484 and 3963827. They fix every item from my last review; I checked each against the code, and the affected tests and the typecheck pass at 3963827.
Two new problems, both reproduced. Each makes a continued or completed turn export its spans twice, with the same span id. Neither leaks content. The first hits the main cross-invocation path (every drive that runs out of its budget), so I'd fix both before merging. Details inline.
NIT (outside this diff): harness-process's README ModelSteps example (README.md:398) shows gatewayProxy({ credential, token, track: steps.request }) without captureRequest. Code copied from it now records model steps with no request messages. It could add captureRequest: ctx.tracing !== undefined, as the OpenCode and reference harnesses do.
🤖 Posted by Claude Code
|
Fixed both new duplicate-export findings in cf4fb1a and updated the ModelSteps README example with captureRequest. Both new regressions were reproduced before the fix. Validation: 43 tracing/recovery/packaging tests and all 14 scripted harness capture-policy cases passed, plus package typecheck, build, check-dist, generic public hygiene, and changeset checks. The soft-deadline regression verifies unique span IDs across fresh-process recovery; the slow-write regression verifies the durable finished marker prevents a second turn export. Per the requested release-note policy, #10 now adds exactly one changeset. #11 includes this base fix and likewise adds exactly one fx changeset. Requesting re-review. |
cf4fb1a to
e2e462c
Compare
Replace hand-built agent tracing with the shared Agent Runs library. Preserve turn identity and checkpoints across invocations, apply current capture policy on recovery, and record model/tool structure for every harness without exposing provider credentials. Bound tracing waits without dropping final checkpoints, keep untraced gateway requests streaming, isolate tracing failures from agent outcomes, and retain Node 22 for consumers that do not enable the optional tracing peers. Cover capture policy, recovery, deadlines, late writes, packaging, and the scripted harness matrix. Signed-off-by: Chad Hietala <chad.hietala@vercel.com>
f811d0c to
1f3279d
Compare
…g at the crash (fixes CI on main) (#15) CI on `main` has failed every run since 89c0e14 (the push and three daily runs), always on one test: harness-opencode's "a crash while the model streams, after a never tool…" (added in #4). It also failed on one of #10's runs. **Why:** the test's crash is in-process (the chaos step). The first drive is killed, but its stand-in OpenCode host keeps streaming the reply the crash cut off: `unsafe_send_email returned: email sent to a@b.c (message id …)`, 12 characters every 150 ms. That's about half a second of deltas after the crash. When the next drive's recovered reply starts streaming within that window, the two interleave after the `restart` event. Examples from the CI logs: - `"nt to a@b.c Recovered from the log: the email we(message id nt out.msg_qnjjsm74"` - `"Recovered from the log: the email went to a@b.c nt out."` The test's own comment said a real crash writes nothing more; the stand-in didn't model that. So it was a race. It passed when #4 merged and has lost every run since. **Fix:** the stand-in host stops sending at the crash, as a real crash takes the host with it. With that, the test checks the resumed text exactly. The text that streamed before the crash is still checked. Locally, the old test failed 1 run in 6; the new one passed 15 of 15. The whole harness-opencode suite passes. Also in `docs/followups.md`: live events aren't fenced. What this stand-in did (a drive's host still streaming after the next drive took over) could happen with a driver still running after a takeover, such as a stalled process. Test and docs only. harness-opencode isn't published, so there's no changeset. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pranaygp
left a comment
There was a problem hiding this comment.
@chadhietala re-reviewed at 1f3279d / b778a66: both checkpoint fixes are right. The four new tests fail on 3963827's code and pass on yours. The parked-tool fix closes the cut-off-call gap on #11 too. Approving.
One change on your branches: main fixed the same flaky OpenCode test in #15 (dda5ef2), so 2f07eeb (here) and c30b6e3 (#11) take main's version of harness-opencode/src/harness.test.ts. That was #10's only conflict. Once CI is green, I'll merge the stack (#10, then #11) and cut the next beta.
🤖 Posted by Claude Code
…ts semantic hooks (#11) Stacked on #10. libfx's own durable agents (`createFxAgent` with `durability`) show up in Vercel Agent Runs, from the semantic execution hooks libfx added in vercel-labs/fx#1298: ```ts import { createFxAgent } from 'libfx' import { agentRunsHooks } from 'durable-harness/fx' export const agent = createFxAgent({ durability, model, hooks: agentRunsHooks() }) ``` `agentRunsHooks()` maps libfx's events onto `@vercel/agent-runs`, through the same `agentRuns()` integration and per-turn checkpoints as the kernel's sessions: - `turn.start` / `turn.end` are a turn. `completed`, `failed` and `cancelled` end it. `yielded`, `paused`, `interrupted` and `fenced` leave it open, which libfx's contract says aren't failures, and the execution that resumes it, in any process, continues the same turn from its checkpoint. - `step.start` / `step.end` are a step. libfx numbers steps within an execution, and each resume is a fresh execution, so a step's attempt is the execution's own number for the turn, recorded before use. A resumed turn never reuses an earlier execution's step. - `model.start` / `model.end` are the step's model call: messages, instructions and tool definitions in, then the finish reason, usage, generation id, output text and tool calls. A call outside a step (a title, a compaction) gets a step of its own. - `tool.start` / `tool.end` are tool calls under the step, keyed by libfx's authoritative call id, so they join the call in the model's reply. A rerun after a crash is the same call, and an execution can end a call an earlier one started. - `wait.start` / `wait.end` are approvals under their tool call. libfx doesn't wait on hook callbacks, so each session's events are recorded one at a time, in order, and kept alive with the request's `waitUntil` on Vercel. `flush()` settles everything recorded so far. Content is captured from libfx and recorded only where the session's audience allows. It types against libfx 0.0.14 (`FxHooks`, `FxHookEvent`), the first release with the hooks and the one `main` pins. Older libfx has no `hooks` option, so nothing changes for it. ### Also in this PR - The demo's "fx (libfx durable)" lane passes `hooks: agentRunsHooks()`. Its own fetch and tool tracing is gone, and so is `fx-trace.ts`, which read turn ids back from the journal: the hooks hand them over. Its fetch wrapper's timing marks and armed crashes stay, with their tests. - The canary's fx rows trace libfx's agent through the hooks and search those spans too. ### Docs The fx page at vercel.com/docs/agent-harnesses should show `hooks: agentRunsHooks({ world?, name?, audience? })` for Agent Runs. ### Testing `packages/durable-harness/test/fx-agent-runs.test.ts` drives a real libfx durable agent on world-local, with a scripted Gateway, through `agentRunsHooks()`. It checks one turn, completed; its model calls; the tool call with libfx's call id and result, under the step whose model call asked for it; and one trace. It also replays a recorded event sequence through `agentRunsHooks()` on world-local, with an in-memory exporter. A turn's first execution asks for a tool, starts it, waits on an approval and yields. A second execution, on a fresh World handle as in another process, reruns the tool and answers. The test checks one `invoke_agent` span across both executions, three distinct steps with the right usage and input messages, the single tool span ended by the second execution under the step that asked for it, and the approval under the tool. Signed-off-by: Chad Hietala <chad.hietala@vercel.com>
agentRuns()fromdurable-harness/instrumentation/otelnow records sessions with@vercel/agent-runs(vercel/eve#4910), the library eve uses for Agent Runs, instead of durable-harness's own span code. The library writes the turn, step, model and tool spans in the schema Agent Runs reads (theeve.agentscope, schema version 4,vercel.session_idon Vercel), records usage, and keeps a durable turn's checkpoint. durable-harness adds what the library leaves to its host: where the checkpoints live, how the kernel's drives map onto turns, how each harness describes its model calls, and the export to Vercel.What changes for sessions
invoke_agentspan. Before, each invocation added a root of its own to the session's trace. Each turn's checkpoint is a stream of the session's run, written in the background and stored before the drive returns, so any process can continue the turn with the same span ids. A tool call that outlives its drive ends in the drive that settles it.traceAudiencepicks a capture decision (public: inputs and outputs; anything else: metadata only), and content a session declines is never written to a span, a checkpoint or an export.agentRuns({ capture })changes the decision per audience. The per-destinationexportPolicyandfilteringProcessorare gone: the library does this at the source.$eve.*Workflow run tags are no longer written.How harnesses report model calls
harness-world/agent-runs, which imports only the library's types, so a drive withoutagentRuns()registered loads no tracing code. A drive getsctx.tracing.modelCall(), the library's model call and its step.Harness.modelCallssays how a harness reports them.effects(the default) records eachmodeleffect from what it logs;selfmeans the harness records each call itself.harness-world-pi/agent-runs, which pi-durable's storage shares.Package
@vercel/agent-runsand@opentelemetry/apiare dependencies. OpenTelemetry's SDK stays an optional peer that only/instrumentation/otelloads; it owns its tracer provider and leaves the app's alone. durable-harness now requires Node.js 24, as the library does.Docs
main's README now points to the docs at vercel.com/docs/agent-harnesses, so this PR changes no README. The Agent Runs pages there (observability, and the API reference) describe the old integration and need to follow:agentRuns({ audience, capture, serviceName, spanProcessors, onError });traceAudiencedeciding what a session records; noexportPolicyorfilteringProcessor; turns kept across invocations; and Node.js 24.Testing
packages/durable-harness/test/agent-runs.test.tsdrives sessions through the kernel on world-local. It covers the span tree, the attributes Agent Runs reads, a turn continued across two drives as one span, checkpoints deleted once turns finish, and a private session recording no content.packages/cli-check/test/agent-runs.test.tsruns one tool turn of every harness (Pi, pi-durable, fx, AI SDK, Claude Code, Codex, OpenCode) with scripted models; it passes.packages/harness-process/src/agent-runs.test.tscovers the request and response readers for each protocol.AGENT_RUNS_LIVE=1against real AI Gateway models passes for Pi, pi-durable, fx, the AI SDK and Codex. Claude Code was skipped: noclaudeCLI on this machine. OpenCode times out because its model request never starts. That's this machine, not this change: the canary's OpenCode row hangs the same way on a cleanmain.packages/durable-harness/test/canary.test.ts) searches every span again, with content recorded for every audience. fx, Pi, pi-durable and Codex pass. Claude Code is skipped (no pinnedclaudebinary here), and OpenCode hangs, as it does onmainhere.