Little documentation
Guides and APIRunning

Inspect Runs

The event log is the only inspection surface in v0.1.0-alpha.

Little Workflow: published on npm as little-workflow@0.2.0-alpha.1 (2026-10-06). Sections labeled UNRELEASED describe source-only changes. Release status

Alpha is artifact-first and event-driven. The runtime stores facts in the event log and payloads on the filesystem. Inspection in alpha means dumping the event log; the polished inspector (list, timeline, stream, per-step inspect, artifact CLI) is planned post-alpha (see the Roadmap).

Event Vocabulary

Alpha emits two families of events:

  • LWIR events — track workflow compilation, step scheduling, and step lifecycle. Present in all runs.
  • Harness session events — track every model call and tool call inside a planner, orchestrator, or worker harness session. New in this milestone.

Both families write to the same Local World event store, keyed by runId and per-run sequence. For pipeline runs (array of workflows), the orchestrator has its own runId; each sub-workflow run has a separate runId reserved in harness.tool_call.started.payload.args.subRunId and returned as harness.tool_call.succeeded.payload.result.runId.

LWIR Events

OrchestrationRequested
PlannerStarted
PlannerDraftedWorkflow
WorkflowValidationFailed
WorkflowValidationSucceeded
WorkflowVersionRegistered
RunStarted
StepScheduled
StepAttemptStarted
StepCompleted
StepFailed
ParallelGroupStarted
ParallelBranchScheduled
ParallelBranchCompleted
ParallelBranchFailed
ParallelGroupCompleted
ParallelGroupFailed
OuterLoopCycleCompleted
RunCompleted
RunFailed

OuterLoopCycleCompleted is appended to the current cycle's event log when supervise() returns. It is present only in outer-loop runs (maxOuterCycles > 1). Payload: { outerLoopId, cycleNumber, supervise: { kind, finalOutput?, promptNote? } }. This event is the cycle commit point that makes inter-cycle replay decidable.

OrchestrationRequested records request, input, workflow-definition, output, and capability-manifest hashes. Planner lifecycle events (PlannerStarted, PlannerDraftedWorkflow, WorkflowValidationFailed/Succeeded) are persisted before WorkflowVersionRegistered.

Harness Session Events

These events wrap every model call and tool call inside a harness session. They are present in runs where the planner, orchestrator, or worker uses the harness (all runs in this milestone).

Session lifecycle

// Emitted at the start of every harness session (planner, orchestrator, worker, fixer).
// The manifest records the session's full capability surface for drift detection on resume.
type SessionStartedEvent = {
  type: "harness.session.started";
  payload: {
    runId: string;
    role: "planner" | "orchestrator" | "worker.ai-generate" | "worker.code-run" | "worker.tool-call" | "fixer";
    task: { kind: "plan" | "orchestrate" | "execute_step" | "fix_step" };
    manifest: OrchestratorManifest | PlannerManifest | WorkerManifest;
    manifestHash: string;        // sha256 of canonicalized manifest; compared on resume
    parentRunId?: string;        // present for sub-workflow runs inside a pipeline
  };
};

type SessionCompletedEvent = {
  type: "harness.session.completed";
  payload: {
    runId: string;
    output?: unknown;
    // Token-only rollup for this session. No dollar figure: the harness cannot see the
    // model registry, and a `costUsd` on any recorded usage payload is rejected.
    usage: {
      inputTokens: number;
      outputTokens: number;
      cachedInputTokens: number;
      reasoningTokens: number;
    };
  };
};

type SessionFailedEvent = {
  type: "harness.session.failed";
  payload: {
    runId: string;
    error: { message: string; causeCode: string; stack?: string };
  };
};

Agent-loop events

Emitted for harness sessions that run an agent loop: plan, orchestrate, execute_step (ai.generate), fix_step.

type ModelCalledEvent = {
  type: "harness.model.called";
  payload: {
    turn: number;                // 1-indexed
    promptHash: string;          // sha256 of canonicalized {system, messages, tools}; used for replay memoization
    request: {
      model: string;             // slot id, e.g., "gpt-4o-mini"
      messages: unknown[];       // large content stored as artifact refs
      tools: { toolName: string; schemaHash: string }[];
    };
  };
};

type ModelRespondedEvent = {
  type: "harness.model.responded";
  payload: {
    turn: number;
    callId: string;
    stepPath?: string;                 // stamped by the runtime; absent for planner/orchestrator
    model: { provider: string; modelId: string };  // what the call is priced against
    response: {
      text?: string;
      toolCalls?: { toolName: string; callId: string; args: unknown }[];
      usage: {
        inputTokens: number;
        outputTokens: number;
        cachedInputTokens?: number;    // subset of inputTokens
        reasoningTokens?: number;      // subset of outputTokens
      };
    };
  };
};

harness.model.responded is the atomic, priceable record of one model call, and it is the only source a run's dollar total is built from. Two details make that work:

  • Model identity rides the payload, not the trace metadata, because only the payload is written to the durable log. Without it a call cannot be matched to a registry rate.
  • stepPath is stamped by the runtime as the event is bridged into the log, which is what makes per-step attribution correct under parallel branches. Sessions with no owning step (planner, orchestrator) leave it absent and land in the report's (unattributed) row.

Usage on every event is token-only. Cost is derived downstream, once, at materialization.

Tool-call events

Emitted for all roles. The caller field distinguishes model-driven calls, runtime-scripted calls, and code-driven calls.

type ToolCallStartedEvent = {
  type: "harness.tool_call.started";
  payload: {
    turn?: number;               // present when caller="model"; absent otherwise
    callId: string;
    caller: "model" | "code";
                                 // "model" = provider returned a tool call from the agent loop
                                 // "code" = a code.run step body invoked a registered tool directly
    toolName: string;
    args: unknown;
  };
};

type ToolCallSucceededEvent = {
  type: "harness.tool_call.succeeded";
  payload: {
    callId: string;
    result: unknown;             // for start_workflow/run_workflow: includes runId
    durationMs: number;
  };
};

type ToolCallFailedEvent = {
  type: "harness.tool_call.failed";
  payload: {
    callId: string;
    error: { message: string; causeCode: string };
    durationMs: number;
  };
};

For start_workflow and run_workflow tool calls, harness.tool_call.started.payload.args.subRunId is reserved before the sub-run starts, and harness.tool_call.succeeded.payload.result.runId is the runId of the spawned sub-workflow. Use this to navigate from an orchestrator's event log to a sub-run's event log.

Direct-execute step events

Emitted for code.run, tool.call, and decision steps (no model in the loop).

type ExecuteStepStartedEvent = {
  type: "harness.execute_step.started";
  payload: {
    stepPath: string;
    stepKind: "code.run" | "tool.call" | "decision";
    inputHash: string;
  };
};

type ExecuteStepSucceededEvent = {
  type: "harness.execute_step.succeeded";
  payload: {
    stepPath: string;
    outputHash: string;
    durationMs: number;
  };
};

For code.run steps, any tools the code body invokes also emit harness.tool_call.started, harness.tool_call.succeeded, harness.tool_call.failed with caller: "code" between harness.execute_step.started and harness.execute_step.succeeded.

Event Sequencing by Task Kind

Task kindEvent sequence
planharness.session.started → (harness.model.called → harness.model.responded → harness.tool_call.* × tools per turn) × turns → harness.session.completed
orchestrateharness.session.started → (harness.model.called → harness.model.responded → harness.tool_call.* × tools per turn) × turns → harness.session.completed
execute_step (ai.generate)harness.session.started → harness.execute_step.started → harness.model.called → harness.model.responded → harness.execute_step.succeeded → harness.session.completed (one model call, no tool calls)
execute_step (code.run)harness.session.started → harness.execute_step.started → (harness.tool_call.* × tools invoked by code) → harness.execute_step.succeeded → harness.session.completed
execute_step (tool.call)harness.session.started → harness.execute_step.started → harness.tool_call.* (single) → harness.execute_step.succeeded → harness.session.completed
execute_step (decision)harness.session.started → harness.execute_step.started → harness.execute_step.succeeded → harness.session.completed
fix_stepharness.session.started → agent-loop events × turns → harness.session.completed

Example: Event log shape

{ "type": "harness.session.started", "payload": { "runId": "run_abc", "role": "orchestrator", "task": { "kind": "orchestrate" }, "manifest": { "harnessId": "workflowHarness@1.0.0", "orchestratorModelSlotId": "gpt-5", "maxConcurrentSubRuns": 10 }, "manifestHash": "sha256:orch789" } }
{ "type": "harness.model.called", "payload": { "turn": 1, "promptHash": "sha256:p1", "request": { "model": "gpt-5", "messages": [], "tools": [{ "toolName": "plan_workflow", "schemaHash": "sha256:s1" }] } } }
{ "type": "harness.model.responded", "payload": { "turn": 1, "callId": "m1", "model": { "provider": "openai", "modelId": "gpt-4o-mini" }, "response": { "toolCalls": [{ "toolName": "start_workflow", "callId": "c1", "args": { "workflowId": "candidate.review", "input": { "candidateId": "c_1" } } }], "usage": { "inputTokens": 800, "outputTokens": 120 } } } }
{ "type": "harness.tool_call.started", "payload": { "turn": 1, "callId": "c1", "caller": "model", "toolName": "start_workflow", "args": { "workflowId": "candidate.review", "input": { "candidateId": "c_1" }, "subRunId": "run_def" } } }
{ "type": "harness.tool_call.succeeded", "payload": { "callId": "c1", "result": { "workflowVersionId": "sha256:lwir1", "runId": "run_def" }, "durationMs": 4200 } }
{ "type": "harness.session.completed", "payload": { "runId": "run_abc", "usage": { "inputTokens": 2800, "outputTokens": 450, "cachedInputTokens": 0, "reasoningTokens": 0 } } }

The harness.session.completed rollup restates model calls that were already recorded one-by-one as harness.model.responded. It exists for session-level consumers; the run materializer deliberately skips it, because summing both would double-count every call. If you aggregate the log yourself, aggregate the atomic events, not the rollup.

For large payloads (messages, args, results), the event stores an artifact ref instead of inlining the content:

{
  "type": "StepCompleted",
  "stepPath": "review-candidates.extract-profile",
  "outputRef": "artifact://art_step_output",
  "artifactRefs": ["artifact://art_resume_text"]
}

StepCompleted carries no usage or cost of its own. Per-step tokens and dollars are derived at materialization time from the harness.model.responded events stamped with that stepPath, and surface as MaterializedRunState.steps[stepPath].usage and in little report.

CLI Inspection

# All events for a run, in commit order:
little events run_123

# Filter by event type:
little events run_123 | jq 'select(.type == "harness.model.called")'

# Show only tool calls made by code (not the model):
little events run_123 | jq 'select(.type == "harness.tool_call.started" and .payload.caller == "code")'

# Show all harness session summaries (usage + status):
little events run_123 | jq 'select(.type | startswith("harness.session."))'

# Navigate to a sub-run from an orchestrator event log:
little events run_123 | jq 'select(.type == "harness.tool_call.succeeded" and .payload.result.runId != null) | .payload.result.runId'
# → "run_def"
little events run_def   # inspect the sub-run's own event log

That is the alpha event-dump surface. JSON output, one event per line, in commit order.

Cost Report

little report folds the same log into a per-step ledger of tokens, cache-hit share, and real dollars:

little report run_123            # JSON (the default)
little report run_123 --table    # human-readable fixed-width table
Run run_7f3a (completed)

STEP          IN  CACHED  CACHE%    OUT  CALLS  COST (USD)
---------  -----  ------  ------  -----  -----  ----------
classify     900       0    0.0%    120   0+1?         n/a
extract    20000   16000   80.0%  10000      1    0.007800
summarize  20000       0    0.0%  10000      1    0.009000
---------  -----  ------  ------  -----  -----  ----------
TOTAL      40900   16000   39.1%  20120   2+1?    0.016800

Note: 1 model call(s) had no registry pricing and are excluded from the cost above.

Reading the table:

  • CACHED is the part of IN served from the provider's prompt cache — a subset of the input tokens, not an addition to them. CACHE% is CACHED / IN.
  • CALLS is priced when everything on the row could be priced, and priced+unpriced? when it could not. classify above ran on a model the registry has no entry for.
  • n/a is a cost that could not be computed. It is rendered distinctly from 0.000000 on purpose: an unpriced call is not a free call.
  • <0.000001 is real money too small to show at six decimals — one gpt-4o-mini call with a single input token costs $0.00000015. It is likewise never rendered as 0.000000, so spend can't be mistaken for a step that cost nothing. The JSON output carries the exact figure.
  • 0.000000 therefore means a genuine zero.
  • The trailing note counts unpriced calls whenever there are any, so the total can never quietly under-report.

Model calls made outside any step — a planner or orchestrator session — appear as an (unattributed) row so the per-step rows and the run total always reconcile.

Automatic Retry And Replay

Automatic retry re-executes a failed step attempt inside the same run using the same WorkflowVersion and input. Alpha retry is best used for idempotent steps; production-grade dedup arrives in v0.1.0.

Replay reconstructs state from event history. It does not call models, tools, shell, code, or external APIs. Harness session events (Layer C) are replayed in addition to LWIR step events (Layer A) — committed model turns and tool calls are fast-forwarded without re-invoking the model or tool. This is what proves the kill-and-resume invariant.

The user-invoked little retry command is not part of alpha. It is planned post-alpha (see the Roadmap).

Fork (branching a run from a checkpoint with changed input, prompts, or policy) is planned post-alpha (see the Roadmap).

See Durability for the full resume model, crash scenarios, and CapabilityDriftError behavior.

On this page