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.
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'.
function exitCodeForHeadlessResult(result: HeadlessResult): numberParameters
| Name | Type | Description |
|---|---|---|
| result | HeadlessResult | Result of |
Returns
numberformatHeadlessResultFunction#
Formats a headless result for output. Returns the result unchanged for 'zidane', or its transcript as provider messages for 'provider'.
function formatHeadlessResult(
result: HeadlessResult,
options: { format: HeadlessOutputFormat; providerName: string },
): FormattedHeadlessResultParameters
| Name | Type | Description |
|---|---|---|
| result | HeadlessResult | Result of |
| options | { format: HeadlessOutputFormat; providerName: string } | Output |
Returns
formatHeadlessTurnEventFunction#
Formats a turn event for output. Returns the event unchanged for 'zidane', or its turn as provider messages for 'provider'.
function formatHeadlessTurnEvent(
event: { index: number; turn: SessionTurn; type: 'turn' },
options: { format: HeadlessOutputFormat; providerName: string },
): FormattedHeadlessTurnEventParameters
| Name | Type | Description |
|---|---|---|
| event | { index: number; turn: SessionTurn; type: 'turn' } | A |
| options | { format: HeadlessOutputFormat; providerName: string } | Output |
Returns
formattedHeadlessTurnEventToJsonlFunction#
Serializes a formatted turn event as a newline-terminated JSON line.
function formattedHeadlessTurnEventToJsonl(event: FormattedHeadlessTurnEvent): stringParameters
| Name | Type | Description |
|---|---|---|
| event | FormattedHeadlessTurnEvent | Event from |
Returns
stringheadlessEventToJsonlFunction#
Serialize one event as a newline-terminated JSON line (stream-json).
function headlessEventToJsonl(event: HeadlessEvent): stringParameters
| Name | Type | Description |
|---|---|---|
| event | HeadlessEvent | Event to serialize. |
Returns
stringinstallHeadlessEventAdapterFunction#
Converts agent hook events into HeadlessEvents and passes them to onEvent. Errors thrown by onEvent are swallowed.
function installHeadlessEventAdapter(
hooks: Hookable<AgentHooks>,
onEvent: (event: HeadlessEvent) => void,
): () => voidParameters
| Name | Type | Description |
|---|---|---|
| hooks | Hookable<AgentHooks> | Agent hook bus to listen on. |
| onEvent | (event: HeadlessEvent) => void | Called with each event. |
Returns
() => voidA function that removes the listeners.
providerTranscriptFormatForProviderFunction#
Picks the transcript format for a provider: 'anthropic' for the Anthropic provider, 'openai' for every other.
function providerTranscriptFormatForProvider(
providerName: string,
): ProviderTranscriptFormatParameters
| Name | Type | Description |
|---|---|---|
| providerName | string | Name of the provider, as in |
Returns
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.
function runHeadless(opts: HeadlessOptions): Promise<HeadlessResult>Parameters
| Name | Type | Description |
|---|---|---|
| opts | HeadlessOptions | Prompt, provider and run limits. |
Returns
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)
~~~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.
function transcriptToOpenAIMessages(
turns: SessionTurn[],
options?: { strictToolPairing?: boolean },
): OpenAIChatMessage[]Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| turns | SessionTurn[] | Session turns, for example | |
| options? | { strictToolPairing?: boolean } | {} |
|
Returns
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.
function transcriptToProviderMessages(
turns: SessionTurn[],
providerName: string,
): unknown[]Parameters
| Name | Type | Description |
|---|---|---|
| turns | SessionTurn[] | Session turns, for example |
| providerName | string | Name of the provider, as in |
Returns
unknown[]Interfaces#
HeadlessErrorInfoInterface#
Error that ended a headless run.
interface HeadlessErrorInfoProperties
| Name | Type | Description |
|---|---|---|
| message | string | Error message. |
| type | string | Typed-error class name ( |
HeadlessOptionsInterface#
Options for runHeadless.
interface HeadlessOptionsProperties
| Name | Type | Description |
|---|---|---|
| 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 |
| durable? | DurableRuntime | DurableOptions | Durable runtime the session is stored on ( |
| execution? | ExecutionContext | Execution context. Defaults to a process context rooted at |
| extensions? | ExtensionRegistry | Pre-built extension registry folded into the agent via |
| includeThinking? | boolean | Keep |
| maxTokens? | number | Maximum output tokens per turn. Sets |
| maxTurns? | number | Maximum number of loop turns. Sets |
| 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 |
| prompt | string | PromptPart[] | User prompt — plain string or multimodal |
| provider | Provider | Built provider (e.g. |
| resumeRunId? | string | Continue this checkpoint run of |
| schema? | Record<string, unknown> | JSON Schema for structured-output enforcement → populates |
| 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 |
| 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 |
| timeoutMs? | number | Wall-clock cap; on expiry the run is aborted and |
| tools? | Record<string, ToolDef> | Tool overrides. Omit to use the basic preset's tools. |
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).
interface HeadlessResultProperties
| Name | Type | Description |
|---|---|---|
| durationMs | number | Wall-clock duration of the run in milliseconds. |
| error? | HeadlessErrorInfo | Error that ended the run. Absent when it did not throw. |
| finalText | string | Concatenated text of the last assistant turn that produced any text. |
| finishReason? | TurnFinishReason | Finish reason of the final turn that reported one. |
| numToolCalls | number | Total |
| output? | Record<string, unknown> | Schema-enforced structured output (only when |
| sessionId | string | Id of the session the run used. |
| status | HeadlessStatus | How the run ended. |
| summary? | RunSummary | Incident postmortem (errors, gate blocks, budget events) via run-summary. |
| transcript | SessionTurn[] | Lossless transcript — raw |
| turns | number | Number of turns of the run's own loop. |
| usage | HeadlessUsage | Token and cost totals, including child runs. |
HeadlessUsageInterface#
Token and cost totals of a headless run.
interface HeadlessUsageProperties
| Name | Type | Description |
|---|---|---|
| cacheCreation | number | Tokens written to the prompt cache. |
| cacheRead | number | Tokens read from the prompt cache. |
| cost? | number | Cumulative USD cost when the provider/registry could price the run. |
| input | number | Input tokens, excluding cache tokens. |
| output | number | Output tokens. |
OpenAIChatMessageInterface#
Chat-completion message in the OpenAI wire format.
interface OpenAIChatMessageProperties
| Name | Type | Description |
|---|---|---|
| content | string | OpenAIChatContentPart[] | null | Message text, |
| role | 'system' | 'tool' | 'user' | 'assistant' | Author of the message. |
| tool_call_id? | string | Id of the tool call a |
| tool_calls? | { function: { arguments: string; name: string }; id: string; type: 'function' }[] | Tool calls requested by an assistant message. |
Types#
FormattedHeadlessResultType#
Result of formatHeadlessResult: the HeadlessResult itself, or the transcript as an array of provider messages.
type FormattedHeadlessResult = HeadlessResult | unknown[]FormattedHeadlessTurnEventType#
Result of formatHeadlessTurnEvent: the turn event itself, or its turn as an array of provider messages.
type FormattedHeadlessTurnEvent = Extract<HeadlessEvent, { type: 'turn' }> | unknown[]HeadlessEventType#
Live event union — the in-process equivalent of a stream-json line. Every
member is JSON-serializable; render to JSONL with headlessEventToJsonl.
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' }HeadlessOutputFormatType#
Output shape for transcripts: 'zidane' keeps zidane turns, 'provider' converts them to the provider's message format.
type HeadlessOutputFormat = 'zidane' | 'provider'HeadlessStatusType#
How a headless run ended. 'timeout' means timeoutMs expired.
type HeadlessStatus = 'completed' | 'aborted' | 'error' | 'timeout'OpenAIChatContentPartType#
One part of a multimodal OpenAI chat message.
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 } }ProviderTranscriptFormatType#
Provider message format a transcript can be converted to.
type ProviderTranscriptFormat = 'anthropic' | 'openai'