Problem
An agent step today is spawn(cli, [instruction]) (sdk/src/worker.ts), and the "CLI" is a per-flow wrapper script that calls claude -p … and hands back stdout (testdata/preflight/analyze-story-claude-cli). The research example's first shim did the same. Consequences:
- No trajectory.
claude -p prints only the final message; the step's journal gets one line. RFC-0001 says an agent step's output is artifact + diff + trajectory.
- No budget line. Token usage and cost are discarded, so decision 10 (every token has one owner) cannot be enforced or even observed.
- No subagent evidence. A flow that instructs "spawn two subagents" cannot tell whether it happened.
- Fail-open success. A CLI that exits 0 with no readable final message reads as a success with an empty summary.
- Every flow re-invents the invocation, and each wrapper drifts (model flag, permissions flag, prompt via argv vs stdin).
What exists
Every supported CLI has a non-interactive mode with structured output and usage, verified 2026-09-02:
| CLI |
Invocation |
Final message |
Usage / cost |
Session |
Extra |
| claude |
-p --output-format stream-json --verbose |
{"type":"result","result"} |
usage.*, total_cost_usd |
session_id |
subagent_stats, modelUsage, is_error |
| codex |
exec --json - |
item.completed / agent_message.text |
turn.completed.usage (no cost) |
thread.started.thread_id |
— |
| grok |
--prompt-file F --output-format json |
text |
usage.*, total_cost_usd |
sessionId |
thought, modelUsage |
Proposal
A first-class headless adapter per CLI in the SDK, owned by AgentWorker, replacing per-flow wrapper scripts:
interface HeadlessAdapter {
invocation(opts: { model?: string; promptFile: string; cwd: string }): { argv: string[]; stdin: "prompt" | "none" };
parse(stdout: string): { finalText: string; usage?: Usage; sessionId?: string; subagents?: unknown; events: unknown[] };
}
- The instruction travels by stdin or prompt file, never argv (ARG_MAX).
parse throws when there is no final message → worker_error, never an empty success.
events are journaled as the step's trajectory; usage (cost as a decimal string) is the step's budget line and feeds the kernel's budget envelope; subagents lets a gate assert delegation happened.
RELAYFLOW_MODEL semantics unchanged: set only when declared.
flows check preflight can use the same adapter for the auth status probe, so the probe and the real invocation cannot disagree.
- Adding a CLI (opencode, gemini) is adding one adapter, not touching every flow.
Reference implementation
examples/research/shims/headless.ts on branch research-flow implements the three adapters above with parser tests against the verified shapes (examples/research/tests/research.test.ts), and shims/agent-cli.ts consumes it: writes <step>.trajectory.jsonl, returns usage, sessionId, subagents. Live smoke 2026-09-02: all three CLIs returned final text, usage, session ids and trajectories (12 / 10 / 1 events) in ~19s. Its header says REPLACE-WHEN: the SDK's AgentWorker owns a headless adapter per CLI — this issue.
Done when
AgentWorker dispatches a declared cli: claude|codex|grok through the adapter with no wrapper script; testdata/preflight/analyze-story-claude-cli is deleted or reduced to a fixture.
- The journal's
step.complete for an agent step carries trajectory, usage and sessionId.
- A step whose CLI exits 0 without a final message completes
worker_error.
- The research example's
shims/headless.ts is deleted and the flow runs unchanged.
Related: #132 (v2 authoring ergonomics).
Problem
An
agentstep today isspawn(cli, [instruction])(sdk/src/worker.ts), and the "CLI" is a per-flow wrapper script that callsclaude -p …and hands back stdout (testdata/preflight/analyze-story-claude-cli). The research example's first shim did the same. Consequences:claude -pprints only the final message; the step's journal gets one line. RFC-0001 says an agent step's output is artifact + diff + trajectory.What exists
Every supported CLI has a non-interactive mode with structured output and usage, verified 2026-09-02:
-p --output-format stream-json --verbose{"type":"result","result"}usage.*,total_cost_usdsession_idsubagent_stats,modelUsage,is_errorexec --json -item.completed/agent_message.textturn.completed.usage(no cost)thread.started.thread_id--prompt-file F --output-format jsontextusage.*,total_cost_usdsessionIdthought,modelUsageProposal
A first-class headless adapter per CLI in the SDK, owned by
AgentWorker, replacing per-flow wrapper scripts:parsethrows when there is no final message →worker_error, never an empty success.eventsare journaled as the step's trajectory;usage(cost as a decimal string) is the step's budget line and feeds the kernel's budget envelope;subagentslets a gate assert delegation happened.RELAYFLOW_MODELsemantics unchanged: set only when declared.flows checkpreflight can use the same adapter for theauth statusprobe, so the probe and the real invocation cannot disagree.Reference implementation
examples/research/shims/headless.tson branchresearch-flowimplements the three adapters above with parser tests against the verified shapes (examples/research/tests/research.test.ts), andshims/agent-cli.tsconsumes it: writes<step>.trajectory.jsonl, returnsusage,sessionId,subagents. Live smoke 2026-09-02: all three CLIs returned final text, usage, session ids and trajectories (12 / 10 / 1 events) in ~19s. Its header saysREPLACE-WHEN: the SDK's AgentWorker owns a headless adapter per CLI— this issue.Done when
AgentWorkerdispatches a declaredcli: claude|codex|grokthrough the adapter with no wrapper script;testdata/preflight/analyze-story-claude-cliis deleted or reduced to a fixture.step.completefor an agent step carriestrajectory,usageandsessionId.worker_error.shims/headless.tsis deleted and the flow runs unchanged.Related: #132 (v2 authoring ergonomics).