npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ai-sdk-tool/headless

v3.1.1

Published

A non-interactive, JSONL event-streaming runtime for agent sessions. Instead of rendering to a terminal, it writes structured events to stdout — one JSON object per line. Suitable for CI/CD pipelines, benchmarks, and any programmatic consumer that needs a

Readme

@ai-sdk-tool/headless

A non-interactive, JSONL event-streaming runtime for agent sessions. Instead of rendering to a terminal, it writes structured events to stdout — one JSON object per line. Suitable for CI/CD pipelines, benchmarks, and any programmatic consumer that needs a machine-readable transcript.

Installation

pnpm add @ai-sdk-tool/headless
# or
npm install @ai-sdk-tool/headless

Peer dependencies:

pnpm add @ai-sdk-tool/harness ai

Quick Start

import { createAgent, CheckpointHistory, SessionManager } from "@ai-sdk-tool/harness";
import { runHeadless, registerSignalHandlers } from "@ai-sdk-tool/headless";
import { openai } from "@ai-sdk/openai";

const session = new SessionManager();
const sessionId = session.initialize();

const messageHistory = new CheckpointHistory();
const agent = await createAgent({
  model: openai("gpt-4o"),
  instructions: "You are a helpful assistant.",
});

// Register signal handlers before any async work
registerSignalHandlers({
  onCleanup: () => {},
  onFatalCleanup: (exitCode) => process.exit(exitCode),
});

// Run the agent — emits JSONL events to stdout
await runHeadless({
  agent,
  initialUserMessage: {
    content: "Fix the type error in src/index.ts",
  },
  modelId: "gpt-4o",
  sessionId,
  messageHistory,
});

Example output (JSONL stream):

{"type":"metadata","timestamp":"2026-04-03T10:00:00.000Z","session_id":"ses-abc123","agent":{"name":"code-editing-agent","version":"1.0.0","model_name":"gpt-4o"}}
{"type":"step","step_id":1,"timestamp":"2026-04-03T10:00:00.000Z","source":"user","message":"Fix the type error in src/index.ts"}
{"type":"step","step_id":2,"timestamp":"2026-04-03T10:00:01.000Z","source":"agent","message":"I'll inspect the file.","model_name":"gpt-4o","tool_calls":[{"tool_call_id":"call_1","function_name":"read_file","arguments":{"path":"src/index.ts"}}],"observation":{"results":[{"source_call_id":"call_1","content":"{\"stdout\":\"...file contents...\"}"}]},"metrics":{"prompt_tokens":520,"completion_tokens":80}}
{"type":"step","step_id":3,"timestamp":"2026-04-03T10:00:03.000Z","source":"agent","message":"I found the issue and fixed it.","model_name":"gpt-4o","metrics":{"prompt_tokens":410,"completion_tokens":65}}

API Reference

runHeadless(config)

Runs the agent loop, emitting JSONL events for each turn. Continues looping while the agent makes tool calls. Optionally continues after the main loop if there are incomplete TODO items.

import { runHeadless } from "@ai-sdk-tool/headless";

await runHeadless({
  agent,           // RunnableAgent — required
  sessionId,       // string — becomes metadata.session_id
  messageHistory,  // CheckpointHistory from @ai-sdk-tool/harness
  modelId,         // string — current model ID for metadata and agent steps
  initialUserMessage, // optional initial user turn
  maxIterations,   // optional number — safety cap on loop iterations
  maxTodoReminders, // optional number — cap follow-up TODO reminder turns
  measureUsage,    // optional async usage probe for tighter budgeting
  emitEvent,       // optional (event: TrajectoryEvent) => void — defaults to stdout JSONL
  circuitBreaker,  // optional compaction circuit breaker
  compactionCallbacks, // optional compaction lifecycle callbacks
  disableCompaction, // optional boolean to skip automatic compaction
  onBeforeTurn,    // optional async hook before each stream call
  onInterrupt,     // optional (event: InterruptEvent) => void — caller-abort lifecycle hook
  onTurnComplete,  // optional receives messages, usage, snapshot, finishReason
  onTodoReminder,  // optional () => Promise<{ hasReminder, message }> — TODO continuation
  shouldContinue,  // optional override for the default tool-loop continuation gate
  streamTimeoutMs, // optional stream response timeout override
  atifOutputPath,  // optional path for writing trajectory.json directly
});

Config fields:

| Field | Type | Required | Description | |-------|------|----------|-------------| | agent | RunnableAgent | yes | Agent with a stream(opts) method | | sessionId | string | yes | Unique ID emitted once as metadata.session_id | | messageHistory | CheckpointHistory | yes | Conversation history — read and written during the loop | | modelId | string | yes | Current model ID for metadata and step events | | initialUserMessage | { content, eventContent?, originalContent? } | no | Bootstraps the first user turn without mutating history manually | | maxIterations | number | no | Total iteration budget across the entire headless run, including TODO reminder turns; emits an error event and stops if exceeded | | maxTodoReminders | number | no | Caps TODO reminder follow-up turns without lowering the main loop iteration budget | | measureUsage | (messages) => Promise<UsageMeasurement \| null> | no | Optional usage probe used to tighten the next stream budget | | emitEvent | (event) => void | no | Custom event sink; defaults to console.log(JSON.stringify(event)) | | circuitBreaker | CompactionCircuitBreaker | no | Circuit breaker controlling automatic compaction retries | | compactionCallbacks | CompactionOrchestratorCallbacks | no | Lifecycle callbacks for compaction events | | disableCompaction | boolean | no | Disables automatic compaction entirely | | onBeforeTurn | (phase) => BeforeTurnResult \| Promise<BeforeTurnResult \| undefined> \| undefined | no | Runs before each stream call and can override stream options | | onInterrupt | (event: InterruptEvent) => void | no | Called when the caller aborts the active run | | onTurnComplete | (messages, usage?, snapshot?, finishReason?) => void \| Promise<void> | no | Runs after each completed turn with usage and snapshot metadata | | onTodoReminder | () => Promise<{ hasReminder, message }> | no | Called after the main loop; if hasReminder is true, sends message as a user turn and continues | | shouldContinue | (finishReason) => boolean | no | Overrides the default continuation predicate | | streamTimeoutMs | number | no | Overrides the default stream response timeout | | atifOutputPath | string | no | Writes the collected ATIF trajectory JSON to disk after the run |


emitEvent(event)

Writes a single TrajectoryEvent as a JSONL line to stdout. This is the default event sink used by runHeadless.

import { emitEvent } from "@ai-sdk-tool/headless";

emitEvent({
  type: "interrupt",
  reason: "caller-abort",
  timestamp: new Date().toISOString(),
});
// stdout: {"type":"interrupt","reason":"caller-abort","timestamp":"..."}

registerSignalHandlers(config)

Registers process signal handlers for graceful shutdown. Handles SIGINT, SIGTERM, SIGHUP, SIGQUIT, uncaughtException, and unhandledRejection.

import { registerSignalHandlers } from "@ai-sdk-tool/headless";

registerSignalHandlers({
  onCleanup: () => {
    // Called on process exit — flush buffers, write final state
    flushOutputBuffer();
  },
  onFatalCleanup: (exitCode) => {
    // Must call process.exit — typed as `never` to enforce this
    process.exit(exitCode);
  },
});

Exit codes by signal:

| Signal | Exit code | |--------|-----------| | SIGINT | 0 | | SIGTERM | 143 | | SIGHUP | 129 | | SIGQUIT | 131 | | uncaughtException / unhandledRejection | 1 |

Important: Uses process.once — calling registerSignalHandlers twice for the same signal is a bug. Call it once at startup.


JSONL Event Types

The runner streams an internal JSONL event protocol documented in packages/headless/AGENTS.md. The persisted trajectory.json produced by TrajectoryCollector conforms to Harbor's ATIF-v1.4 schema (https://www.harborframework.com/docs/agents/trajectory-format). Lifecycle annotations on the JSONL stream split into two categories: approval, compaction, and interrupt are persisted into trajectory.extra.* buckets (not as steps[*].source values); turn-start and error are transient and stay JSONL-only.

Event overview

| Type | Source | Description | |------|--------|-------------| | metadata | system | Emitted once at start with session_id and agent info | | step | user | A user message step | | step | agent | An agent response, including text, reasoning, tool calls, and optional observations | | approval | system | Structured tool approval lifecycle (pending, approved, denied) | | compaction | system | Lifecycle event for history compaction | | error | system | Fatal error or iteration-limit event | | interrupt | system | Intentional caller interruption (caller-abort) | | turn-start | system | Lifecycle annotation emitted right after agent.stream() is dispatched, before the first chunk arrives |

metadata

{
  type: "metadata",
  timestamp: string,
  session_id: string,
  agent: {
    name: string,
    version: string,
    model_name: string,
  },
}

step

{
  type: "step",
  step_id: number,
  timestamp: string,
  source: "user" | "agent",
  message?: string,
  model_name?: string,
  tool_calls?: Array<{
    tool_call_id: string,
    function_name: string,
    arguments: Record<string, unknown>,
  }>,
  observation?: {
    results: Array<{
      source_call_id: string,
      content: string,
    }>,
  },
  metrics?: {
    prompt_tokens: number,
    completion_tokens: number,
    cached_tokens?: number,
    cost_usd?: number,
  },
}

compaction

{
  type: "compaction",
  timestamp: string,
  event: "start" | "complete" | "blocking_change",
  tokensBefore: number,
  tokensAfter?: number,
  strategy?: string,
  durationMs?: number,
  blocking?: boolean,
  reason?: string,
}

approval

{
  type: "approval",
  timestamp: string,
  state: "pending" | "approved" | "denied",
  toolCallId?: string,
  toolName?: string,
  reason?: string,
  providerExecuted?: boolean,
}

error

{
  type: "error",
  timestamp: string,
  error: string,
}

interrupt

{
  type: "interrupt",
  timestamp: string,
  reason: "caller-abort",
}

turn-start

{
  type: "turn-start",
  timestamp: string,
  phase: "new-turn" | "intermediate-step",
}

TypeScript types

import type {
  TrajectoryEvent,
  StepEvent,
  UserStepEvent,
  AgentStepEvent,
  ApprovalEvent,
  MetadataEvent,
  CompactionEvent,
  ErrorEvent,
  InterruptEvent,
  TurnStartEvent,
} from "@ai-sdk-tool/headless";

Note: pre-ATIF examples that used standalone user, assistant, tool_call, and tool_result event types are obsolete. Tool results are now carried in step.observation.results, and session_id appears only in the initial metadata event.


Advanced Usage

Custom event sink (write to file)

import { runHeadless, type TrajectoryEvent } from "@ai-sdk-tool/headless";
import { appendFileSync } from "node:fs";

const logFile = "output.jsonl";

await runHeadless({
  agent,
  sessionId,
  messageHistory,
  modelId,
  emitEvent: (event: TrajectoryEvent) => {
    appendFileSync(logFile, JSON.stringify(event) + "\n");
  },
});

With TODO continuation

Keeps the agent running until all TODO items are complete.

import { TodoContinuation } from "@ai-sdk-tool/harness";
import { runHeadless } from "@ai-sdk-tool/headless";

const todo = new TodoContinuation({
  todoDir: ".sisyphus/todos",
  sessionId,
});

await runHeadless({
  agent,
  sessionId,
  messageHistory,
  modelId,
  onTodoReminder: () => todo.getReminder(),
});

Iteration safety cap

await runHeadless({
  agent,
  sessionId,
  messageHistory,
  modelId,
  maxIterations: 50,  // emits an error event and stops after 50 iterations
});

Parsing JSONL output

# Run headless and filter only agent step events
pnpm run headless -- "Fix the bug" | grep '"type":"step"' | jq 'select(.source == "agent") | .message'
// Parse events from a JSONL event log
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
import type { TrajectoryEvent } from "@ai-sdk-tool/headless";

const rl = createInterface({ input: createReadStream("output.jsonl") });

for await (const line of rl) {
  const event = JSON.parse(line) as TrajectoryEvent;
  if (event.type === "step" && event.source === "agent") {
    console.log("Assistant:", event.message);
  }
}

License

MIT