Loop
JavaScript · package toolnexus · SPEC §7D
// under agents.*function harness(spec: AgentSpec): AgentSpec // identity — a name, not a wrapper
class Loop { constructor(agent: Agent, options: ClientOptions, toolkit: Toolkit) get status(): string get turns(): number run(prompt: PromptInput, opts?: { model?: string }): Promise<Outcome>}
type Guardrail = (ev: { name: string; args: Record<string, unknown>; id?: string; turn: number }) => string | undefined | void
interface Verdict { ok: boolean; reason?: string }interface Completion { verify: (result: RunResult) => Verdict | Promise<Verdict> maxAttempts: number // REQUIRED — an unbounded verify loop is a denial-of-service on the bill}interface Outcome { text: string status: "done" | "incomplete" | "pending" | "error" stoppedBy?: string // named whenever status is not "done" — never a silent stop attempts: number turns: number result?: RunResult}
function allTodosDone(result: RunResult): Verdictfunction loopUnsupported(spec: { uses?: { tools?: unknown[] }; team?: unknown[]; waitFor?: unknown; onMetric?: unknown }): string[]Agent.Loop(...).Run drives the agent under a Guardrail policy that vets every tool call and a
Completion check that decides when the task is done — the gated door beside the plain
Agent.Run, with unsupported spec fields (tools, team, waitFor, onMetric) named
explicitly rather than silently dropped. A Loop takes client options and a toolkit the
caller already built, not a constructed 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. See the
harness & loop guide for the full walkthrough of the placement law this encodes; this
page is the symbol reference.
When to use it
Section titled “When to use it”Reach for Loop when an agent needs a policy gate on its tool calls (a Guardrail that can deny
a call before it runs) or a verification gate on its completion (a Completion that keeps
re-prompting until the work actually checks out, up to maxAttempts) — and the agent does not
need to delegate to a team, suspend on a human via waitFor, or route its own metrics. Those four
spec fields are exactly what loopUnsupported names: a Loop-driven agent has no task tool and
cannot delegate at all.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — harness() is a name, not a wrapper
Section titled “1. The smallest useful call — harness() is a name, not a wrapper”import assert from "node:assert"import { agents } from "toolnexus"
const { harness } = agents as any
const spec = { does: "drafts release notes", soul: "You write terse, factual release notes." }assert.equal(harness(spec), spec, "harness is a name, not a wrapper — the identical object comes back")console.log("ok:", harness(spec).does)2. The realistic case — a Guardrail denies a call, and Completion gates on allTodosDone
Section titled “2. The realistic case — a Guardrail denies a call, and Completion gates on allTodosDone”An agent’s guardrails (compiled into beforeTool via guardedHooks internally) deny a
dangerous tool call outright; its completion re-prompts until every declared todo is checked
off, judging the ACCUMULATED work across retries so a later attempt cannot escape by dropping
the plan.
import assert from "node:assert"import { agents, createToolkit } from "toolnexus"
const { agent, allTodosDone } = agents as any
const say = (content: string) => ({ role: "assistant", content })const callTodo = (todos: any[]) => ({ role: "assistant", tool_calls: [{ id: "t1", type: "function", function: { name: "todowrite", arguments: JSON.stringify({ todos }) } }],})
function scripted(messages: any[]) { let i = 0 return async (_url: string, init: any) => { const message = messages[Math.min(i, messages.length - 1)] i++ return new Response( JSON.stringify({ choices: [{ index: 0, message, finish_reason: message.tool_calls ? "tool_calls" : "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, }), { status: 200, headers: { "content-type": "application/json" } }, ) }}
// Attempt 1 ends with an open todo (retried); attempt 2 closes it.const fetchImpl = scripted([ callTodo([{ id: "1", text: "draft", completed: false }]), say("I think I am finished"), callTodo([{ id: "1", text: "draft", completed: true }]), say("all done"),])
const tk = await createToolkit({ builtins: { tools: { todowrite: true, bash: false, read: false, write: false, edit: false, glob: false, grep: false, webfetch: false, apply_patch: false, question: false } },})const a = agent("gated", { does: "plans", guardrails: [(ev: any) => (ev.name === "bash" ? "policy: no shell in this agent" : "allow")], completion: { verify: allTodosDone, maxAttempts: 3 },})const out = await a.loop( { baseUrl: "http://scripted.invalid", style: "openai", model: "test-model", apiKey: "unused", fetch: fetchImpl }, tk,).run("do the thing")
assert.equal(out.status, "done")assert.ok(out.attempts >= 2, "the open todo forced a retry")await tk.close()console.log("ok:", out.status, out.attempts)3. The full surface — loopUnsupported names exactly what a Loop cannot honour
Section titled “3. The full surface — loopUnsupported names exactly what a Loop cannot honour”import assert from "node:assert"import { agents } from "toolnexus"
const { loopUnsupported } = agents as any
// A spec with all four Loop-unsupported fields set.const spec = { uses: { tools: ["some_tool"] }, team: ["helper"], waitFor: async () => ({ id: "x", ok: true }), onMetric: () => {},}const unsupported = loopUnsupported(spec)assert.deepEqual([...unsupported].sort(), ["onMetric", "team", "tools", "waitFor"])
// An empty spec loses nothing — the vocabulary is additive and advisory, never fatal.assert.deepEqual(loopUnsupported({}), [])
console.log("ok:", unsupported.join(", "))See also
Section titled “See also”- The harness & loop guide — the placement law (MAY it? / with WHAT? / DID it? / is it RIGHT?) and the full narrative walkthrough.
agents.Agent— Define a sub-agent with its own toolkit, prompt and budget, callable as a tool by its parent.agents.AgentRuntime— The six host verbs that drive sub-agents, plus the read-only list and inspect views.agents.Handle— The state machine for one spawned agent: pending, running, suspended, done.agents.Budget— Cap tool calls and wall-clock per agent and per team, enforced while the run is in flight.