agent
Function, in @jr2/orchestrator/src/harness-client.ts. It is an export of @jr2/orchestrator, on the Authoring surface page, in Agents.
function agent(declaration: AgentDeclaration): AgentLogicMake an Agent: an actor slot that runs one Turn of a model each time a state invokes it. Put it under actors in jr2Setup, and invoke it by its key: actors: { coder: agent(…) }, then invoke: { src: "coder", input: { prompt } }. The key is the Agent's name.
The invoke's input is the Turn: prompt (required), cwd, model, thinkingLevel and continue. cwd is the absolute directory where the Working tools work. Absent, it is the Worktree of the Workspace's one Repo Slot. continue: true continues the Agent's conversation in this Machine. Absent, each Turn starts a new conversation. continue is the literal true, not a boolean. A Turn can join only a conversation that an earlier Turn started with continue: true. Thus in a retry loop, put continue: true on each Turn, also on the first, so that attempt 2 sees attempt 1. Then a Runaway on the first Turn gets no second start, because a continued Turn gets none (ADR-0035). Spread it only when a state must start a clean conversation on some Turns only: ...(context.clean ? {} : { continue: true as const }). A clean Turn does not start the continued conversation again: the next Turn with continue: true joins the earlier one. The Turn ends when the state that invoked the Agent exits, usually on a pick from the Agent's Menu.
The actor of an Agent never finishes, so an onDone on its invoke never runs. Give the Agent a pick, such as finish, and route on it. When a Turn fails with no pick, jr2 sends the event agent.fault to the Machine. It is an event, not an error of the invoke, so an onError does not get it. Route it on each state that invokes an Agent, or once on a state above them: on: { "agent.fault": "failed" }. jr2 check and jr2 up refuse a state that invokes an Agent with no route for it, and an agent.fault that no transition takes ends the run error.
An Agent with workspace: "write" or "read" works in a Sandbox, so invoke it inside a workspace() body. An Agent with workspace: "none" can run anywhere. A model can lack the definition's thinking level: see ThinkingLevel.
When a Turn starts, it refuses:
- a key in
inputthat a Turn does not have:agent "<slot>": the Turn's input carries `<key>`, which a Turn has no use for — its input is its Frame (`prompt`, `cwd`), its Dials (`model`, `thinkingLevel`), and `continue` (ADR-0057) - no
cwdunder a Workspace with more than one Repo Slot:turn <id>: agent "<slot>" works under a Workspace carrying more than one Repo Slot (<slots>) and its Turn framed no `cwd` — say which Worktree it works in (`cwd: context.workspace.repos.<slot>`, one of those), because the kit gives no slot a meaning (ADR-0051) and a Turn's Frame is where it works (ADR-0057) - a
cwdthat is not an absolute path:agent "<slot>": the Turn's Frame carries a `cwd` that is not an absolute path — the Frame says WHERE this Turn works (ADR-0057), and a Turn rooted anywhere else would work in the wrong directory silently. The Harness makes this refusal when it admits the Turn, and the error starts withharness admission failed (400):. - an Agent with Working tools outside a
workspace():turn <id>: agent "<slot>" has workspace: "<access>" — invoke it inside a workspace() (ambient resolution), or pass an explicit `endpoint` (workspace-less stub path) - a
modelthat is stillopen:agent "<slot>" has an Open model — nobody bound it; bind it where the Machine is registered (`customize(<machine>, { agents: { <slot>: { model: "<provider>/<model>" } } })`), because a packaged Machine cannot pick a model on your behalf (ADR-0054).jr2 uprefuses this before it builds anything.
Parameters
declaration(AgentDeclaration): The Agent's definition (seeAgentDefinition).modelmay beopenin a Machine that you publish as a package.
Returns: The actor logic of the Agent's slot.
Example
import { z } from "zod";
import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
const finish = defineEvent({
name: "finish",
description: "Call this when `node --test` passes.",
input: z.object({}),
});
const runInput = z.object({ branch: z.string() });
type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
const body = jr2Setup({
types: {} as { input: BodyInput; context: BodyInput },
events: [finish],
actors: {
coder: agent({
model: "anthropic/claude-haiku-5-5",
instructions: "You fix failing tests. You change no test.",
}),
fail: fail(),
},
}).createMachine({
id: "fix",
context: ({ input }) => input,
on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
initial: "coding",
states: {
coding: {
invoke: {
src: "coder",
input: ({ context }) => ({
prompt: "Make `node --test` pass, then commit.",
cwd: context.workspace.repos.app,
thinkingLevel: "high",
}),
},
on: { finish: "done" },
},
done: { type: "final" },
failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
},
});
export const machine = workspace(body, {
input: runInput,
repos: { app: "https://github.com/you/app.git" },
spec: ({ input }) => ({ branch: input.branch }),
});See also