streamHarness()
Stream an interactive Little Harness agent turn.
Little Harness: published on npm as little-harness@0.2.0-alpha.0 (2026-10-05). Sections labeled UNRELEASED describe source-only changes. Release status
streamHarness() runs a harness and streams the response. Use it for chat
routes, live progress, and UI experiences that should update while the agent is
working.
It should feel close to AI SDK streamText(), while also owning sessions,
files, artifacts, Persistent Dirs, and trace metadata.
Import
import { streamHarness } from "@little-stack/harness";Basic Usage
import type { UIMessage } from "ai";
import { streamHarness } from "@little-stack/harness";
export async function POST(req: Request) {
const { messages, chatId, extraBody }: {
messages: UIMessage[];
chatId: string;
extraBody: { userId: string; teamId: string };
} = await req.json();
const result = streamHarness({
harness,
messages,
session: chatId,
extraBody,
});
return result.toUIMessageStreamResponse();
}API Signature
function streamHarness<TOutput = string, TInput = unknown, TExtraBody = unknown>(
options: StreamHarnessOptions<TInput, TExtraBody, TOutput>,
): StreamHarnessResult<TOutput>;Parameters
harness
Harness
The reusable harness returned by createHarness().
streamHarness({ harness, messages });messages
UIMessage[]
Raw AI SDK UI messages. Use this for normal chat routes.
const { messages } = await req.json();
const result = streamHarness({
harness,
messages,
});When messages are provided, do not also provide type and input.
prompt
string
One user message, as plain text. Use it when the caller has a sentence rather than a chat transcript.
const result = streamHarness({
harness,
session: "loan_APP-1001",
prompt: "Pre-approve APP-1001.",
});Pass prompt or messages, not both, and not with type or input.
type
string
Open string that names a custom Input Type.
const result = streamHarness({
harness,
type: "document.review",
input: { documentId: "doc_123" },
session: "doc_123",
});If the type is registered in createHarness({ inputTypes }), Little Harness can
validate the input, convert it into model messages, and apply default output
settings. If it is not registered, the call can still run with a warning.
input
TInput
App-defined payload for type + input calls.
input: {
documentId: "doc_123",
reviewMode: "risk",
}Use messages for chat, prompt for one plain-text request, and type + input
for non-chat callers with a typed payload.
session
string | HarnessSession
Session key or session handle. Omit it for one-shot work. Pass a stable id when the next request should continue the same workspace.
streamHarness({
harness,
messages,
session: chatId,
});extraBody
TExtraBody
App-defined request context. Little Harness should pass it to tools,
chat.stageMessage, Input Type hooks, Persistent Dir callbacks, event
callbacks, and trace metadata.
extraBody: {
userId: "user_123",
teamId: "team_456",
role: "account_manager",
}Little Harness should not impose an auth or tenancy model on this object.
model
LanguageModel
Per-call model override. createHarness() still requires a default model in
the TypeScript API; use this when request-time configuration temporarily
overrides that default.
streamHarness({
harness,
messages,
model: requestModel,
});system
string | ModelMessage | ModelMessage[]
Per-call system override. When provided, it replaces the harness-level system prompt for this turn. Input Type instructions, skills, memory, and runtime hints are still added around the selected system prompt.
streamHarness({
harness,
messages,
system: requestSystem,
});runtime
HarnessRuntimeOptions
Per-call runtime policy override. It merges with the harness-level runtime policy.
streamHarness({
harness,
messages,
runtime: {
bash: false,
network: false,
},
});temperature
number
Per-call AI SDK temperature setting. Little Harness includes it in durable model request identity, so replay does not reuse a response generated with a different temperature.
output
Output
Optional AI SDK output specification for structured final output.
const result = streamHarness({
harness,
type: "support.ticket_triage",
input: ticket,
output: Output.object({ schema: triageSchema }),
});
const triage = await result.output;If the Input Type defines an output shape, a per-call output should override
it.
abortSignal
AbortSignal
Cancels staging, model calls, tool calls, and streaming when possible.
streamHarness({
harness,
messages,
abortSignal: req.signal,
});restage
boolean
Whether message staging should run again for messages that were already staged.
Default should be false.
streamHarness({
harness,
messages,
session: chatId,
restage: true,
});Use this only when intentionally rebuilding the session filesystem from message history.
runId
string
Durable identity of this run for kill-and-resume. Re-invoking with the same
runId (and the same inputs) against the same session log replays the
recorded run: recorded tool results are reused instead of re-executing tool
side effects. Defaults to a fresh id per call.
streamHarness({
harness,
messages,
session: chatId,
runId: `chat:${chatId}:${messageId}`,
sessionLog: remoteSessionLog({ baseUrl, authToken }),
});sessionLog
HarnessSessionLog (type from @little-stack/harness/events)
Per-call session-log override (the durable event log this run appends to and
replays from). When omitted, the log configured on createHarness() is used.
UNRELEASED: the current source removes the
durabilityalias. Published0.2.0-alpha.0acceptsdurabilityas an alias ofsessionLog(sessionLogwins when both are set). See Alpha Migrations.
See Execution Environments for the remote session-log adapter.
onEvent
Function that receives a HarnessEvent.
Optional per-call event callback.
streamHarness({
harness,
messages,
onEvent: (event) => requestTrace.append(event),
});onPersistenceError
Function that receives a PersistenceError.
Optional per-call persistence failure callback.
streamHarness({
harness,
messages,
onPersistenceError: (error) => retryQueue.enqueue(error.failedCommits),
});uiMessageStream
{ onError?: (error: unknown) => string }
Optional UI message stream formatting options. onError maps thrown stream errors to the text sent
in AI SDK error chunks. Keep the default for public web responses when server-side error details
should stay hidden; local tools such as the CLI can pass a more detailed formatter.
toolResultSpooling
false | { maxInlineBytes?: number; previewBytes?: number; outputDir?: string }
Per-call override for automatic oversized tool-result spooling. When omitted,
the setting from createHarness() is used. If neither is configured, Little
Harness spools tool results larger than 16 KB into /artifacts/tool-results.
The compact result includes summary metadata with text counts and, for JSON
content, a bounded structure sketch.
trace
false | HarnessTraceOptions
Per-call override for trace capture. When omitted, the setting from
createHarness() is used.
streamHarness({
harness,
messages,
session: chatId,
trace: {
content: {
previewBytes: 256,
},
},
});Set trace: false on a single call when the app should stream a response
without persisting a Local Host trace for that request.
Agent Loop Options
streamHarness() forwards the common AI SDK loop controls:
streamHarness({
harness,
messages,
stopWhen,
prepareStep,
toolChoice,
activeTools,
temperature,
providerOptions,
headers,
maxRetries,
});stopWhen takes AI SDK 7 stop conditions such as isStepCount(8) (imported
from ai). When it is omitted, Little Harness uses
isStepCount(workflowBudgets.maxModelSteps) — 20 by default — so streamed turns
can continue after executed tool calls.
connector
HarnessActiveConnector
Identity of the active connector for this turn: { id, kind, endpoint? }. It is
surfaced to tool execute handlers as ctx.connector. Connector loaders
(loadChatSdkConnector / loadWebRichConnector) populate it automatically when a
string connector reference or explicit connectorId is used; direct callers
rarely set it themselves.
connectorTools
ToolSet
Connector-scoped tool overrides resolved from
connectors/<id>/tools/* by the connector loaders. When present, Little Harness
runs resolveConnectorTools(harnessTools, connectorTools) before MCP/workflow
merging: executable connector tools override or add to the base tools, and
abstract base tools (no execute) are exposed to the model only when the active
connector supplies an executable extension. Like connector, this is loader
plumbing and is not usually set by hand.
toolPolicy
{ allow?: string[]; deny?: string[] }
Optional connector tool access policy applied after connector tools resolve. allow exposes only the
listed names; deny removes names and wins over allow; unknown names are ignored. The connector
loaders forward descriptor.toolPolicy here. It scopes the agent's own tools, not bash or
MCP/workflow tools.
Abstract-tool hiding
Abstract tools (declared without an execute) are structural placeholders and are never sent to the
model — this now holds on plain streamHarness/generateHarness/CLI runs, not just connector-loaded
ones. Each hidden tool adds one warning to result.finished.warnings:
{ code: "policy_warning", metadata: { reason: "abstract-tool-hidden", tool } }bash and runtime tools are unaffected.
Returns
text
PromiseLike<string>
The complete generated text. It can be awaited alongside
toUIMessageStreamResponse() because Little Harness tees the UI message stream.
The UI message stream itself can still only be consumed once.
const text = await result.text;output
Promise-like value for the parsed output.
The parsed structured output when output is provided.
const output = await result.output;textStream
AsyncIterable<string>
Incremental assistant text, like AI SDK streamText's .textStream — for non-AI-SDK consumers that
need to pipe the reply elsewhere (chat frameworks, CLIs, queues). It yields only assistant text deltas;
reasoning and protocol chunks are skipped. This is the surface the Chat SDK connector loader posts to a
platform thread (thread.post(result.textStream)). Like the UI message stream, it lazily consumes the
underlying stream, so iterate it or toUIMessageStream*() — not both.
for await (const delta of result.textStream) {
process.stdout.write(delta);
}toUIMessageStream()
Function that returns a readable stream of UIMessageChunk values.
Converts the result to an AI SDK-compatible UI message stream.
toUIMessageStreamResponse()
() => Response
Creates a streamed Response for useChat().
return result.toUIMessageStreamResponse();finished
Promise that resolves to StreamHarnessTurnFinished (in published 0.2.0-alpha.0,
StreamHarnessFinished; see the table below).
Resolves after the model, tools, file writes, artifact creation, and persistence commit work has finished.
If the AI SDK UI message stream emits an error chunk, the turn is failed even
when partial text already reached the client. uiMessageStream.onError only
formats the client-facing error chunk; result.text, result.output, and
result.finished still reject with the underlying error when available.
After-turn Persistent Dir commits run only for successful turns, so apps should
handle finished rejection when they need retry or cleanup behavior.
const finished = await result.finished;
if (finished.status === "completed" && finished.persistence.status === "failed") {
await retryQueue.enqueue(finished.persistence.failedCommits);
}When a tool parks the turn until pending work settles, finished resolves to
status: "parked" with continuationId and pending, plus artifacts, trace and
warnings, and without persistence or commitManual. resumeParked() continues
a turn that parked in await_tasks, as a generateHarness() turn; see
Asking A Person.
The guard above compiles with every version. What the type says differs:
| Version | Declared type of finished |
|---|---|
Published 0.2.0-alpha.0 | StreamHarnessFinished, an alias of StreamHarnessCompletedResult only. The runtime can still resolve to a parked result; the type does not include it. |
| UNRELEASED source (no npm version yet) | StreamHarnessTurnFinished, the union of the completed and parked results. StreamHarnessFinished no longer exists. |
See Typed parked results.
A completed turn has status: "completed" and these fields:
| Field | Type | Description |
|---|---|---|
session | HarnessSession | The session that ran the turn. |
artifacts | ArtifactRef[] | Artifacts created during the run. |
trace | TraceRef | Host-facing trace reference. |
persistence | PersistenceStatus | Persistent Dir commit status. |
commitManual | () => Promise<PersistenceStatus> | Commits commit: "manual" Persistent Dirs for this completed turn. |
warnings | HarnessWarning[] | Undefined input types, provider warnings, staging warnings, or policy warnings. |
Examples
Chat Route
export async function POST(req: Request) {
const body = await req.json();
const result = streamHarness({
harness,
messages: body.messages,
session: body.chatId,
extraBody: {
userId: body.userId,
teamId: body.teamId,
},
});
return result.toUIMessageStreamResponse();
}Stream A Custom Input
const result = streamHarness({
harness,
type: "document.review",
input: {
documentId: "doc_123",
requestedChecks: ["renewal-risk", "privacy"],
},
session: "doc_123",
});
return result.toUIMessageStreamResponse();Observe Persistence After Streaming
const result = streamHarness({
harness,
messages,
session: chatId,
});
result.finished.then(async ({ persistence }) => {
if (persistence.status === "failed") {
await retryQueue.enqueue(persistence.failedCommits);
}
});
return result.toUIMessageStreamResponse();