The Menu
The Menu of an Agent is the set of tools that the Agent uses to speak to its Machine. Each tool is one workflow event of the Machine. The Agent calls one tool, with its input. This call is a pick. The Machine decides what the pick does.
Why the Menu exists
When a model decides the next step itself, you cannot read the control flow before a run starts. In jr2, the Machine holds the control flow. The model chooses only from the transitions that the Machine has. A pick that the Machine does not take moves nothing. Thus the graph that you read in the Console is the full set of paths that a run can take.
The Menu does not change during a conversation. A model provider keeps the start of a conversation in a prompt cache, and the tools block is first in it. A change to the tools block makes the provider read the full conversation again. Thus jr2 keeps the Menu the same, and tells each Turn which part of it the Turn can pick now.
Where the Menu comes from
You do not write the Menu. jr2Setup makes the Menu from the Machine when you call createMachine:
- A state that invokes an Agent gives the Agent the events of its own
onkeys and of theonkeys of its parent states. - It also gives the events of the
onkeys of its child states. A child state that invokes nothing is a mode. A child state that runs an Effect or a child Machine also gives its events. - A child state that invokes a different Agent, or a Gate, does not give its events. Those events belong to that Agent or to that Gate.
- When two or more states invoke the same Agent slot, its Menu has the events of all of them.
- The
descriptionof eachdefineEventis the description of its tool. Make it true at each state that takes the event.
The position of an on key decides who can send its event. The event belongs to the party (an Agent or a Gate) that its state, or the nearest state above it, invokes. An on key with no such state above it belongs to both parties. Set audience on an event only to give it to the other party too, or to keep an event at the root from one party.
A worked example
In this Workflow, a writer drafts release notes. It can ask an editor a question, and it finishes with done:
import { assign } from "xstate";
import { z } from "zod";
import { agent, answer, defineEvent, fail, jr2Setup } from "@jr2/orchestrator";
// The writer's picks. Each one is a tool on the writer's Menu.
const ask = defineEvent({
name: "ask",
description: "Ask the editor one question. The editor's reply is the answer to this call.",
input: z.object({ question: z.string().describe("One question for the editor.") }),
});
const done = defineEvent({
name: "done",
description: "Call this once, with the finished release notes. Then stop.",
input: z.object({ notes: z.string().describe("The release notes, in Markdown.") }),
});
// The editor's reply. The editor sends it to a Gate, so it is not on the writer's Menu.
const reply = defineEvent({
name: "reply",
description: "Answer the writer's question.",
input: z.object({ text: z.string().describe("The answer, in prose.") }),
});
const door = z.object({ changes: z.string().describe("The changes in this release, one on each line.") });
type Input = z.infer<typeof door>;
type Ctx = Input & { question?: string; replies: number; notes?: string };
export const machine = jr2Setup({
types: {} as { context: Ctx; input: Input; output: { notes: string } },
events: [ask, done, reply],
actors: {
writer: agent({
model: "anthropic/claude-haiku-5-5",
instructions: "You write short release notes. Ask the editor at least one question before you finish.",
workspace: "none",
}),
fail: fail(),
},
}).createMachine({
id: "notes",
input: door,
context: ({ input }) => ({ changes: input.changes, replies: 0 }),
on: { "agent.fault": ".failed" },
initial: "writing",
states: {
// The writer's state. Its Turn lasts until `done` leaves it.
writing: {
invoke: {
src: "writer",
input: ({ context }) => ({ prompt: `Write the release notes for these changes:\n${context.changes}` }),
},
on: {
// The guard keeps `done` out of the Allowed picks until the editor has replied once.
done: {
guard: ({ context }) => context.replies > 0,
target: "published",
actions: assign({ notes: ({ event }) => event.notes }),
},
},
initial: "drafting",
states: {
// A mode: it invokes nothing, so the writer is at Rest here and can pick `ask`.
drafting: {
on: { ask: { target: "asking", actions: assign({ question: ({ event }) => event.question }) } },
},
// While this Gate is open, the `ask` pick is outstanding and the writer can pick nothing.
asking: {
invoke: { src: "gate", input: ({ context }) => ({ meta: { question: context.question } }) },
on: {
reply: {
target: "drafting",
actions: [
assign({ replies: ({ context }) => context.replies + 1 }),
// The writer reads this text as the answer to its `ask`.
answer(({ event }) => `The editor replied: ${event.text}`),
],
},
},
},
},
},
published: { type: "final" },
failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
},
// The output of the run: the root `output`, not an `output` on the final state.
output: ({ context }) => ({ notes: context.notes ?? "" }),
});The writer's Menu is ask and done. The reply event belongs to the Gate in asking, so it is not on the Menu. jr2 up prints who can send what at each state. The Console shows the same seats in the "who may send what" panel of the graph:
…
seats: workflow "notes" — who may send what, per state (ADR-0015)
notes.writing agent:writer: [ask, done]
notes.writing.asking gate: [reply]
…Allowed picks
The Allowed picks are the part of the Menu that one Turn can pick now. jr2 reads them from the live Machine. They change with the active mode and with the guards. The Menu does not change.
In the example, one Turn can have three sets of Allowed picks:
| The writer is in | Allowed picks | Why |
|---|---|---|
drafting, before a reply | ask | The guard on done is false. |
asking | none | The ask pick is outstanding. The Gate is still open. |
drafting, after one reply | ask, done | The guard on done is now true. |
The Agent reads its Allowed picks after the prompt of each Turn. It reads them again in the answer to each pick that does not end the Turn. When the Allowed picks change while the Agent works, it reads them again before its next step. Agents and Turns tells how jr2 gives this text to the model.
A guard that reads only the context decides the Allowed picks exactly. A guard that reads the input of the pick cannot decide before the pick. jr2 keeps that pick in the Allowed picks, and the guard decides when the pick comes.
One pick at a time
A pick that leaves the state that invokes the Agent ends the Turn. In the example, done goes to published and ends the Turn. A pick whose target is inside that state keeps the Turn open. A pick with no target also keeps the Turn open. Thus one Turn can hold many picks, and the last one leaves the state.
A pick that starts work inside the Agent's state is outstanding until that work is done. In the example, ask starts a Gate in asking, and it is outstanding until the editor replies. While a pick is outstanding, jr2 refuses each other pick of the Agent, and delivers nothing.
jr2 answers an outstanding pick when the Agent's state is at Rest. At Rest, no state below the Agent's state runs something: an Effect, a Gate, a different Agent or a child Machine. Also, no timer is set that moves the Machine to a different state inside the Agent's state. Rest is a condition that jr2 reads from the Machine. A state with the name rest has no special meaning.
When something else leaves the Agent's state while the work of a pick runs, the work stops and the Turn ends. A timer, a Gate or an outside event can do this. The answer tells the Agent that its pick did not finish.
The answer to a pick
The answer comes in this order. The work of the pick finishes, then the Machine runs its route (the guards and the actions), then jr2 answers the Agent. The answer has the Allowed picks that jr2 reads after the route.
By default, the answer is the last reply since the pick. A reply is the output of a child that ran below the Agent's state. An event from a party below it is also a reply: a Gate's event, or the pick of a nested Agent. Every reply since the pick, oldest first, is the Chain.
To give the Agent a different answer, write answer on the route. It is an xstate action. It reads the context, the event and the Chain, and its value is the answer. In the example, the writer reads The editor replied: <text> and not the raw reply event. Where you write answer decides which pick it answers. The Reference entry gives the rule.
The error of a child is not a reply, and the Agent does not see it. To tell the Agent about an error, write answer on the onError route.
When the route leaves the Agent's state, the Turn ends, and jr2 keeps the answer as a notice. If the Turn that made the pick continues a conversation (continue: true), the next Turn on that conversation reads the notice. If it does not, no Agent reads the answer.
Keep the Turn or end it
The target of a pick decides who controls the order of the work. When the target is inside the Agent's state, the Agent decides when each step runs. When the target is outside it, the Machine decides the next step. Choose the target by what the Agent must do next:
| The Agent must | Put the target of the pick | The Agent reads the result |
|---|---|---|
| use the result of a step to continue its work | in a child state that runs the step | as the answer to the pick, in this Turn |
| make only the picks that are correct now | in a mode, or behind a guard that reads the context | in its Allowed picks, after each answer |
| stop, because the Machine decides the next step | in a state outside the Agent's state | in the prompt of a later Turn |
In the example on this page, the writer uses the first two kinds. ask opens a Gate in asking, and the writer reads the editor's reply as the answer to ask, in the same Turn. The guard on done keeps done out of the Allowed picks until the editor replies. Then done leaves writing, and that ends the Turn.
Use the third kind when the next step is not the choice of the Agent. Use it also when the next step needs a different prompt, a different Agent or different Dials. On the transition, assign the result to the context, and write the prompt of the next state from the context. When the Agent must keep its conversation, set continue: true on its Turns (see Continue a conversation). In the example of Agents and Turns, request_changes moves the run to fixing. The prompt of fixing gives the notes of the checker to the coder.
The first two kinds keep the Agent in its work: the result is the answer to its own call, not a new task. The third kind gives the Agent a new task. One Machine can use all three kinds.
The Sender
Each event that a party sends carries its Sender, as event.sender. The Sender is { party, id }: an Agent by its slot, or a Gate by its invoke id (see Sender). The Orchestrator writes it on each event that a party delivers. An event that the Machine sends to itself has no Sender.
Use the Sender when two parties can send the same event to one state. The guard sentBy(id) is true for an event from that party, and for an event with no Sender. It is false for an event from a different party, so xstate tries the next transition. sentBy can only make a transition refuse other senders. It never gives an event to a party. The position of the on key does that.
Rules
- jr2 refuses a pick that is not on the Menu.
- A pick that is on the Menu but not in the Allowed picks moves nothing. Its answer gives the Allowed picks.
- While a pick is outstanding, or while the Agent's state is not at Rest, jr2 refuses each pick and delivers nothing. This includes a pick that would end the Turn.
- An Agent cannot send an event to a Gate. Only the Gate's caller can.
- No party can write the Sender.
defineEventrefuses an input with asenderfield, and the Orchestrator refuses a body that has one. createMachinerefuses anonkey that no listed event has, and a listed event that no state takes.createMachinerefuses a parallel state at or below a state that invokes an Agent.createMachinerefuses asentBythat names no Agent slot and no Gate invoke id of the Machine.jr2 upwarns about a mode that no transition enters. Its picks are on the Menu, but never in the Allowed picks.jr2 upwarns about a Gate below an Agent's state whose event has no target. The Gate stays open after that event, so the Agent's state does not come to Rest. jr2 refuses each pick of the Agent until the Gate's state exits.
Read next
- Write a Workflow
- Set an Agent's access and Dials
- Run a command or call a URL with an Effect
- Agents and Turns, Gates and Effects
- Reference:
jr2Setup,defineEvent,answer,sentByandEventAudience - ADR-0006: the Agent picks from a Menu that the Machine defines
- ADR-0015: the Menu comes from the Machine, and the position of an
onkey gives it to a party - ADR-0024: a Turn ends with its state, and an outstanding pick is answered at Rest
- ADR-0029: the Menu does not change, and a Turn reads its Allowed picks
- ADR-0067:
answer, and the Chain