# LLM streaming and transcripts

> harness.llm.stream returns a channel that yields response chunks as they arrive. Iterate over it with a for loop:

Website: https://harnlang.com/llm/streaming.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

## Streaming responses

`harness.llm.stream` returns a channel that yields response chunks as they
arrive. Iterate over it with a `for` loop:

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

`harness.llm.stream` accepts the same options as `harness.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 `harness.llm.call`, including typed
provider-stream `phase`, `deadline`, and `partial` fields; transport failures
are never represented as a successful close.

`harness.llm.stream_call` is the script-facing streaming variant of `harness.llm.call`.
It returns a first-class `Stream` of chunk dicts instead of a channel of
raw strings:

```harn
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 `harness.llm.call` envelope — when the
provider reports one. Dropping the stream
aborts the background LLM request. The existing `stream` option on
`harness.llm.call` and `harness.llm.stream_call` still only controls provider transport
selection; it does not change `harness.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:

```harn,ignore
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 `harness.llm.stream_call`, use the loop's own
[`on_delta` streaming seam](agent_loop.md#streaming-visible-text-deltas) 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:

```harn,ignore
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 every intermediate state to live agent
event subscribers. Durable agent-event sinks retain the first cumulative
argument snapshot, successive power-of-two prefix-growth checkpoints, and every
settled or anomalous transition. This keeps replay and crash evidence available
without rewriting the same growing argument body on every transport tick.

The durable projection depends only on event order, never on whether a live
subscriber happens to be attached. A prior subscriber-gated policy made the
same execution produce different evidence under headless and interactive hosts;
that topology-dependent design is intentionally not used.

Final token usage is recorded after the provider response completes. Read it
from the `harness.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:

```harn
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."
})
```

Pass `strategy: "llm"` when you want Harn to create a fresh summary with an
LLM, and use `transcript_compact()` whenever 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:

```harn
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:

```harn
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:

```harn
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:

```harn
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.

---

## Read next

- [LLM ensemble helpers](https://harnlang.com/llm/ensemble.md)
- [Transcript projection](https://harnlang.com/llm/transcript-projection.md)
