# A Machine carries its parts and composes by invoke

A workflow could not use another workflow's Machine as a child without re-declaring the child's events at its own root
(the `triaged-task` example documented the workaround), and a Machine had no way to travel: its Agents lived in the
Instance's roster by name and its Sandbox Image in the Instance's `images/`, so a packaged Machine depended on someone
else's config holding the right strings. The grill of 2026-09-06/09 settled composition and packaging as one decision,
because they are one problem: **what a Machine depends on that is not inside it.**

## Decision

- **Composition is Machine composition.** A workflow module imports another module's exported Machine and invokes it as
  an xstate child (`actors: { deep, quick }`, `invoke: { src: "deep" }`). One run, one snapshot, one run id; the child's
  Gates list under the parent's run and its emits ride the same feed. There is no sub-run and no
  invoke-by-registered-name: a nested Machine is reached by `import`, never by the name `jr2 run` uses. Rejected:
  Temporal-style child workflows with their own run ids — a new actor kind, cross-run correlation, and cancel
  propagation, for no requirement that needs a second run id. A run that STARTS a run is not composition: it is the
  `dispatch` Effect ([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)), by the name `jr2 run` uses, and
  the two runs are independent. Two uses, two mechanisms.
- **Everything a Machine depends on rides the Machine, scoped to it.** Vocabulary already did (ADR-0011, per-Machine
  since this decision). The two remaining dependencies join it:
  - **An Agent is an actor slot.** `jr2Setup({ actors: { researcher: agent({ model, instructions, … }) } })`, invoked as
    `src: "researcher"` with `{ prompt, …dials }` as input. `agent(definition)` is `agentRun` closed over one definition
    and branded with it; `agentRun` and `agent: "name"` leave the authoring surface. The typo check is xstate's own
    `src` typing — a slot the Machine does not declare is a compile error with nothing added — and Stately shows
    `researching → invoke researcher` instead of a uniform `agentRun`. Two Machines in one run may both carry a
    `researcher`; each resolves against its own implementations, and the Instance ID's actor path keeps their
    conversations apart.
  - **An Effect is an actor slot too**
    ([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)), and its plain-data
    parts ride the slot: the kind, where it runs, a `command`'s argv, an `http` Effect's method, fixed host and the
    names of the secrets it names — or the `open` sentinel where a package left the url, the argv or a secret's name for
    its composer ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). `jr2 up` reads them
    without running anything, and a secret named there is what places it.
  - **The Sandbox Image is a `workspace()` option**, static: `workspace(body, { input, image, repos, spec })` (`repos`
    is ADR-0051's, and follows the same rule). `image` is a string in one of two shapes — a `file:` URL
    (`import.meta.resolve("./image")`, a docker context the module ships) or a registry ref — or absent, which keeps
    ADR-0037's fallback. It moved out of the per-run spec because `jr2 up` must find it statically. It is never
    persisted: the provisioning state re-reads it off the Machine on restore.
- **The definition rides the Turn.** The admission body carries the slot's definition; the Harness runs what it was
  handed and re-reads it per Submission as before. The Instance roster (`agents/`, then `config.agents`), its ConfigMap,
  and `JR2_AGENTS_JSON` retire — a flat roster cannot hold two `researcher`s, and a Machine edit already needs `jr2 up`
  to publish the Assembly again, so the ConfigMap bought nothing a Machine-carried definition loses. Placement
  (ADR-0031) reads `workspace: "none"` off the slot's definition.
- **Parts resolve at invoke time through the live actor's logic, never a build-time closure.** `agentRun` finds its
  definition in the logic it was invoked as; `provision` reads the image off the wrapper's parts map keyed on
  `machine.config`. This is the one rule that lets xstate's `provide()` and customization coexist, and the same rule
  ADR-0011 adopted for Vocabulary.
- **Customization is `customize(machine, parts)`**, a plain function in the family of `workspace()` and `pool()`,
  returning a plain `StateMachine`. `parts` has the declaration's own shape, recursively partial:
  `{ agents?: { <slot>?: Partial<AgentDefinition> }, effects?: { <slot>?: { url } | { argv } | { secret } }, image?, user?, repos?: { <slot>?: Binding }, actors?: { <child>?: parts } }`
  — the two image seats together, since ADR-0005 gives the User Container the same static option, and the Repo Slots
  beside them, since ADR-0051 makes a Repo a `workspace()` slot a composer binds. `effects` binds an Effect's Open url,
  argv or secret name, once — an Effect with an Open secret and an Open url or argv takes one object with both keys,
  `{ url, secret }` or `{ argv, secret }`
  ([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)): the slot's logic is built
  again with the bound part and substituted as an Agent override is. Internally an Agent override is xstate's
  `machine.provide({ actors: { researcher: agent(merged) } })`, with the override layered over the stock definition; a
  child override is the same call one level down, recursing — each level is exactly the one-level reach ADR-0015 found
  `provide` has, held by the composer who owns the child object, never host-side injection. jr2's wrappers are
  transparent: `customize(research, { agents })` reaches the body through the `body` slot (and `pool()`'s `worker`, at
  any depth), so a consumer never writes `body`. The images and the Repo bindings are the parts `provide` cannot carry
  (xstate copies only implementations, and every part is keyed on the config it passes through by reference), so those
  fields clone the wrapper's config and rebuild it with the same implementations, re-attaching what the original
  carried. A bound argv's load warnings are such a part too: `customize()` rebuilds the Machine the author wrote the
  same way, with the vocabulary, the seats and the door it carried and the warnings the walk gives for the bound argv,
  so no attachment is keyed on anything but the config. Only DECLARED parts can be retuned, and the compiler is what
  says so: the slots are read off xstate's own `TActor` parameter, so an Agent, a Repo Slot, or a child the Machine does
  not carry is a type error (ADR-0050), and the runtime refuses the same call naming the Machine's own slots. Rejected:
  a `.with()` method on the machine — it needs a jr2-owned machine type over xstate's, which ADR-0015 avoided, and it
  vanishes after any `.provide()`; a callable-machine hybrid — verified to work, reads as a trick.
- **A package exports a Machine, nothing beside it.** The door (ADR-0033), the vocabulary, the Agents, and the image all
  ride the exported object. The three uses:

  ```ts
  export const machine = research;                                                            // as-is
  export const machine = customize(research, { agents: { researcher: { model } }, image });   // retuned
  const deep = customize(research, { agents: { researcher: { model: opus } } });              // nested twice,
  const quick = customize(research, { agents: { researcher: { model: haiku } } });            // differently
  export const machine = jr2Setup({ events: [route], actors: { triager: agent(triager), deep, quick } }).createMachine(…);
  ```

  A local `workflows/*.ts` is written exactly the same way; there is no package-side API.

- **The wrapper's body is a named slot** (`invoke: { id: "body", src: "body" }` over `setup({ actors: { body } })`), and
  `pool()`'s worker likewise. That is what lets `provide`, `customize`, Stately, and the `jr2 up` walk reach it
  uniformly.
- **A Part is a key of `actors`.** An `agent()`, an Effect (ADR-0067) or a `workspace()` written inline on an
  `invoke.src` is refused at build (`jr2Setup.createMachine`) and at load (the walk), with the slot form in the message.
  xstate gives an inline `src` a generated key (`xstate.invoke.<n>.<state id>`) that moves when a state is renamed, so
  the ledger, `jr2 up`'s report and the Machine doc would name the Part by a number, and `customize()` — `provide`,
  keyed on `actors` — could not reach it at all. A plain child Machine written inline is not a Part: the walk descends
  it, with no `customize()` route below it.
- **`jr2 up` walks the registered Machines** — `implementations.actors`, descending into child Machines — to collect
  models to preflight and `file:` image contexts to build. A context is keyed by its content digest (the same digest
  ADR-0038 tags by), taken over its copy in the staged Assembly, the bytes the Orchestrator holds
  ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)), so the host at `jr2 up`
  and the Orchestrator agree without a path table even where staging dropped a `.env` or a `node_modules` the folder
  holds. Repos are slots on the same walk (ADR-0051): a bound slot's url is collected, so its `Repo` resource is warm
  before a run asks; an open slot nobody bound is refused, naming the Machine, the slot, and the `customize` line that
  fixes it; a per-run slot is the run's business and the walk leaves it alone.

## Consequences

- **Agent identity moves from the Instance to the Machine.** GLOSSARY.md's Agent entry restates: an Agent is a part of a
  Machine, not an Instance-scoped roster entry; `customize` is a definition-level act by the composer, so ADR-0018's
  identity-vs-Dial line is untouched — a Turn still sets `model`/`thinkingLevel` and nothing else.
- **A Workflow is a Machine registered under a name.** The "wired to its Agents and config" clause retires: the wiring
  is inside the Machine.
- `defineAgent` retires in favor of `agent()`; `AgentTurnInput.agent` goes; `deriveMenus` keys on agent-slot logic
  instead of `src === "agentRun"`; the unit-test seam becomes `provide({ actors: { researcher: fake } })`.
- Only declared parts can be customized; none can be added. A consumer who needs a third Agent composes a new Machine. A
  `workspace()` that declares its Repo Slot map Open (`repos: open`, ADR-0051) has declared every slot as the composer's
  to name, so the map they write is the declaration, not an addition.
- **`import.meta.resolve` stays** in any Machine that ships a context; there is no other way for an ES module to name a
  folder it owns.
- The ledger records which slot a Turn ran as, not the definition's text. If per-Turn auditability of instructions is
  ever needed, the admission marker can carry a definition digest.
- `jr2 dev`'s reload cache-busts the workflow module only; an edit to an imported Machine module is not picked up until
  the next full reload. Known, unaddressed here.
- A `workflows/` file that is both discovered and imported by another registers as its own Workflow too; a Machine meant
  only for composition lives in a `_`-prefixed file or outside `workflows/`.
- A conversation belongs to one Agent slot in one Machine instance by construction — the continued iid is
  `<run>/<machine actor path>/<agent>` (ADR-0057) — so no continuation can cross personas. What a continuation does NOT
  check is that the definition behind the slot is the one that started it: a run whose `instructions` were edited before
  a redeploy continues, which ADR-0030 deliberately allows (shape decides restorability, not behavior). A definition
  digest on the ledger record would refuse exactly that, so it is not taken.
