Skip to content

jr2Setup ​

Function, in @jr2/orchestrator/src/setup.ts. It is an export of @jr2/orchestrator, on the Authoring surface page, in Machine and events.

ts
function jr2Setup<
  TContext extends MachineContext = MachineContext,
  const TDefs extends readonly EventDef[] = readonly [],
  TActors extends Record<string, UnknownActorLogic> = {},
  TActions extends Record<string, ParameterizedObject["params"] | undefined> = {},
  TGuards extends Record<string, ParameterizedObject["params"] | undefined> = {},
  TDelay extends string = never,
  TTag extends string = string,
  TInput = NonReducibleUnknown,
  TOutput extends NonReducibleUnknown = NonReducibleUnknown,
  TEmitted extends EventObject = EventObject,
  TMeta extends MetaObject = MetaObject,
>(
  def: {
    types?: {
      context?: TContext;
      input?: TInput;
      output?: TOutput;
      tags?: TTag;
      emitted?: TEmitted;
      meta?: TMeta;
    };
    events?: TDefs;
    actors?: TActors;
    actions?: {
      [K in keyof TActions]: ActionFunction<
        TContext,
        WorkflowEvent<TDefs>,
        WorkflowEvent<TDefs>,
        TActions[K],
        ToProvidedActor<MergedActors<TActors>>,
        ToParameterizedObject<TActions>,
        ToParameterizedObject<TGuards>,
        TDelay,
        TEmitted
      >;
    };
    guards?: {
      [K in keyof TGuards]: GuardPredicate<TContext, WorkflowEvent<TDefs>, TGuards[K], ToParameterizedObject<TGuards>>;
    };
    delays?: {
      [K in TDelay]: DelayConfig<
        TContext,
        WorkflowEvent<TDefs>,
        ToParameterizedObject<TActions>["params"],
        WorkflowEvent<TDefs>
      >;
    };
  } = {},
): SetupReturn<
  TContext,
  WorkflowEvent<TDefs>,
  MergedActors<TActors>,
  {},
  TActions,
  TGuards & SentByGuards,
  TDelay,
  TTag,
  TInput,
  TOutput,
  TEmitted,
  TMeta
>

Set up a Machine that takes workflow events: the jr2 form of xstate's setup(). Write a Machine with it when an Agent or a Gate sends events into the Machine, or when the Machine declares a door. Call .createMachine(config) on the result, as with setup(). The Machine you get is a plain xstate Machine.

What it adds to setup():

  • events replaces types.events. The event type of the Machine comes from the defineEvent events you list, plus the events jr2 itself sends (agent.fault, workspace.lost). An event that a party sent has event.sender. A Machine that takes no workflow event leaves out events.
  • The gate actor is ready to invoke by name: invoke: { src: "gate" }.
  • Each Agent slot (actors: { coder: agent(…) }) gets its Menu from the states that invoke it.
  • The sentBy(id) guard is ready to use.
  • createMachine also takes input: a zod object for the input that starts a run. jr2 checks each start against it. Without it, a run can start with any input. The setup() of xstate does not read this input, so a Machine with a door uses jr2Setup, also when it takes no workflow event. jr2 up and the publish door refuse a registered Machine that has a zod input if jr2Setup did not make the Machine: workflow "<name>": machine "<id>" has a zod `input` on createMachine, but jr2Setup did not make this Machine, so no door reads that input …

The output of a run is the root output of createMachine, beside id and states, as in xstate v5. xstate does not read the output of a final state as the output of the Machine. It gives that output to the root output as event.output. Write output: ({ event }) => event.output as Output at the root, or make the output from context there. createMachine refuses an output on a final state directly below the root when the root has no output (see the list below).

createMachine refuses a Machine that it cannot run as written. Among the refusals:

  • an on key that no listed event has: machine "<id>" handles event "<key>", which no def in jr2Setup({ events }) declares (declared: <names>) — a typo, or a missing defineEvent
  • a listed event that no state takes: machine "<id>" declares "<name>", which no state handles — a pick no state takes is on no Menu and no Gate: handle it, or drop it from jr2Setup({ events })
  • an entry of events that defineEvent did not make, or one name listed twice: machine "<id>": `events` entry is not a defineEvent() def: <entry>, machine "<id>": duplicate event "<name>" in `events`
  • an agent(), an Effect or a workspace() written on invoke.src instead of under actors: machine "<id>": state "<state>" invokes <part> inline — a Part is a key of actors, so it has a name that the ledger, the report and customize() share: actors: { <name>: <factory>(...) }, then invoke: { src: "<name>" }. <part> is an Agent, an Effect or a Workspace. <factory> is agent, the Effect's kind (such as command), or workspace.
  • an output on a final state directly below the root, when the root has no output: machine "<id>": final state "<state>" declares an output, and the root has no output — xstate v5 gives a run only its root output, so this output reaches nothing and the run ends with none: write it at the root, as output: ({ context }) => ..., or keep it on the final state and pass it on with output: ({ event }) => event.output at the root. With more than one such final state, it names each one and puts the words in the plural: final states "<state>", "<state>" declare an output, these outputs reach nothing, write one at the root and keep them on the final states and pass them on. An output on a final state inside a compound state is legal: the onDone of that compound state gets it as event.output.
  • a parallel state at or below a state that invokes an Agent: machine "<id>": state "<state>" is parallel, at or below Agent "<slot>"'s state "<state>" — a parallel region below an Agent's state is reserved for picks the Agent does not wait for, which jr2 does not build; background work goes above the Agent's state
  • a state below an Agent's state that invokes something and that nothing enters: machine "<id>": state "<state>" below Agent "<slot>"'s state "<state>" is entered by nothing, so what it invokes (<src>) never runs — target it from a pick or a route, or delete it
  • a state that invokes an Agent when that state and each state above it have no agent.fault route: machine "<id>": state "<state>" invokes an Agent, and no route for "agent.fault" is on it or on a state above — jr2 sends agent.fault when the Agent's Turn cannot go on, and with no route the run faults: route it on that state or on a state above it, as on: { "agent.fault": "<the state that handles it>" } (ADR-0074). A state that invokes an Open body that lends an Agent or allows a model gets the same line, with invokes an Open body that can run an Agent: the body sends it a Chart's unrouted agent.fault. When both kinds have no route, one line names both, joined by , and. One route on the root is a route for each state. A Chart's Machine does not get this refusal: in a Chart the route is optional (ADR-0070).

It also refuses a sentBy that names no party (see sentBy). It refuses a pick param that nothing reads, where the pick enters a state that invokes an Effect (see fromPick). Shapes that are legal but probably not what you mean, such as a mode that nothing enters, are warnings that jr2 up prints.

Parameters

  • def: What setup() takes, with events in place of types.events. A Machine that needs none of it calls jr2Setup().

    • types (optional): object

      The types of the Machine, as in setup(): context, input, output, tags, emitted and meta. Not events: the event type comes from events.

      • context (optional): TContext
      • input (optional): TInput
      • output (optional): TOutput
      • tags (optional): TTag
      • emitted (optional): TEmitted
      • meta (optional): TMeta
    • events (optional): TDefs

      The workflow events this Machine takes: each one made by defineEvent. Leave it out when the Machine takes none. Each listed event must be the name of an on key. Each on key must be listed, if it is not a jr2 or xstate event (an event with a . in its name).

    • actors (optional): TActors

      The actors the states invoke by name, as in setup(): Agent slots (agent(…)), Effects (command(…), http(…), push(…)), child Machines, workspace(…) and plain xstate logic. The key is the slot's name. An actor named gate replaces jr2's Gate.

    • actions (optional): { [K in keyof TActions]: ActionFunction<TContext, WorkflowEvent<TDefs>, WorkflowEvent<TDefs>, TActions[K], ToProvidedActor<MergedActors<TActors>>, ToParameterizedObject<TActions>, ToParameterizedObject<TGuards>, TDelay, TEmitted>; }

      Named actions, as in setup(). Their event has the type of this Machine's events.

    • guards (optional): { [K in keyof TGuards]: GuardPredicate<TContext, WorkflowEvent<TDefs>, TGuards[K], ToParameterizedObject<TGuards>>; }

      Named guards, as in setup(). sentBy is there already; do not name a guard jr2.sentBy.

    • delays (optional): { [K in TDelay]: DelayConfig<TContext, WorkflowEvent<TDefs>, ToParameterizedObject<TActions>["params"], WorkflowEvent<TDefs>>; }

      Named delays, as in setup().

Returns: What setup() returns. Call .createMachine(config) on it.

Example

ts
import { z } from "zod";
import { agent, defineEvent, fail, jr2Setup } from "@jr2/orchestrator";

const proposed = defineEvent({
  name: "proposed",
  description: "Call this with your plan.",
  input: z.object({ plan: z.string() }),
});
const approve = defineEvent({ name: "approve", input: z.object({}) });
const reject = defineEvent({ name: "reject", input: z.object({ reason: z.string() }) });

export const machine = jr2Setup({
  types: {} as { input: { goal: string }; context: { goal: string } },
  events: [proposed, approve, reject],
  actors: {
    planner: agent({
      model: "anthropic/claude-haiku-5-5",
      instructions: "You plan small releases.",
      workspace: "none",
    }),
    fail: fail(),
  },
}).createMachine({
  id: "plan",
  input: z.object({ goal: z.string() }),
  context: ({ input }) => ({ goal: input.goal }),
  on: { "agent.fault": ".failed" },
  initial: "planning",
  states: {
    planning: {
      invoke: { src: "planner", input: ({ context }) => ({ prompt: `Plan this: ${context.goal}` }) },
      on: { proposed: "review" },
    },
    review: {
      invoke: { src: "gate" },
      on: { approve: "approved", reject: "planning" },
    },
    approved: { type: "final" },
    failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
  },
});

See also

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