Skip to content

Harness & loop

An agent framework has two things you must be able to name:

  • The harness — everything the agent may do. Its tools, identity, team, ceilings, policy. Fixed per problem.
  • The loop — a live execution of that harness. What happened, how many turns it spent, whether it finished. Observed, never configured.

toolnexus already shipped both and named neither, so everyone invented their own vocabulary. These are the names.

This is the whole design in four rows. If you are unsure where an option belongs, it is answering one of these questions:

question answered by scope
MAY it? — capability, tools, ceilings the harness per problem
with WHAT? — the model for this call run options per call
DID it? — status, turns, stop reason the loop observed
is it RIGHT? a tool, a skill, or another agent never the loop

Two consequences fall out of it, and both are deliberate:

The loop takes no configuration. There is nothing to set on it. Capability belongs to the harness; per-call choices belong to the run options. A loop is read, not tuned.

The model is per call, not per loop. So one conversation may change model between turns — cheap model to plan, expensive model to finish — without rebuilding anything.

A loop takes client options, not a built client — because a per-call model override has to be able to change the model, and the model is fixed the moment a client is constructed.

import { agents, createToolkit } from "toolnexus"
const writer = agents.agent("writer", {
does: "drafts release notes",
soul: "You write terse, factual release notes.",
})
const toolkit = await createToolkit({ builtins: false })
const loop = writer.loop(
{ baseUrl: "https://openrouter.ai/api/v1", style: "openai",
model: "openai/gpt-4o-mini", apiKey: process.env.OPENROUTER_API_KEY },
toolkit,
)
const out = await loop.run("Draft notes for 0.15.0.")
console.log(out.status) // "done"
console.log(out.turns) // model round trips
console.log(out.stoppedBy) // undefined when done; ALWAYS set otherwise
// A different model for one call only. The conversation continues.
await loop.run("Now tighten it.", { model: "openai/gpt-4o" })

Every run returns an Outcome. The field that matters most is the one that is easy to skip:

field meaning
text the final answer
status done · incomplete · pending · error — the shipped vocabulary, no new strings
stoppedBy always set when status is not done — a loop never stops silently
attempts how many times the completion gate ran the work (1 when there is no gate)
turns model round trips, accumulated across runs on this loop
result the underlying RunResult, including limit for a structured stop reason

The loop answers did it finish? — never is the work any good? That question belongs to a tool, a skill, or another agent, because only your domain can answer it.

The one exception is structural, and it is the next page: a completion gate that refuses to let an agent report done while its own declared plan is unfinished.

Next: The completion gate · or see it proved against live models.