Skip to content

SDK: first-class headless adapter per agent CLI, replacing raw -p wrapper scripts #141

Description

@khaliqgant

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions