Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

LLM streaming and transcripts

Streaming responses

llm_stream returns a channel that yields response chunks as they arrive. Iterate over it with a for loop:

const stream = harness.llm.stream("Tell me a story", "You are a storyteller")
for chunk in stream {
  harness.stdio.log(chunk)
}

llm_stream accepts the same options as llm_call (provider, model, max_tokens). The channel closes automatically when the response is complete. If the provider fails after emitting zero or more string chunks, the last item is the same structured error dict used by llm_call, including typed provider-stream phase, deadline, and partial fields; transport failures are never represented as a successful close.

llm_stream_call is the script-facing streaming variant of llm_call. It returns a first-class Stream of chunk dicts instead of a channel of raw strings:

const chunks = harness.llm.stream_call("Tell me a story", nil, {provider: "openai"})
for chunk in chunks {
  harness.stdio.log(chunk.visible_delta)
  if chunk.partial.contains("REFUSAL") {
    break
  }
}

Each chunk has {delta, visible_delta, partial, role, stop_reason} (the typed shape is LlmStreamChunk from std/llm/envelope). delta is the provider text delta, visible_delta and partial hide open internal <think> blocks, and the terminal chunk carries stop_reason — the same spelling as the llm_call envelope — when the provider reports one. Dropping the stream aborts the background LLM request. The existing stream option on llm_call and llm_stream_call still only controls provider transport selection; it does not change llm_call's return type.

When an app-level persona asks the model to keep a private notebook in tagged text, use std/agent/stream instead of filtering chunks inline:

import {agent_stream_call} from "std/agent/stream"

const result = agent_stream_call(prompt, system, {
  provider: "openai",
  model: "gpt-5-mini",
  private: {open_tag: "<secret>", close_tag: "</secret>"},
  on_delta: { delta, _event, _state -> harness.stdio.print(delta) },
})

agent_private_stream_delta(...) holds back enough suffix text to detect opening private tags split across chunks, agent_private_stream_finish(...) flushes only safe visible suffixes, and agent_stream_call(...) returns a terminal {ok, status, text, visible_text} envelope on both success and stream interruption.

When the harness runs a full agent_loop (tools, transcript, completion policy) rather than a single llm_stream_call, use the loop's own on_delta streaming seam instead of dropping to a raw stream. Each per-turn call is issued through the streaming transport, and the closure sees one delta per chunk of the assistant's visible text — fold each delta through agent_private_stream_delta from std/agent/stream inside the callback to mask a <secret> span while it renders. The callback is observational and the turn still returns a complete result, so tool dispatch is unaffected; providers that do not stream fall back to a single full-text delta:

agent_loop(harness, "summarize the diff", nil, {
  provider: "anthropic",
  model: "claude-sonnet-5",
  on_delta: { delta -> harness.stdio.print(delta) },
})

Partial deltas and usage

Streaming transports emit text deltas as soon as the provider sends them. Native tool-call streams also surface partial argument deltas in agent trace events: raw_input when the bytes parse as JSON, or raw_input_partial while the JSON object is still incomplete. Harn sends these intermediate states to live agent event subscribers and persists them for deterministic replay, including in headless sessions.

Final token usage is recorded after the provider response completes. Read it from the llm_call / agent_loop result, from harness.obs.llm_usage(), or from the workflow session usage summary shown below.

Transcript management

Harn includes transcript primitives for carrying context across calls, forks, repairs, and resumptions:

const first = harness.llm.call("Plan the work", nil, {provider: "mock"})

const second = harness.llm.call("Continue", nil, {
  provider: "mock",
  transcript: first.transcript
})

const compacted = transcript_compact(second.transcript, {
  keep_last: 4,
  summary: "Planning complete."
})

Use transcript_summarize() when you want Harn to create a fresh summary with an LLM, or transcript_compact() when you want the runtime compaction engine outside the agent_loop path. transcript_compact() accepts the same CompactionPolicy instruction fields as agent-loop auto-compaction, so hosts can route /compact <instructions> through one audited path.

Transcript helpers also expose the canonical event model:

const visible = transcript_render_visible(result.transcript)
const full = transcript_render_full(result.transcript)
const events = transcript_events(result.transcript)

Use these when a host app needs to render human-visible chat separately from internal execution history.

For chat/session lifecycle, std/agents now exposes a higher-level workflow session contract on top of raw transcripts and run records:

import "std/agents"

const result = task_run("Write a note", some_flow, {provider: "mock"})
const session = workflow_session(result)
const forked = workflow_session_fork(session)
const archived = workflow_session_archive(forked)
const resumed = workflow_session_resume(archived)
const persisted = workflow_session_persist(result, ".harn-runs/chat.json")
const restored = workflow_session_restore(persisted.run.persisted_path)

Each workflow session also carries a normalized usage summary copied from the underlying run record when available:

harness.stdio.log(session?.usage?.input_tokens)
harness.stdio.log(session?.usage?.output_tokens)
harness.stdio.log(session?.usage?.total_duration_ms)
harness.stdio.log(session?.usage?.call_count)

std/agents also exposes worker helpers for delegated/background orchestration: worker_request(worker), worker_result(worker), worker_provenance(worker), worker_research_questions(worker), worker_action_items(worker), worker_workflow_stages(worker), and worker_verification_steps(worker).

For durable persona handoff, prefer a typed artifact over copying the child or parent transcript forward. Use handoff(...) to normalize a structured handoff payload, handoff_artifact(...) to carry it through the workflow artifact channel, and handoff_context(...) when a receiver needs a prompt-safe summary of the transferred task/evidence/budget fields. The handoff artifact is the product; the transcript stays on the source side of the boundary.

This is the intended host integration boundary:

  • hosts persist chat tabs, titles, and durable asset files
  • Harn persists transcript/run-record/session semantics
  • hosts should prefer restoring a Harn session or transcript over inventing a parallel hidden memory format

Workflow runtime

For multi-stage orchestration, prefer the workflow runtime over product-side loop wiring. Define a helper that assembles the tools your agents will use:

fn review_tools() {
  let tools = tool_registry()
  tools = tool_define(tools, "read", "Read a file", {
    parameters: {path: {type: "string"}},
    returns: {type: "string"},
    handler: nil
  })
  tools = tool_define(tools, "edit", "Edit a file", {
    parameters: {path: {type: "string"}},
    returns: {type: "string"},
    handler: nil
  })
  tools = tool_define(tools, "run", "Run a command", {
    parameters: {command: {type: "string"}},
    returns: {type: "string"},
    handler: nil
  })
  return tools
}

const graph = workflow_graph({
  name: "review_and_repair",
  entry: "act",
  nodes: {
    act: {kind: "stage", mode: "agent", tools: review_tools()},
    verify: {kind: "verify", mode: "agent", tools: tool_select(review_tools(), ["run"])}
  },
  edges: [{from: "act", to: "verify"}]
})

const run = workflow_execute(
  "Fix the failing test and verify the change.",
  graph,
  [],
  {max_steps: 6}
)

This keeps orchestration structure, transcript policy, context policy, artifacts, and retries inside Harn instead of product code.