Skip to content

Agents and Turns ​

An Agent is a model with instructions, which a Machine invokes to do the work of a state. Its definition tells who it is: its model, its instructions, and what it can do to the Workspace. A Turn is one answer of the Agent to one state. It holds the prompt of the state, the work of the Agent, and its pick from its Menu. This page tells what a definition holds, what a state sets for one Turn, and how a Turn ends.

Why the work is in Turns ​

A long conversation in which a model decides all the work is difficult to review and difficult to control. In jr2, each state of the Machine asks the Agent for one thing. The Agent does that thing, then picks an event. The Machine decides where the run goes next. Thus the Agent does one thing at a time, and the Console shows the state of each Turn.

A model cannot correctly control some parts of a Turn, so jr2 controls them. jr2 sets where the Agent works and how its Turn ends. It also decides what to do when a Turn does not end. You do not write that code, so you cannot forget it.

The definition ​

An Agent is an agent() under actors in jr2Setup: actors: { coder: agent({ … }) }. The key is the name of the Agent. A state invokes it with src: "coder". The definition is plain data, and it has these fields:

FieldWhat it tells
modelThe model, as <provider>/<model id>. It is required.
instructionsWho the Agent is and how it works. The model reads them on each Turn.
workspaceWhat the Agent can do to the Workspace: "write" (the default), "read" or "none".
thinkingLevelHow hard the model thinks, when the state sets no other level. The default is "off".
descriptionA short text for the people who read the Machine. The model does not read it.

AgentDefinition gives each field.

An Agent belongs to the Machine that holds it. Two Machines in one run can each hold an Agent named coder, and the two are different Agents. A Machine that you get from a package can leave its model open, for you to bind with customize(). Composition tells how.

instructions and workspace are the identity of the Agent. Only the definition sets them. A state cannot change them for one Turn, because then the name of the Agent would not tell what it is.

The Working tools ​

The Working tools are the file and shell tools of an Agent: read, write, edit, bash, grep and glob. The Harness runs them in its own container. The workspace field selects them:

  • "write" gives all the Working tools.
  • "read" gives no write tool and no edit tool. The Agent keeps bash, so a command can still write a file. A review Turn whose cwd is the review Worktree cannot move the branch with a change. Workspaces tells how.
  • "none" gives no Working tools. The Agent reads its prompt and picks from its Menu. This is a Menu-only Agent.

The field also tells where the Turn runs. An Agent with "write" or "read" works in the Sandbox of a Workspace, so a state invokes it in the body of a workspace(). A Menu-only Agent has no Sandbox. Its Turn runs on the Instance Harness, also when a workspace() holds it. Architecture shows where each Harness runs.

What a state sets for one Turn ​

The input of the invoke is the Turn. It has five keys, and no other:

  • The Frame: prompt and cwd. The prompt tells what this Turn is about. cwd is the directory where the Working tools work. A definition cannot hold a cwd, because the Worktree of a run exists only when the run starts. Under a Workspace with one Repo Slot, the default cwd is the Worktree of that slot.
  • The Dials: model and thinkingLevel. They tell how hard the Agent works on this Turn, and they replace the values of the definition for this Turn only. The Agent stays the same Agent.
  • continue. It tells if the Turn continues the conversation of the Agent. See Continue a conversation.

The state writes the input as a function of its context. Thus each Turn can have its own prompt and its own directory.

An example ​

In this Workflow, a coder fixes the tests of a Repo, and a Menu-only checker reads the coder's summary. When the checker asks for changes, the coder continues its conversation and thinks harder on that Turn:

ts
import { z } from "zod";
import { assign } from "xstate";
import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";

const finish = defineEvent({
  name: "finish",
  description: "Call this when `node --test` passes and you committed the fix. Say what you changed.",
  input: z.object({ summary: z.string() }),
});
const approve = defineEvent({
  name: "approve",
  description: "Call this when the summary shows a complete fix.",
  input: z.object({}),
});
const requestChanges = defineEvent({
  name: "request_changes",
  description: "Call this when the fix is not complete. Say what is missing.",
  input: z.object({ notes: z.string() }),
});

const runInput = z.object({ branch: z.string() });
type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
type Context = BodyInput & { summary: string; notes: string };

const body = jr2Setup({
  types: {} as { input: BodyInput; context: Context },
  events: [finish, approve, requestChanges],
  actors: {
    // The definition: who the Agent is. It is the same on every Turn.
    coder: agent({
      model: "anthropic/claude-haiku-5-5",
      instructions: "You fix failing tests. You change no test.",
    }),
    // A Menu-only Agent: no Working tools. It reads its prompt and picks.
    checker: agent({
      model: "anthropic/claude-haiku-5-5",
      instructions: "You read the summary of a fix, and you decide if the fix is complete.",
      workspace: "none",
      thinkingLevel: "low",
    }),
    fail: fail(),
  },
}).createMachine({
  id: "fix",
  context: ({ input }) => ({ ...input, summary: "", notes: "" }),
  on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
  initial: "coding",
  states: {
    coding: {
      invoke: {
        src: "coder",
        // The Frame (prompt and cwd) and continue. This Turn starts the coder's conversation.
        input: ({ context }) => ({
          prompt: "Make `node --test` pass, then commit.",
          cwd: context.workspace.repos.app,
          continue: true,
        }),
      },
      on: { finish: { target: "checking", actions: assign({ summary: ({ event }) => event.summary }) } },
    },
    checking: {
      invoke: { src: "checker", input: ({ context }) => ({ prompt: `Is this fix complete?\n\n${context.summary}` }) },
      on: {
        approve: "done",
        request_changes: { target: "fixing", actions: assign({ notes: ({ event }) => event.notes }) },
      },
    },
    fixing: {
      invoke: {
        src: "coder",
        // The same conversation as in coding, with a Dial: the coder thinks harder on this Turn.
        input: ({ context }) => ({
          prompt: `The fix is not complete: ${context.notes}\nFix it, then commit.`,
          cwd: context.workspace.repos.app,
          continue: true,
          thinkingLevel: "high",
        }),
      },
      on: { finish: { target: "checking", actions: assign({ summary: ({ event }) => event.summary }) } },
    },
    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 }),
});

The two Agents use the model of the tab. The coding and fixing states invoke the same Agent with different prompts, and only fixing sets a Dial. The checker's Turn has no cwd, because it has no Working tools.

One Turn ​

Orchestratorthe state that invokes the Agent
Harnessruns the Turn
model providerthe model of the Agent
the definition, the Frame and the Dials
the instructions, the prompt and the Briefing
zero or more times
a Working tool call
its result
a pick from the Menu
the pick, as an event
the answer to the pick
the answer
One Turn, from the state that invokes the Agent to the answer to its pick

The Briefing is the text that jr2 itself adds. The loop can run zero times: a Menu-only Agent has no Working tools. The answer to the pick tells the model if its Turn is over.

How a Turn ends ​

A Turn belongs to the state that invoked the Agent. When that state exits, the Turn is over, for any cause: a pick of the Agent, a Gate, a timer, or a CANCEL. jr2 then stops the work of the Agent on that Turn, and it does not prompt that Turn again.

Usually, a pick of the Agent moves the Machine out of the state, and that ends the Turn. A pick whose target stays in the state does not end the Turn. Then the Agent can pick again, one pick at a time. The Menu tells which picks an Agent can make, and when.

An agent() actor never finishes, so an onDone on its invoke never runs. Give the Agent a pick, such as finish, and route on it.

Continue a conversation ​

Without continue, each Turn starts a new conversation. The model knows only the instructions, the prompt and the Briefing. Thus the prompt must give all that the Agent needs to know.

With continue: true, the Turn continues the one conversation of this Agent in this Machine instance. Each state of the Machine that invokes the Agent with continue: true uses the same conversation. A Turn without continue does not join it. Thus, in the example, coding sets continue: true too, so that fixing continues the conversation of coding.

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. If the first Turn has no continue, attempt 2 does not see attempt 1. A first Turn with continue: true gets no second start on a Runaway (see When a Turn fails). jr2 accepts this, so that attempt 2 sees attempt 1.

continue is the literal true, not a boolean, so continue: context.attempts > 0 is a type error. A state that always starts a clean conversation leaves continue off. Spread it into the input 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 conversation has no name that you write. jr2 makes its id from the run, the Machine instance and the Agent. Thus two Agents never share a conversation. Two workers of a Pool never share one either, because each worker is a Machine instance of its own.

When jr2 sends agent.fault for a continued conversation, it does not continue that conversation again. The next Turn with continue: true starts a new one. Its prompt must then give the full task again, because jr2 cannot write that prompt for you. A Harness restart is the only exception: the restarted Harness keeps the conversation, so the next Turn continues it.

The Briefing ​

The Briefing is the text that jr2 itself gives the model. It tells facts that a correct Agent needs and cannot find for itself. It tells nothing about the task: the prompt tells the task. It has two parts:

  • The standing part comes after the instructions, and it is the same on each Turn. It tells how the Agent acts on the run: with a pick from its Menu. It also tells the CPUs and the Size of the Sandbox, or that a Menu-only Agent has no Working tools.
  • The Turn part comes after the prompt. It tells the directory of the Turn, its Allowed picks, and the notices. The Menu tells what the Allowed picks are. When they change during the Turn, the Harness tells them to the model again. When an Allowed pick carries a Chart, the Turn part also tells how a Chart runs (see Charts).

A notice tells the next Turn what happened before it. For example: the conversation is new and the earlier context is gone, or the processes of the Sandbox stopped at its memory limit. jr2 gives each notice one time, to the next Turn that it is for.

You cannot turn the Briefing off. It contains only facts that make the Agent work better. An instructions that tells the same fact again does no harm.

When a Turn fails ​

jr2 tries to recover a Turn before it tells the Machine. When it cannot recover the Turn, it sends one event, agent.fault, to the Machine. The Machine decides what to do next: the state that invokes the Agent, or a state above it, routes agent.fault. jr2 check refuses a Machine without that route. In a Chart the route is optional. jr2 recovers from these failures:

  • The Harness or the model provider fails. The Harness tries the model provider again during the Turn. When the Turn still fails, jr2 sends agent.fault. When a Harness restarts while the state is busy, jr2 starts the Turn again on the same conversation, and it sends no fault. If the restarted Harness cannot read the conversation, the Turn starts a new one, and the Briefing tells the model.
  • The Agent stops with no pick. When the model stops and its Turn can still make a pick, jr2 prompts it again. It does this two times at most. Then it sends agent.fault.
  • The Turn is a Runaway.

A Runaway is a Turn that does not end by itself. The Harness ends a Turn when it makes the same tool call four times in sequence, with the same arguments. It also ends a Turn of more than 256 steps on one prompt. A step is one tool call that starts while the Turn is live: a call of a Working tool, or a pick. A call that does not run is a step too, for example a call of a tool that does not exist. A call after a pick that ended the Turn is not a step. Then jr2 starts the Turn again one time, with the same prompt, on a new conversation. When that Turn is also a Runaway, jr2 sends agent.fault. jr2 does not start the Turn again when it continues a conversation, or when the Agent already made a pick in it. Then it sends agent.fault immediately.

You cannot change these limits. They are a part of jr2.

Compaction ​

A long Turn can fill the context window of the model. Then the model cannot answer, and the Turn fails. To prevent this, the Harness takes a Compaction during the Turn. Before it sends the conversation to the model, it examines the size of the context. When the context is near its limit, the Harness replaces the earlier part of the conversation with a summary. The recent part stays. The Turn continues with the smaller context.

The summary comes from the model and the thinking level of the Turn. A Compaction changes only what the model sees. The history of the conversation keeps all that was said.

You do not set when a Compaction occurs. jr2 calculates it from the context window and the output limit of the model. A model of a catalog provider, such as anthropic, has a known context window. A custom provider gets it from harness.provider.contextWindow or from the contextWindow of the model in models. A model with no context window gets no Compaction.

Rules ​

  • model is required in each definition. jr2 up refuses a model that is still open.
  • The input of a Turn has only prompt, cwd, model, thinkingLevel and continue. jr2 refuses a Turn with a different key.
  • A Turn cannot set instructions or workspace.
  • cwd is an absolute path. The Harness refuses a Turn with a relative cwd.
  • Under a Workspace with more than one Repo Slot, each Turn of an Agent with Working tools sets cwd. jr2 refuses the Turn without it.
  • An Agent with "write" or "read" runs in the body of a workspace(). jr2 refuses its Turn outside one.
  • jr2 up sees only the models of the definitions. It cannot see a model that a Turn sets as a Dial. The Harness refuses a model that it cannot find when the Turn starts.
  • A Turn ends when the state that invoked the Agent exits.
  • After agent.fault for a conversation, jr2 does not continue it, except after a Harness restart.

Set an Agent's access and Dials gives the exact text of each refusal.

The docs of Kit version 0.5.1. Each page is also at its .md URL.