feat(sdk,kernel): f.agent resume continues a recorded CLI session

Merged
KH
13d ago
+655-32

This is the session-resume piece of the Babysitter owner plan, approved by the lead as "the heart of Khaliq's knows the original intent ask". A fixer woken again on the same PR, in the same reused sandbox (cloud#4356 + flows#635), continues its previous agent session instead of starting cold. The feature itself is generic f.agent + kernel; Babysitter adoption is a follow-up.

API

const first = await f.agent("fix", { task });
await f.agent("fix-again", { task: next, resume: first.sessionId });
  • AgentResult.sessionId: the session the CLI ran as (Claude's session id, Codex's thread.started id).
    • It's read from the journaled transcript evidence (trajectory_tail.transcript.result.session_id), so it is also reported for agents that answer with a JSON object.
    • It's only offered when it passes the resume rule, so a bounded or truncated label never is.
  • resume (also resume: on a YAML agent step):
    • Claude runs -p --resume <id>; Codex runs exec resume … <id> with the same flags.
    • A resume to any other CLI, or over transport: "relay", is refused rather than run cold.
  • Validation: one lexical rule, identical in TS and Rust and tested against testdata/agent-resume-cases.json: 1–200 of [A-Za-z0-9._:-], starting with a letter or digit, so an id can never be parsed as a CLI flag.
  • Kernel:
    • resume joins the closed STEP_AGENT_FIELDS;
    • its shape is checked before serde (null/number refused, not read as absence);
    • a malformed id is InvalidAgentResume;
    • skip_serializing_if, so every existing spec keeps its bytes and hash.

Checked against the real CLIs first (evidence/agent-resume/05-real-cli-resume.txt)

# Missing session, real Claude CLI (2.x on this host):
$ claude -p --resume 00000000-0000-4000-8000-0000000000aa "hi" --output-format json
No conversation found with session ID: 00000000-0000-4000-8000-0000000000aa
exit=1

# Resume from a different directory than the session was created in:
$ (cd A && claude -p "Remember the code word PELICAN-42. Reply only with OK." --output-format json)
A session: 1908508f-989e-4159-82b0-6a676c3d74d3 result: OK
$ (cd B && claude -p --resume 1908508f-989e-4159-82b0-6a676c3d74d3 "What was the code word? Reply with just it." --output-format json)
B session: 1908508f-989e-4159-82b0-6a676c3d74d3 result: 'PELICAN-42' error: False

Cloud's bootstrap does not override CLAUDE_CONFIG_DIR/CODEX_HOME, so sessions live under $HOME and survive in a reused box.

Gate edits (please review)

  • tests/spec-parity.test.ts: agent-resume is added to the fixture list. It extends the gate and loosens nothing. It caught the YAML path silently dropping the field, which is fixed.
  • packages/schema/flows.schema.json is regenerated by scripts/generate-json-schema.mjs, as schema-publish.yml requires.

Evidence (evidence/agent-resume/, unedited)

Red, with packages/sdk/src and packages/surface/src at main via git stash:

# packages/sdk/src and packages/surface/src at origin/main (22cd1836) via git stash; tests and testdata from this branch.
$ (cd packages/sdk && npx vitest run tests/agent-resume.test.ts tests/spec-parity.test.ts)
   × spec parity: one dialect at the SDK<->kernel boundary > compiles agent-resume to the pinned canonical JSON 5ms
   × spec parity: one dialect at the SDK<->kernel boundary > hashes agent-resume to the pinned spec_hash 1ms
   × spec parity: one dialect at the SDK<->kernel boundary > round-trips agent-resume across the kernel dialect 1ms
Error: Failed to load url ../src/agent-resume.js (resolved id: ../src/agent-resume.js) in /home/khaliqgant/Projects/AgentWorkforce/flows-worktrees/agent-resume/packages/sdk/tests/agent-resume.test.ts. Does the file exist?
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯
CompileError: spec compile failed:
CompileError: spec compile failed:
CompileError: spec compile failed:
 Test Files  2 failed (2)
      Tests  3 failed | 46 passed (49)

Green:

$ (cd packages/sdk && npx vitest run tests/agent-resume.test.ts tests/spec-parity.test.ts)
 Test Files  2 passed (2)
      Tests  75 passed (75)
$ (cd kernel && cargo test -p relayflowd-core)
test result: ok. 86 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.71s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ node scripts/generate-json-schema.mjs && git diff --stat -- packages/schema
Generated packages/schema/flows.schema.json (74 definitions)
 packages/schema/flows.schema.json | 10 ++++++++++
 1 file changed, 10 insertions(+)
$ (cd packages/schema && bun run test)
 86 pass
 0 fail

Full SDK suite (02: typecheck, build and test typecheck pass):

# RELAYFLOWD_BIN points at this branch's kernel build (kernel/target/debug/relayflowd); the ambient value named a missing toolchain binary.
$ (cd packages/sdk && RELAYFLOWD_BIN=$PWD/../../kernel/target/debug/relayflowd npx vitest run)
   × durable authored root > journals exact source and Surface authority and completes the admitted root 5069ms
   × durable authored root > resumes past an answered question: the wait is closed, the body re-runs and completes 5005ms
   × hosted extension hostile protocol > rejects an import-time different PR frame with zero adapter calls 1543ms
   × flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 5118ms
   × flows check --watch > refreshes the import graph and notices missing imports being created 6406ms
   × flows check --watch > detects a nearer config appearing and falls back after it is deleted 5822ms
   × built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step 8098ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯
 Test Files  6 failed | 267 passed | 1 skipped (274)
      Tests  7 failed | 4166 passed | 20 skipped (4193)

# The 7 failures above are 5s timeouts under full-suite load; the same files in isolation:
$ (cd packages/sdk && RELAYFLOWD_BIN=... npx vitest run tests/cli-watch.test.ts tests/authored-root.test.ts tests/hosted-extension-protocol.test.ts tests/live-kernel.test.ts tests/flow-executor-chain.test.ts tests/mcp.test.ts )
 Test Files  6 passed (6)
      Tests  156 passed (156)
  • The 7 full-run failures are 5s timeouts under load; the same 6 files pass in isolation (156/156).
  • RELAYFLOWD_BIN points at this branch's kernel build, because the ambient value named a missing binary.
  • cargo fmt --check and clippy -D warnings report findings only in files this PR doesn't touch. Clippy reports 8 errors on clean main with the local toolchain, too. My new hunks are formatted; I did not reformat unrelated code.

Not in this PR

Babysitter adoption. The fixer has to persist {cli, sessionId} beside its PR checkout cache and pass resume only when that cache was restored on the same box. A missing session fails the step: Claude exits 1 with "No conversation found". The adoption also needs an SDK release that the sandbox runs.

🤖 Generated with Claude Code


[!NOTE]
Medium Risk
Changes agent-step execution and spec hashing for flows that declare resume; mis-resume or missing CLI session stores fail steps at runtime rather than silently starting new sessions.

Overview
Adds resume on agent steps and AgentResult.sessionId so a later f.agent (or YAML agent step) can continue the same Claude or Codex conversation instead of starting cold.

Spec and validation: resume is a new optional agent field in the SDK, JSON schema, and kernel (STEP_AGENT_FIELDS, spec hashing when set). Both sides enforce the same lexical session-id rule (shared testdata/agent-resume-cases.json); invalid ids fail closed, and resume with transport: "relay" is rejected.

Execution: Direct-spawn workers pass resume into Claude (-p --resume) and Codex (exec resume); other CLIs or relay dispatch refuse the step rather than run without the session. Codex transcripts now surface thread.started as session_id; authored f.agent reads journaled transcript evidence and returns sessionId when it passes validation (including JSON-object outputs via onCompleted).

Docs: docs/SURFACE.md documents the resume workflow and constraints.

Reviewed by Cursor Bugbot for commit 146ba8dfb625ee4c6ecede5ecb08bf52aca23b99. Bugbot is set up for automated code reviews on this repo. Configure here.


Agent Relay sessions

  • claude session 497569fc · opened via gh pr create · last active 2026-10-09