Skip to content
Français

Build your own agent loop

Use Outpost’s built-in harness with a model provider, tools and explicit limits.

Choose the built-in harness when you want to control the agent’s model API, tools and execution rules yourself. The loop runs in your Node.js process, while its tools act in the task’s sandbox.

CLI agentBuilt-in harness
Loop runsIn the sandbox, as the CLI processIn your Node.js process
ToolsThe CLI’s ownOnly those you pass to tools
Model accessAccount login or API keyAn API key on a model provider
Sandbox imageContains the CLI, or installs it at startupNo agent CLI to install
ControlCLI settingsPermissions, hooks and limits per call, and subagents
Usage reportingDepends on the CLIAfter each model response

Choose an agent compares every capability.

createHarness() configures the loop; createAgent() pairs it with a model. The agent then goes to dispatch() like any other.

import { createAnthropicModelProvider } from "@elie-laloum/outpost";

export const modelProvider = createAnthropicModelProvider({
  apiKey: process.env.ANTHROPIC_API_KEY ?? "",
});
import {
  createAgent,
  createHarness,
  createHarnessFileTools,
  createHarnessSearchTools,
} from "@elie-laloum/outpost";
import { modelProvider } from "./review-model.ts";

export const reviewer = createAgent({
  model: { name: "claude-sonnet-5-5", maxOutputTokens: 16_000 },
  harness: createHarness({
    modelProvider: modelProvider,
    instructions: "Inspect the repository and answer with evidence.",
    tools: [createHarnessFileTools(), createHarnessSearchTools()],
  }),
});
import { reportValue } from "./reporter.ts";
import { dispatch } from "@elie-laloum/outpost";
import { repository, sandboxProvider } from "./outpost.config.ts";
import { reviewer } from "./review-agent.ts";

export const result = await dispatch({
  repository,
  sandboxProvider,
  agent: reviewer,
  brief: { text: "List the exported functions that no test calls." },
});
reportValue(result.text);
// Example output: No test calls parseDate() or formatDate().
reportValue(result.usage);
// Example output: { input: 1200, cached: 0, output: 320 }

Set ANTHROPIC_API_KEY, then run node harness-review.ts. result.text holds the model’s final answer. These tools only read files, so the agent cannot edit the repository.

Drag to move · Ctrl + scroll to zoom
100 %
  • Start the turnOnce per brief, pass or repair.
    1. Build the system promptResolve instructions and the skill catalog. host
    2. Start MCP serversDeclared mcpServers start and add their tools. sandbox
    (Steps)
    • → Run a step : then
  • Run a stepRepeated until the model answers without tool calls.
    1. Request the modelSend the history, the system prompt and the tool list. host
    2. Check the callsValidate each input against its schema, then apply permissions and before-tool hooks. host
    3. Run the toolsConsecutive read-only calls run in parallel, the others one at a time. sandbox
    4. Return the resultsResults and tool errors join the history for the next step. host
    (Steps)
    • → Finish : then
  • FinishThe model answers.
    1. Return the answerThe final text becomes result.text, unless a stop hook or a steering message sends the model back to work. host
    (Steps)

Outpost checks the limits before every step and after each model response. The first one reached ends the turn with an OutpostError.

limits bounds the loop; toolExecution sets how tool calls run.

import {
  createAnthropicModelProvider,
  createHarness,
  createHarnessShellTools,
} from "@elie-laloum/outpost";

const harness = createHarness({
  modelProvider: createAnthropicModelProvider({
    apiKey: process.env.ANTHROPIC_API_KEY ?? "",
  }),
  tools: [createHarnessShellTools()],
  limits: { maxSteps: 30, maxToolCalls: 60, usage: { output: 20_000 } },
  toolExecution: { deadlineMs: 60_000, onError: "fail" },
});

API reference: HarnessLimits and HarnessToolExecution.

With the default onError, a failed or expired call goes back to the model as an error result, and the loop continues. usage counts subagents and context summaries, and requires a model provider that reports usage completely.

deadlineMs and idleMs on dispatch() also bound each harness turn: its total duration, and the silence between loop events. A running tool call pauses the idle timer; toolExecution.deadlineMs bounds it instead. See Limits and cancellation.

API reference: AgentObservation.

Watch tool calls and model requests while the harness runs. This example enables verbose observation so the callback can inspect the full model request.

import { createAnthropicModelProvider } from "@elie-laloum/outpost";

export const observedModel = createAnthropicModelProvider({
  apiKey: process.env.ANTHROPIC_API_KEY ?? "",
});
import {
  createAgent,
  createHarness,
  createHarnessFileTools,
} from "@elie-laloum/outpost";
import { observedModel } from "./observed-model.ts";

export const agent = createAgent({
  model: { name: "claude-sonnet-5-5", maxOutputTokens: 16_000 },
  harness: createHarness({
    modelProvider: observedModel,
    tools: [createHarnessFileTools()],
  }),
});
import { reportValue } from "./reporter.ts";
import { dispatch, createObservationHub } from "@elie-laloum/outpost";
import { repository, sandboxProvider } from "./outpost.config.ts";
import { agent } from "./observed-agent.ts";

await dispatch({
  repository,
  sandboxProvider,
  agent,
  brief: { text: "Explain how the build is configured." },
  observation: createObservationHub({ verbose: true }),
  observe(event) {
    if (event.kind === "tool") reportValue(event.name, event.input);
    // Example output: read_file { path: "README.md" }
    if (event.kind === "model-request") console.dir(event.request);
  },
});

tool-output streams command output, correlated with its call by callId. Full model-request and model-response payloads exist only with a verbose observation hub. They hold the whole conversation, and the normal journal leaves them out.

  • Model access needs an API key, or a keyless local server. CLI account logins do not apply.
  • Tool code runs in your process with your permissions. Only what it does through context.sandbox runs in the sandbox.
  • Model requests leave from your process, so the sandbox’s network restrictions do not apply to them.

API: createHarness · HarnessOptions · HarnessLimits · HarnessToolExecution · createAgent · createObservationHub.

Add an optional routing declaration to select among models of this harness model provider after compaction and before model hooks. Tools and hooks see the selected model while the sandbox and history remain available. See model routing for candidate validation, state, fallback and usage behavior.