createHarness()
Define a reusable Little Harness runtime surface.
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
createHarness() defines the reusable agent runtime surface. It does not start a
model call by itself. It collects the host, system prompt, model, tools, memory,
skills, workflows, file staging hooks, Input Types, Persistent Dirs, runtime
policy, session log, and callbacks that streamHarness() and generateHarness()
will use later. AI SDK loop controls such as stopWhen are per-call options of
those functions, not createHarness() options.
Import
import {
createHarness,
localHost,
memory,
} from "@little-stack/harness";Basic Usage
const harness = createHarness({
host: localHost({ dataDir: ".little-harness" }),
system: "Help account teams prepare evidence-backed renewal briefs.",
model,
tools: { searchDocs, queryCustomers, saveReport },
skills: [skill("./skills/customer-renewal-analysis")],
});API Signature
function createHarness<TTools extends ToolSet = ToolSet, TExtraBody = unknown>(
options: CreateHarnessOptions<TTools, TExtraBody>,
): Harness<TTools, TExtraBody>;Parameters
host
HarnessHost
The session, filesystem, trace, artifact, and persistence host used by this harness.
host: localHost({ dataDir: ".little-harness" })The first SDK target is Local Host. Managed and self-hosted hosts can later implement the same host contract.
system
string | SystemModelMessage | SystemModelMessage[]
Instructions for the agent. This is the global behavior that applies to every execution call unless an Input Type or step policy adds narrower instructions.
system: "Use uploaded files and approved tools before answering."Use the system prompt for stable behavior. Put product-specific request details
in messages, type + input, extraBody, chat.stageMessage, or Input Types.
model
LanguageModel
The language model used by default.
model: gateway("anthropic/claude-sonnet-4.5")A streamHarness() or generateHarness() call can override the model for one
run by passing its own model; otherwise this default is used.
tools
ToolSet
Host app capabilities exposed to the agent. Little Harness accepts normal AI SDK-style tools and adds a harness execution context as the second argument.
import { harnessToolContext } from "@little-stack/harness";
const queryCustomers = tool({
description: "Query customer account data by account id or search string.",
inputSchema: z.object({
accountId: z.string().optional(),
query: z.string().optional(),
}),
execute: async (input, options) => {
const rows = await customerDb.query(input);
// The AI SDK types do not declare the harness context; read it through the typed accessor.
const ctx = harnessToolContext(options);
const file = await ctx.files.writeJSON(
"/artifacts/tool/query-customers/result.json",
rows,
);
return {
preview: "Customer query exported for analysis.",
path: file.path,
bytes: file.bytes,
};
},
});Tool return values should stay compact. Planned large data should be written
with ctx.files and returned as paths plus summaries. If a tool returns an
oversized value anyway, Little Harness can automatically spool it to a file.
Tools also receive ctx.artifacts, which can list artifacts and read
/artifacts/... files that another tool or runtime command already wrote.
The AI SDK types do not declare these fields; for typed access, call
harnessToolContext(options) from @little-stack/harness (it returns the context and throws outside a
Little Harness turn; tryHarnessToolContext(options) returns undefined instead).
Some tool names are reserved because Little Harness injects or protects them.
createHarness rejects a configured tool with a reserved name, and
loadHarness rejects a discovered tools/<name>.ts with one.
| Version | Reserved for configured and discovered tools |
|---|---|
Published 0.2.0-alpha.0 | bash, __proto__, constructor, prototype |
| UNRELEASED source (no npm version yet) | The names above, plus checkpoint and the five Agent Group tools: start_agent_group, spawn_agent, watch_agent_group, join_agent_group, cancel_agent_group |
In the unreleased source, these extra names stay reserved even when
checkpoints or agents is not configured. An existing tool named
checkpoint or after an Agent Group tool passes the published name validation in
0.2.0-alpha.0, but must be renamed for the unreleased SDK. See
Broader reserved tool names.
mcp
HarnessMcpConfig
Declarative MCP servers exposed through ordinary AI SDK gateway tools. MCP is
not a parallel execution API; the model normally lists capabilities with
mcp_list_tools and invokes one MCP server tool with mcp_call_tool.
const harness = createHarness({
host: localHost({ dataDir: ".little-harness" }),
model,
mcp: {
servers: [
{
id: "figma",
description: "Read Figma design context and screenshots.",
transport: {
type: "stdio",
command: "node",
args: ["./mcp/figma.mjs"],
},
guide: {
description: "Use when a task depends on live Figma design context.",
body:
"Call `mcp_list_tools`, then `mcp_call_tool` with server `figma`.",
},
},
],
},
});Gateway tools share the normal tool-call path: trace events, abort signal,
durability replay, Runtime Tool Bridge access, and toolResultSpooling all work
the same way as app tools. Oversized MCP outputs are spooled through the normal
tool result artifact path.
When guide is enabled, Little Harness stages a guide skill under
.agents/skills/<server-id>-mcp/SKILL.md. Guide skills are only instructions;
they do not grant capability. Tool availability is controlled by the mcp
config and normal tool visibility.
toolResultSpooling
false | { maxInlineBytes?: number; previewBytes?: number; outputDir?: string }
Controls automatic file spooling for oversized tool return values. It applies
to app tools and the built-in bash tool.
toolResultSpooling: {
maxInlineBytes: 32_000,
previewBytes: 512,
outputDir: "/artifacts/tool-results",
}When omitted, oversized tool results are spooled after 16 KB. The model receives
a compact JSON object with the artifact path, byte count, media type,
line/character counts, a bounded JSON structure sketch when available, preview,
and a message telling it to read the file for the full result.
Set toolResultSpooling: false to disable this behavior.
promptCaching
boolean, default true
Anthropic prompt caching on the agent's model calls. The harness puts a cache point on the system prompt and turns on Anthropic's automatic cache point at the end of the conversation:
- Anthropic sends tools first, then the system prompt, so the tool definitions and instructions every session repeats are read from the cache by the next session that starts within the cache's lifetime (5 minutes by default).
- Within a turn, each step reads everything the step before it sent.
A cache read costs a tenth of the normal input price or less, and the first write
about 1.25 times. Both cache points ride in providerOptions.anthropic, so a
provider that does not read Anthropic options ignores them. On Amazon Bedrock (a
model whose provider id starts with bedrock, such as the Anthropic models of
@ai-sdk/amazon-bedrock), the harness keeps the system prompt's cache point but
leaves out the automatic one, because Bedrock's InvokeModel rejects a top-level
cache_control with a 400 for older Claude models; there, steps within a turn do
not read each other from the cache.
Anthropic allows 4 cache points per request, and the harness uses 2 of them (1 on
Bedrock). If you add your own cacheControl markers to messages or tools, use at
most 2 more, or Anthropic rejects the request.
To use the 1-hour cache, pass
providerOptions: { anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } }
to generateHarness() or streamHarness(); the system prompt's cache point uses
the same TTL.
createHarness({ host, model, promptCaching: false }); // send no cache pointsThe cache only helps if the system prompt and tools stay the same from session to
session: keep dates, user names and other per-session values out of system
and pass them in the messages instead.
trace
false | HarnessTraceOptions
Controls Local Host trace capture for model steps, tool calls, mounted files, file changes, artifacts, runtime commands, and Persistent Dir commits.
trace: {
content: {
previewBytes: 512,
maxInlineBytes: 16 * 1024,
captureReasoning: true,
captureModelMessages: true,
captureToolInputs: true,
captureToolOutputs: true,
},
fileDiffs: {
enabled: true,
maxInlineBytes: 8 * 1024,
maxBytesToDiff: 256 * 1024,
},
redaction: {
metadataKeys: ["authorization", "cookie", "token", "password", "secret"],
paths: ["/persistent/company/secrets"],
},
}By default, Little Harness records metadata, small previews, and bounded text
diffs. Large captured content is written under /artifacts/trace/... and
referenced by contentRef. Raw mounted file contents, hidden provider
reasoning, credentials, and binary payload bodies are not written to the trace.
Set trace: false to disable trace persistence for this harness. A
streamHarness() or generateHarness() call can override the harness-level
policy for that run.
chat
HarnessChatOptions
Chat-specific hooks for raw AI SDK UIMessage[] input.
chat: {
stageMessage: async ({ message, inputFiles, files }) => {
const stagedFiles = await Promise.all(
inputFiles.map((file) =>
files.write(
`/session/user-input/${message.id}/${file.safeName}`,
file.content,
{ mediaType: file.mediaType, source: "user-message" },
),
),
);
return {
stagedFiles,
notice: "Uploaded files were staged into /session/user-input/.",
};
},
}chat.stageMessage
(options: StageMessageOptions<TExtraBody>) => StageMessageResult | Promise<StageMessageResult>
Runs before a newly submitted user message reaches the model. It lets the app move uploaded files into the session filesystem and return a short notice that mentions paths instead of raw file contents.
StageMessageOptions:
| Field | Type | Description |
|---|---|---|
message | UIMessage | The newly submitted user message. |
inputFiles | InputFile[] | Normalized file parts from the message. |
files | FileWriter | Writer for /session/, /artifacts/, and Persistent Dir harness directories. |
session | HarnessSession | The current session handle. |
extraBody | `TExtraBody | undefined` |
abortSignal | `AbortSignal | undefined` |
StageMessageResult:
| Field | Type | Description |
|---|---|---|
stagedFiles | StagedFile[] | Files written into the harness filesystem. |
notice | `string | undefined` |
metadata | object or undefined | Host-facing trace metadata. |
skills
SkillInput[]
Procedural knowledge exposed read-only for on-demand use.
skills: [
skill("./skills/customer-renewal-analysis"),
skill("./skills/domain-glossary"),
skill("https://github.com/anthropics/skills", {
skills: ["frontend-design"],
}),
]Skill metadata is injected into the system prompt. Full skill bodies are mounted
read-only under .agents/skills/<name>/ so the model can inspect details only when needed.
skillMaxRisk ("NONE" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL") and
skillOidcToken configure the optional remote-skill risk gate for every skill on
this harness; see skill().
inputTypes
Record of input type names to HarnessInputType definitions.
Reusable definitions for non-chat request shapes.
inputTypes: {
"support.ticket_triage": inputType({
description:
"Triage one support ticket and decide priority, owner, and next action.",
inputSchema: ticketSchema,
output: Output.object({ schema: triageSchema }),
toMessages: ({ input }) => [
{
role: "user",
content: `Triage this ticket:\n\n${input.body}`,
},
],
}),
}Undefined input types run with a host-facing warning rather than fail by default.
memory
HarnessMemoryOptions | HarnessMemoryOptions[]
Opt-in long-term memory kept as plain files. memory(...) mounts a Persistent Dir,
shows the agent a short note and one line per file every turn, and adds a built-in
memory skill on how to save, change and forget memories. There is no memory tool:
the agent writes, edits and deletes files with its file tools.
Pass an array for layers, such as company knowledge plus one memory per user. Each
needs its own harnessDir. canWrite({ extraBody }) mounts a memory read-only for a
turn, and backend: { load, store } keeps it in another service instead of a
sourceDir. A skill named memory in skills replaces the built-in one. See
Memory.
const harness = createHarness({
host: localHost({ dataDir: ".little-harness" }),
model,
system: "Answer database questions using schema-aware tools.",
tools: { queryDatabase, inspectSchema },
memory: memory({
sourceDir: "memory/db-query-agent",
description: "Lessons about this database.",
}),
});Defaults:
| Field | Default |
|---|---|
harnessDir | /persistent/memory |
commit | after-turn |
maxIndexBytes | 8000 |
canWrite | anyone may write |
skill | the built-in memory skill |
Use commit: "read-only" to show existing memory without ever storing changes,
and skill: false to leave the built-in skill out. Memory uses the same Persistent
Dir load, diff, trace, and commit behavior as explicit persistentDirs.
persistentDirs
PersistentDir[]
Opt-in, developer-backed directories that can persist across sessions.
persistentDirs: [
{
harnessDir: "/persistent/user",
load: async ({ extraBody }) =>
storage.loadFolder(`users/${extraBody.userId}/memory`),
store: async ({ extraBody, changes }) =>
storage.applyChanges(`users/${extraBody.userId}/memory`, changes),
commit: "after-turn",
},
]Writes land inside the isolated session first. Little Harness calls the
developer's store callback at the commit boundary.
harnessDir is the path inside the harness filesystem. Backing storage is
defined by load / store, localDir({ sourceDir }), or an object-store
adapter's prefix.
runtime
HarnessRuntimeOptions
Runtime policy for the built-in Local Host runtime.
runtime: {
bash: true,
network: false,
python: true,
javascript: true,
toolBridge: true,
}bash controls whether Little Harness exposes the built-in command tool to the
model. It defaults to true. Set bash: false when app tools are the only
capabilities the model should call. Without bash the harness adds load_skill
when the turn has skills, so the agent can still open them, and, with memory,
list_files, read_file, write_file and delete_file limited to the memory
folders.
network controls network access for the command runtime. It defaults to
false.
python and javascript control language helpers inside the command runtime.
They default to true.
toolBridge controls whether configured tools are exposed inside just-bash
JavaScript as tools.<name>(args). It defaults to true when bash and
JavaScript are enabled. Set it to false when the model should only invoke
tools directly through provider tool calls.
Runtime Tool Bridge calls share normal tool context, spooling, trace events, and
durability replay. They appear as tool events with caller: "runtime".
Execution calls can override runtime policy for one turn:
streamHarness({
harness,
messages,
runtime: { bash: false },
});Under localHost()'s default sandbox: "auto", these toggles also
choose where the turn's commands run: in-process just-bash only when python
and javascript are both false and network is off, the subprocess sandbox
otherwise. See
Execution Environments.
agents
UNRELEASED: the
agentsoption exists only in the current source. Published0.2.0-alpha.0has no runtimeagentsoption, and no npm version includes it yet. Agent folder scaffolding is a separate, published feature.
HarnessAgentGroupsOptions
Optional neutral launcher port for ordered fan-out into isolated agents. It injects
start_agent_group, spawn_agent, watch_agent_group, join_agent_group, and
cancel_agent_group. When agents is absent, none of these tools exist.
const harness = createHarness({
host: localHost(),
model,
agents: {
launcher,
profiles: [{
handle: "repo-worker",
resourcesHandle: "cpu-medium",
secretsHandle: "repo-readwrite",
toolsHandle: "code-and-browser",
networkHandle: "package-registries",
priorityHandle: "interactive",
}],
joinPolicies: [{ handle: "all", policy: { kind: "all_success" } }],
defaultJoinPolicyHandle: "all",
reductionPolicies: [{
handle: "concise",
maxMemberSummaryBytes: 16_384,
maxGroupSummaryBytes: 32_768,
maxResultRefs: 32,
}],
defaultReductionPolicyHandle: "concise",
limits: { maxGroupPromptBytes: 2_097_152 },
},
});Profiles and policies are trusted declarations. The model can select their handles but cannot request raw resources, secrets, tools, network policy, priority, verifiers, or check sets. Launcher starts must be atomic and idempotent, and accepted member bindings commit through one durable group authority. Legacy observation snapshots use monotonic group/member revisions. A launcher that supplies the complete additive page trio may instead return strict aggregate/change pages and configure up to 10,000 members; cursors are opaque, store-bound, bounded, and gap-free. V2 pages sequence explicit member and group-control changes together, while still reading legacy v1 member-only pages. Both modes contain bounded summaries, not child transcripts. Model schemas enumerate trusted handles/descriptions, while adapter task refs and verification receipts stay internal.
Paged groups retain 4,096 replay sequences, permanently reject retired stable calls, and enforce a 65,536-call lifetime ceiling before launcher I/O. Durable hosts can explicitly collect a terminal group through the task ledger's exact version/digest CAS; collection is crash-recoverable and leaves compact receipts that prevent old starts and cross-session reuse of external group refs from recreating deleted authority. Result-ref publication uses recoverable per-ref claims, preserves the first publication across higher terminal revisions, and collection conditionally removes only the exact owner bytes it prepared. Exact accepted-start replay is served from immutable Harness authority before launcher I/O, while an irreversible collection fence is honored by every writer across process restart.
See Agent Groups for the launcher contract,
first_verified binding, revision fencing, and failure semantics.
checkpoints
UNRELEASED: the
checkpointsoption exists only in the current source. Published0.2.0-alpha.0has nocheckpointsproperty onCreateHarnessOptions, and no npm version includes it yet.
HarnessCheckpointsOptions
Optional trusted writer port for durable environment milestones. When configured, Little Harness injects one
checkpoint tool. The model can annotate a point with a label, a goal_reached or pre_risky_op kind, an
optional rationale, and evidence artifact ids:
const harness = createHarness({
host: localHost({ dataDir: ".little-harness" }),
model,
checkpoints: {
writer: {
async checkpoint(request) {
return milestoneService.checkpoint(request);
},
},
},
});The host or control-plane writer owns milestone identity, tier, state, author, timestamp, cost, retention, and
verification. Those fields are deliberately absent from model input, so an agent cannot self-claim
verified_green. The writer also receives the Harness-supplied session, turn, model-step, and sequence position.
A replayed tool call carries the same deterministic callIdentity; writers should use it as their idempotency key
and return the original milestone result.
The result disposition is created, replayed, or coalesced. Coalescing can return an existing milestone id
for a different call that adds no semantic state; the response still binds the current call's identity and
does not move the original milestone's session position. Little Compute's Direction-v2 local profile compares
the current compatible checkpoint head, not arbitrary historical matching trees. A changed tree or meaningful
annotation creates a new node; returning to an older tree after an intervening change does not reuse that
historical node. The retained advanced profile keeps its existing non-coalescing replay behavior.
humanInput
boolean | HumanInputOptions
Lets the agent ask a person a question without waiting on the answer. Adds the
ask_person tool, which returns at once with a task id, and ask to every
tool's execution context. The agent gets the answer from await_tasks; if it is
not in yet, the turn parks. Needs a host with durable services, such as
localHost.
const harness = createHarness({
host: localHost({ dataDir: "./data" }),
model,
humanInput: {
deliver: async (request) => inbox.add(request), // send the question out
},
});| Option | Default | Meaning |
|---|---|---|
deliver(request, { session }) | none | Sends a new question to a person, once per question. When it throws, the question stands and the agent is told sending it failed. |
toolName | "ask_person" | The tool's name. |
description | built in | The tool's description. |
tool | true | false leaves the tool out; tools can still ask. |
Record replies with answerInputRequest(), list open questions with
listInputRequests(), and continue parked turns with resumeParked(). See
Asking A Person.
dynamicWorkflows
DynamicWorkflowsConfig
Opt-in, model-authored one-shot plans. Pass the dynamicWorkflows() helper from
@little-stack/workflow (not a bare true — the helper injects the plan-lowering
factory, which the harness cannot import without a circular dependency). It adds
three tools, run_ad_hoc_plan, search_authored_plans and define_workflow, and
registers a durable plan store at /persistent/dynamic-plans. Each workflow the agent
defines with define_workflow is a tool of its own in later turns and sessions.
import { dynamicWorkflows } from "@little-stack/workflow";
const harness = createHarness({
host: localHost({ dataDir: ".little-harness" }),
model,
tools: { docsSearch, docsRead },
dynamicWorkflows: dynamicWorkflows(),
});A one-shot plan can never reference a capability outside the agent's own
snapshot; anything else fails with capability_not_allowed before it runs. Narrow
the ceiling or tune the caps with options:
| Field | Default |
|---|---|
limits.maxSteps | 8 |
limits.maxRuntimeMs | 120000 |
exclude | [] |
models | none: plan steps use default, the agent's own model |
define | true: the agent gets define_workflow |
registry | none: every stored definition is offered, from the next turn on, in every session. { defined?, resolve? } hooks decide which definitions and revisions each turn offers; see Deciding What Each Turn Offers |
world | localWorld in the harness's stores. A World, or a function of the run, for agent-defined workflow and one-shot plan runs |
dynamicWorkflows: dynamicWorkflows({
exclude: ["dangerousTool"],
limits: { maxSteps: 6, maxRuntimeMs: 60_000 },
models: { fast: anthropic("claude-haiku-5-5") }, // extra model slots for ai.generate steps
})Off by default. See Letting an agent author its own workflow for the full guide.
workflows
readonly HarnessWorkflow[]
Workflows exposed to the agent as tools, one per workflow, named from the
workflow id (billing.refund → billing_refund) and typed by its input schema.
Adapt a Little Workflow defineWorkflow result with asHarnessWorkflow from
@little-stack/workflow; definitionIdentity is required.
import { asHarnessWorkflow } from "@little-stack/workflow";
workflows: [asHarnessWorkflow(refundWorkflow, { definitionIdentity: "billing.refund@1" })]The model receives { status, runId, outputSummary?, outputPath? } — the output
rendered as text and capped at 4096 characters — not the typed output. Agent
folders also discover workflows from workflows/*.ts. See
Triggering a workflow from an agent.
workflowBudgets
Partial<HarnessWorkflowBudgets>
Per-session limits. Omitted fields keep their defaults. The most commonly tuned fields:
| Field | Default | Effect |
|---|---|---|
maxConcurrentWorkflowRuns | 10 | Workflow-tool runs in flight at once per session; extra calls wait (FIFO). |
maxQueuedWorkflowRuns | 100 | Calls allowed to wait; past this a call fails with causeCode: "max_queued_workflow_runs". |
maxModelSteps | 20 | Default step limit (isStepCount(maxModelSteps)) when a call passes no stopWhen. |
workflowBudgets: { maxConcurrentWorkflowRuns: 2, maxModelSteps: 12 }turnOptions
Function that receives a HarnessTurnContext and returns HarnessTurnOverrides
(or nothing).
Decides one turn's per-run options from its session: the model, the system
prompt, and any other option generateHarness takes per run (temperature,
activeTools, providerOptions, ...), plus an onEvent for this turn's events.
It is awaited once per turn, after the session is known and before anything
runs, on every path that runs a turn: generateHarness, streamHarness,
connectors, schedules and little-harness start.
turnOptions: async ({ sessionId, extraBody, trigger }) => {
const plan = await plans.forUser(extraBody?.userId);
return { model: plan.premium ? smartModel : fastModel, system: plan.instructions };
}| Field of the context | Meaning |
|---|---|
sessionId, session | The turn's session. |
extraBody | The turn's extraBody. |
trigger | How the turn started; a connector's turn names the channel and the sender. |
Options a call passes itself win over what the hook returns; runtime and
trace merge, with the call's on top; both onEvents run, the hook's first. A
hook that throws fails the turn. LittleDB's db.managedHarness() returns one
that runs each session on its version of the prompt; see
LittleDB with Little Harness.
onEvent
Function that receives a HarnessEvent.
Receives host-facing trace events.
onEvent: async (event) => {
await traces.append(event);
}Callbacks should not be injected into the model context automatically. They are for host observability, dashboards, retries, and debugging.
onPersistenceError
Function that receives a PersistenceError.
Receives Persistent Dir commit failures.
onPersistenceError: async (error) => {
await retryQueue.enqueue(error.failedCommits);
}Commit failures are control-plane events. The session workspace should keep uncommitted changes recoverable.
onTraceError
(error: unknown, event: TraceHarnessEventInput) => void | Promise<void>
Called when a trace event cannot be written. Trace failures never fail the run; use this to log or count them.
sessionLog
HarnessSessionLog (type from @little-stack/harness/events)
The session log: the append-only durable event log a run appends to and replays
from (append plus priorEvents). Re-invoking a run with the same runId
against the same log replays recorded model and tool calls instead of
re-executing tool side effects. generateHarness() and streamHarness() accept
a per-call sessionLog that overrides this one.
import { fileSessionLog, remoteSessionLog } from "@little-stack/harness/remote-session";
sessionLog: fileSessionLog({ path: ".little-harness/session-log.ndjson" })
// or, shared across processes:
sessionLog: remoteSessionLog({ baseUrl: process.env.SESSION_LOG_URL!, authToken })UNRELEASED: in the current source the session-log helpers come only from
@little-stack/harness/remote-session, the file and in-memory logs arefileSessionLog()andinMemorySessionLog(), and thedurabilityalias is gone. Published0.2.0-alpha.0exportscreateFileSessionLog(),createInMemorySessionLog()andremoteSessionLog()from the root entry and acceptsdurabilityas an alias ofsessionLog. See Alpha Migrations.
Returns
Harness
A reusable configuration object passed to streamHarness() and
generateHarness().
type Harness<TTools extends ToolSet = ToolSet, TExtraBody = unknown> = {
sessions: HarnessSessionManager<TExtraBody>;
config: ResolvedHarnessConfig<TTools, TExtraBody>;
};The exact object shape can stay opaque. The important public contract is that it can create, resume, and execute sessions through the documented APIs.
Examples
Chat Agent With File Staging
const harness = createHarness({
host: localHost(),
model,
system: "Answer using uploaded files and approved account tools.",
tools: { queryCustomers, saveReport },
chat: {
stageMessage: async ({ message, inputFiles, files }) => ({
stagedFiles: await Promise.all(
inputFiles.map((file) =>
files.write(
`/session/user-input/${message.id}/${file.safeName}`,
file.content,
{ mediaType: file.mediaType },
),
),
),
}),
},
});Agent With User-Backed Persistence
const harness = createHarness({
host: localHost(),
model,
system: "Maintain lightweight project notes when they are useful.",
persistentDirs: [
{
harnessDir: "/persistent/user",
load: ({ extraBody }) => userFiles.load(extraBody.userId),
store: ({ extraBody, changes }) =>
userFiles.applyChanges(extraBody.userId, changes),
},
],
});