Skip to content

zidane/headless

Run a prompt to completion without a UI and get one serializable result with the full trajectory.

import { … } from 'zidane/headless'src/headless.ts

Use runHeadless to run a prompt in a chosen working directory for automation, evaluation and RL rollouts. A turn limit and a wall-clock limit bound the run. It resolves with the status, final text, optional schema-checked output, usage and the whole transcript. The other exports format results, stream events as JSON lines and convert transcripts to provider message formats. With ZIDANE_DEBUG set, prompt-cache breaks are logged to stderr; include cache in the value for a line every turn. See Headless and CI.

TypeScript
import { runHeadless } from 'zidane/headless'
import { anthropic } from 'zidane/providers/anthropic'

const result = await runHeadless({
  prompt: 'Add a /health route that returns 200.',
  provider: anthropic(),
  cwd: process.cwd(),
  maxTurns: 30,
  timeoutMs: 600_000,
})
console.log(result.status, result.finalText)

Functions#

exitCodeForHeadlessResultFunction#

Maps a run status to a process exit code: 0 for 'completed', 124 for 'timeout', 130 for 'aborted' and 1 for 'error'.

TypeScript
function exitCodeForHeadlessResult(result: HeadlessResult): number
Parameters
NameTypeDescription
resultHeadlessResult

Result of runHeadless.

number

src/headless-format.ts:110

formatHeadlessResultFunction#

Formats a headless result for output. Returns the result unchanged for 'zidane', or its transcript as provider messages for 'provider'.

TypeScript
function formatHeadlessResult(
  result: HeadlessResult,
  options: { format: HeadlessOutputFormat; providerName: string },
): FormattedHeadlessResult
Parameters
NameTypeDescription
resultHeadlessResult

Result of runHeadless.

options{ format: HeadlessOutputFormat; providerName: string }

Output format and the providerName that picks the message format.

src/headless-format.ts:186

formatHeadlessTurnEventFunction#

Formats a turn event for output. Returns the event unchanged for 'zidane', or its turn as provider messages for 'provider'.

TypeScript
function formatHeadlessTurnEvent(
  event: { index: number; turn: SessionTurn; type: 'turn' },
  options: { format: HeadlessOutputFormat; providerName: string },
): FormattedHeadlessTurnEvent
Parameters
NameTypeDescription
event{ index: number; turn: SessionTurn; type: 'turn' }

A turn event from HeadlessEvent.

options{ format: HeadlessOutputFormat; providerName: string }

Output format and the providerName that picks the message format.

src/headless-format.ts:201

formattedHeadlessTurnEventToJsonlFunction#

Serializes a formatted turn event as a newline-terminated JSON line.

TypeScript
function formattedHeadlessTurnEventToJsonl(event: FormattedHeadlessTurnEvent): string
Parameters
NameTypeDescription
eventFormattedHeadlessTurnEvent

Event from formatHeadlessTurnEvent.

string

src/headless-format.ts:149

headlessEventToJsonlFunction#

Serialize one event as a newline-terminated JSON line (stream-json).

TypeScript
function headlessEventToJsonl(event: HeadlessEvent): string
Parameters
NameTypeDescription
eventHeadlessEvent

Event to serialize.

string

src/headless-format.ts:140

installHeadlessEventAdapterFunction#

Converts agent hook events into HeadlessEvents and passes them to onEvent. Errors thrown by onEvent are swallowed.

TypeScript
function installHeadlessEventAdapter(
  hooks: Hookable<AgentHooks>,
  onEvent: (event: HeadlessEvent) => void,
): () => void
Parameters
NameTypeDescription
hooksHookable<AgentHooks>

Agent hook bus to listen on.

onEvent(event: HeadlessEvent) => void

Called with each event.

() => void

A function that removes the listeners.

src/headless-format.ts:428

providerTranscriptFormatForProviderFunction#

Picks the transcript format for a provider: 'anthropic' for the Anthropic provider, 'openai' for every other.

TypeScript
function providerTranscriptFormatForProvider(
  providerName: string,
): ProviderTranscriptFormat
Parameters
NameTypeDescription
providerNamestring

Name of the provider, as in Provider.name.

src/headless-format.ts:158

runHeadlessFunction#

Run a prompt to completion, headless, and return a single serializable HeadlessResult. Safe to call concurrently for parallel rollouts — each call builds its own agent + session and tears them down in finally.

TypeScript
function runHeadless(opts: HeadlessOptions): Promise<HeadlessResult>
Parameters
NameTypeDescription
optsHeadlessOptions

Prompt, provider and run limits. prompt and provider are required.

Promise<HeadlessResult>

The run result. Errors from the agent loop, aborts and timeouts are reported in status and error instead of being thrown.

Example
~~~ts
import { exitCodeForHeadlessResult, runHeadless } from 'zidane/headless'
import { anthropic } from 'zidane/providers/anthropic'

const result = await runHeadless({
  prompt: 'List the TypeScript files in this directory.',
  provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }),
  cwd: process.cwd(),
  maxTurns: 10,
  timeoutMs: 120_000,
})

console.log(result.status, result.finalText)
process.exitCode = exitCodeForHeadlessResult(result)
~~~

src/headless.ts:153

transcriptToOpenAIMessagesFunction#

Convert raw session.turns into standard OpenAI chat-completion messages: assistant turns carry tool_calls, and each tool_result becomes its own role: 'tool' message. This is the drop-in shape for an SFT renderer — unlike toOpenAI (session/messages.ts), which emits an internal _tag envelope meant for re-sending to a provider, not for training data.

System notes become <system-reminder> text; a note without text is dropped.

Fails closed on corrupt raw turns instead of fabricating tool results; silent placeholders would create structurally-valid but semantically-poisoned SFT examples.

TypeScript
function transcriptToOpenAIMessages(
  turns: SessionTurn[],
  options?: { strictToolPairing?: boolean },
): OpenAIChatMessage[]
Parameters
NameTypeDefaultDescription
turnsSessionTurn[]

Session turns, for example result.transcript.

options?{ strictToolPairing?: boolean }{}

strictToolPairing (default true) throws when the turns need tool pairing repairs. Set it to false to convert them as they are.

src/headless-format.ts:248

transcriptToProviderMessagesFunction#

Converts session turns to the message format of a provider. Non-wire turns are dropped for Anthropic. The OpenAI path does not enforce tool pairing. System notes become <system-reminder> text in both formats.

TypeScript
function transcriptToProviderMessages(
  turns: SessionTurn[],
  providerName: string,
): unknown[]
Parameters
NameTypeDescription
turnsSessionTurn[]

Session turns, for example result.transcript.

providerNamestring

Name of the provider, as in Provider.name.

unknown[]

src/headless-format.ts:168

Interfaces#

HeadlessErrorInfoInterface#

Error that ended a headless run.

TypeScript
interface HeadlessErrorInfo
HeadlessErrorInfo properties
NameTypeDescription
messagestring

Error message.

typestring

Typed-error class name (AgentAbortedError, AgentContextExceededError, …).

src/headless-format.ts:49

HeadlessOptionsInterface#

Options for runHeadless.

TypeScript
interface HeadlessOptions
HeadlessOptions properties
NameTypeDescription
behavior?AgentBehavior

Agent behavior defaults for this headless run. Top-level shortcuts below override matching fields.

cwd?string

Working directory for the default process context (ignored if execution is set).

durable?DurableRuntime | DurableOptions

Durable runtime the session is stored on (createAgent({ durable })), or it with onInterrupted: runs keep checkpoints.

execution?ExecutionContext

Execution context. Defaults to a process context rooted at cwd.

extensions?ExtensionRegistry

Pre-built extension registry folded into the agent via createAgent (build it with buildExtensionRegistry(defs.map(inlineLoadedExtension))). Contributes the registry's tools / hooks / mcpServers / skills / system-fragments / subagents, with the explicit options above winning on collision. Because hooks fold through createAgent, a headless run with extensions also gets extension hooks (e.g. a tool:transform that rewrites results), not just tools.

includeThinking?boolean

Keep thinking blocks in result.transcript (default true).

maxTokens?number

Maximum output tokens per turn. Sets behavior.maxTokens.

maxTurns?number

Maximum number of loop turns. Sets behavior.maxTurns.

mcpServers?McpServerConfig[]

MCP servers to connect and expose as tools.

model?string

Model id. Defaults to the provider's default model.

onEvent?(event: HeadlessEvent) => void

Live event callback — the in-process stream-json equivalent.

preset?Preset

Base Preset for the agent — spread in place of the default basic preset (use it to swap the system prompt / tool set wholesale). The top-level shortcuts above (system, behavior, tools, mcpServers, skills) still override matching preset fields.

promptstring | PromptPart[]

User prompt — plain string or multimodal PromptPart[].

providerProvider

Built provider (e.g. local(), openaiCompat(...), anthropic(...)).

resumeRunId?string

Continue this checkpoint run of session (agent.resume) before running prompt. Needs durable.

schema?Record<string, unknown>

JSON Schema for structured-output enforcement → populates result.output.

session?Session

Reuse / resume an existing session.

signal?AbortSignal

External abort signal — chained with the internal timeout controller.

skills?SkillsConfig

Skills configuration.

store?SessionStore

Session store for a fresh session (defaults to an in-memory store).

subagents?SubagentRegistry

Subagents folded into the preset's spawn tool, filling gaps only (an agent the preset or an extension already defines wins). The CLI passes builtin + markdown agents from loadMarkdownAgents.

system?string

Override the preset system prompt for this run.

thinking?'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive'

Thinking effort for the run. Defaults to 'off'.

timeoutMs?number

Wall-clock cap; on expiry the run is aborted and status becomes 'timeout'.

tools?Record<string, ToolDef>

Tool overrides. Omit to use the basic preset's tools.

src/headless.ts:54

HeadlessResultInterface#

Strictly JSON-serializable postmortem of one headless run. Everything an RL reward function needs: the final answer (finalText), the verifiable structured output (output, present iff a schema was set), usage/turns, and the lossless transcript (the SFT training data).

TypeScript
interface HeadlessResult
HeadlessResult properties
NameTypeDescription
durationMsnumber

Wall-clock duration of the run in milliseconds.

error?HeadlessErrorInfo

Error that ended the run. Absent when it did not throw.

finalTextstring

Concatenated text of the last assistant turn that produced any text.

finishReason?TurnFinishReason

Finish reason of the final turn that reported one.

numToolCallsnumber

Total tool_call blocks across the whole transcript.

output?Record<string, unknown>

Schema-enforced structured output (only when opts.schema is set).

sessionIdstring

Id of the session the run used.

statusHeadlessStatus

How the run ended.

summary?RunSummary

Incident postmortem (errors, gate blocks, budget events) via run-summary.

transcriptSessionTurn[]

Lossless transcript — raw session.turns. Thinking stripped when includeThinking: false.

turnsnumber

Number of turns of the run's own loop.

usageHeadlessUsage

Token and cost totals, including child runs.

src/headless-format.ts:62

HeadlessUsageInterface#

Token and cost totals of a headless run.

TypeScript
interface HeadlessUsage
HeadlessUsage properties
NameTypeDescription
cacheCreationnumber

Tokens written to the prompt cache.

cacheReadnumber

Tokens read from the prompt cache.

cost?number

Cumulative USD cost when the provider/registry could price the run.

inputnumber

Input tokens, excluding cache tokens.

outputnumber

Output tokens.

src/headless-format.ts:35

OpenAIChatMessageInterface#

Chat-completion message in the OpenAI wire format.

TypeScript
interface OpenAIChatMessage
OpenAIChatMessage properties
NameTypeDescription
contentstring | OpenAIChatContentPart[] | null

Message text, null for a tool-call-only assistant message, or a list of content parts.

role'system' | 'tool' | 'user' | 'assistant'

Author of the message.

tool_call_id?string

Id of the tool call a tool message answers.

tool_calls?{ function: { arguments: string; name: string }; id: string; type: 'function' }[]

Tool calls requested by an assistant message.

src/headless-format.ts:214

Types#

FormattedHeadlessResultType#

Result of formatHeadlessResult: the HeadlessResult itself, or the transcript as an array of provider messages.

TypeScript
type FormattedHeadlessResult = HeadlessResult | unknown[]

src/headless-format.ts:96

FormattedHeadlessTurnEventType#

Result of formatHeadlessTurnEvent: the turn event itself, or its turn as an array of provider messages.

TypeScript
type FormattedHeadlessTurnEvent = Extract<HeadlessEvent, { type: 'turn' }> | unknown[]

src/headless-format.ts:101

HeadlessEventType#

Live event union — the in-process equivalent of a stream-json line. Every member is JSON-serializable; render to JSONL with headlessEventToJsonl.

TypeScript
type HeadlessEvent =
  | { provider?: string; runId: string; type: 'start' }
  | { delta: string; type: 'text' }
  | { delta: string; type: 'thinking' }
  | { callId: string; input: Record<string, unknown>; name: string; type: 'tool_call' }
  | { callId: string; isError: boolean; name: string; output: string; type: 'tool_result' }
  | { index: number; turn: SessionTurn; type: 'turn' }
  | { event: 'before' | 'complete' | 'error'; id: string; info?: Record<string, unknown>; type: 'spawn' }
  | { errorType?: string; message: string; type: 'error' }
  | { result: HeadlessResult; type: 'result' }

src/headless-format.ts:124

HeadlessOutputFormatType#

Output shape for transcripts: 'zidane' keeps zidane turns, 'provider' converts them to the provider's message format.

TypeScript
type HeadlessOutputFormat = 'zidane' | 'provider'

src/headless-format.ts:90

HeadlessStatusType#

How a headless run ended. 'timeout' means timeoutMs expired.

TypeScript
type HeadlessStatus = 'completed' | 'aborted' | 'error' | 'timeout'

src/headless-format.ts:32

OpenAIChatContentPartType#

One part of a multimodal OpenAI chat message.

TypeScript
type OpenAIChatContentPart =
  | { text: string; type: 'text' }
  | { image_url: { url: string }; type: 'image_url' }
  | { input_audio: { data: string; format: string }; type: 'input_audio' }
  | { type: 'video_url'; video_url: { url: string } }

src/headless-format.ts:226

ProviderTranscriptFormatType#

Provider message format a transcript can be converted to.

TypeScript
type ProviderTranscriptFormat = 'anthropic' | 'openai'

src/headless-format.ts:93