---
title: agent
description: "Make an Agent: an actor slot that runs one Turn of a model each time a state invokes it."
---

# `agent`

Function, in `@jr2/orchestrator/src/harness-client.ts`. It is an export of `@jr2/orchestrator`, on the [Authoring surface](../authoring-surface.md) page, in Agents.

```ts
function agent(declaration: AgentDeclaration): AgentLogic
```

Make 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](../decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md)). 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`](type-thinkinglevel.md).

When a Turn starts, it refuses:
- a key in `input` that 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 `cwd` under 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 `cwd` that 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 with `harness 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 `model` that is still `open`: ``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 up` refuses this before it builds anything.

**Parameters**

- `declaration` ([`AgentDeclaration`](type-agentdeclaration.md)): The Agent's definition (see [`AgentDefinition`](type-agentdefinition.md)). `model` may be `open`
  in a Machine that you publish as a package.

**Returns**: The actor logic of the Agent's slot.

**Example**

```ts
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**

- [Agents and Turns](../../concepts/agents-and-turns.md)
- [Write a Workflow](../../guides/write-a-workflow.md)
- [ADR-0049: A Machine carries its parts and composes by invoke](../decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md)
- [ADR-0057: A Turn is its Frame, its Dials, and whether it continues](../decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md)
