Little documentation
Guides and APIReference

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.

VersionReserved for configured and discovered tools
Published 0.2.0-alpha.0bash, __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 points

The 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:

FieldTypeDescription
messageUIMessageThe newly submitted user message.
inputFilesInputFile[]Normalized file parts from the message.
filesFileWriterWriter for /session/, /artifacts/, and Persistent Dir harness directories.
sessionHarnessSessionThe current session handle.
extraBody`TExtraBodyundefined`
abortSignal`AbortSignalundefined`

StageMessageResult:

FieldTypeDescription
stagedFilesStagedFile[]Files written into the harness filesystem.
notice`stringundefined`
metadataobject or undefinedHost-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:

FieldDefault
harnessDir/persistent/memory
commitafter-turn
maxIndexBytes8000
canWriteanyone may write
skillthe 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 agents option exists only in the current source. Published 0.2.0-alpha.0 has no runtime agents option, 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 checkpoints option exists only in the current source. Published 0.2.0-alpha.0 has no checkpoints property on CreateHarnessOptions, 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
  },
});
OptionDefaultMeaning
deliver(request, { session })noneSends 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.
descriptionbuilt inThe tool's description.
tooltruefalse 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:

FieldDefault
limits.maxSteps8
limits.maxRuntimeMs120000
exclude[]
modelsnone: plan steps use default, the agent's own model
definetrue: the agent gets define_workflow
registrynone: 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
worldlocalWorld 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:

FieldDefaultEffect
maxConcurrentWorkflowRuns10Workflow-tool runs in flight at once per session; extra calls wait (FIFO).
maxQueuedWorkflowRuns100Calls allowed to wait; past this a call fails with causeCode: "max_queued_workflow_runs".
maxModelSteps20Default 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 contextMeaning
sessionId, sessionThe turn's session.
extraBodyThe turn's extraBody.
triggerHow 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 })

See Execution Environments.

UNRELEASED: in the current source the session-log helpers come only from @little-stack/harness/remote-session, the file and in-memory logs are fileSessionLog() and inMemorySessionLog(), and the durability alias is gone. Published 0.2.0-alpha.0 exports createFileSessionLog(), createInMemorySessionLog() and remoteSessionLog() from the root entry and accepts durability as an alias of sessionLog. 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),
    },
  ],
});

On this page