Skip to content

ADR-0057 Architecture decision record

A Turn is its Frame, its Dials, and whether it continues ​

A live task run (2026-09-20, fa45e88f, seven tool calls, eight minutes, zero bytes of the target read) exposed the seam: the prompt said Work in /work/target/summarize while every Working tool resolved relative paths at /work, because cwd lived on the Agent definition and a definition is written before any run — before any worktree — exists. The package compensated with prose, and a weak model ignored prose: it wrote SUMMARY.md outside the worktree, called finish, and the run parked at review claiming done. The audit that followed found the invoke input carrying four ways to name one conversation (session: "continue", scope, conversation, and an undeclared instanceId), one of them (scope) with two unrelated author intents, none with a stated precedence, and every @kind fixture bypassing all of them. ADR-0028 already said the reviewing state hands a per-round worktree path as the reviewer's "cwd and prompt"; ADR-0018 and GLOSSARY.md said cwd is identity, never per Turn. The decided thing had no field to land in.

Decision ​

  • A Turn's input is its Frame, its Dials, and whether it continues — nothing else.

    ts
    { prompt, cwd?, continue?, model?, thinkingLevel? }

    Identity — instructions, workspace — stays on the definition, and the definition is exactly identity plus one default: agent({ model, instructions, workspace?, thinkingLevel?, description? }).

  • The Frame is prompt and cwd: what this Turn is about and where it works. Neither identity nor a Dial. A worktree path is per run by nature — /work/<slot>/<branch> exists only once a run has a branch — so no definition can name it, and a directory is not a property of a persona any more than a prompt is. cwd leaves AgentDefinition. The Harness roots the Working tools at the Frame's cwd, which rides the admit body beside the prompt and the Dials.

  • The Briefing is not the Frame. jr2's Turn part (ADR-0062) follows the prompt — the resolved working directory, the Allowed picks and any notices — in a block of its own. The prompt stays the Machine state's words alone; jr2 never writes into it.

  • Under a Workspace, an absent cwd resolves to the only Repo Slot's Worktree, and is refused when there is more than one. With one slot there is no choice, so no convention is being smuggled in (ADR-0051: the kit gives no slot a meaning). With two or more, the state must say which — cwd: context.workspace.repos.target — and the actor refuses the Turn before admission, naming the slots and the line to add. The trap is accepted with open eyes: a composer who adds a second slot on the customize line can make a Machine that ran yesterday refuse today, in a file the Machine's author never sees. The refusal is explicit, at the first Turn, before any pod is spent; jr2 up cannot see it because the invoke's input is a function. A workspace: "none" Agent has no Working tools and no cwd (ADR-0028); a stub-Harness run with no Workspace keeps /work. task always passes its cwd, because "the first slot is the one the coder edits" is its convention and a convention is stated, not defaulted.

  • continue: true is the whole continuation surface. Absent, every invocation is a fresh conversation — jr's lossy handoff, ADR-0016's default, unchanged. Present, the Turn lands on this Agent's one conversation in this Machine instance, whichever state of the Machine invokes it. The Instance ID is structural — <runId>/<machine actor path>/<agent> — so the author writes a boolean, not a key: in a state machine the identity is already on the page (the run, the Machine instance, the Agent slot), and a caller-chosen string would only restate it, or contradict it. Every surveyed orchestration framework makes the caller hold a key (LangGraph thread_id, Temporal Workflow Id, Mastra threadId) because nothing else can; here something else can.

    • Fan-out is safe by construction. A Pool spawns each worker with the item id as its actor id (pool.ts), so three task children each mint their own …/backlog.worker.<item>/coder. The packaged Machine never learns it is under a Pool.
    • States of one Machine share by saying continue. States differ by Menu — a coder in implement offers finish, the same coder in fix offers finish and dispute — and the same conversation is wanted across them. A state that wants a clean context leaves continue off. A Turn joins only a conversation that an earlier Turn started with continue: true, so a retry loop says continue on each Turn, the first too: a first attempt without it is not in the conversation that attempt 2 continues. Thus the docs show continue: true on each Turn of a retry loop, and a conditional spread (...(clean ? {} : { continue: true as const })) only for a state that must start a clean conversation on some Turns only (a state that always starts clean leaves continue off). A clean Turn does not start the continued conversation again: the next Turn with continue: true joins the earlier one, so attempt 4 after a clean attempt 3 continues attempts 1 and 2. The alternative, a spread on attempts > 0, keeps attempt 1 out of the conversation, so attempt 2 does not see it. The cost of continue: true on attempt 1 is the Runaway reroll (ADR-0035): a continued Turn gets none, so a Runaway on attempt 1 sends agent.fault at once, and burns the conversation. The docs accept this cost, so that attempt 2 sees attempt 1.
    • Two Agents never share a conversation. The agent name is in the id, and it always was; a second persona under one transcript would swap the system prompt mid-conversation. The memory-Agent shape ("read the coder's conversation, store what matters") is served by handing the transcript as prompt data to an Agent with its own conversation — Letta's sleep-time agents — not by joining; how a transcript reaches a Machine as data is a follow-on decision.
    • A faulted conversation is never continued. No fault leaves a conversation worth continuing: a runaway either had its reroll or has a poisoned context; infra and no-signal mean the Harness that held it is unreachable (ADR-0035). jr2 keeps an epoch per continued conversation in the ledger beside the snapshot; the terminal agent.fault bumps it, and the next continue on that Agent mints <id>/<epoch> — a virgin conversation. The author deletes their generation counter. What stays with the author is re-briefing: the fault event is the signal that the next prompt must carry the whole task again, and jr2 cannot write that prompt. The one fault that is not the conversation's is a Harness restart that cut the Turn (Turn lost, or memory limit behind it): the rebuilt Harness holds the conversation (ADR-0031), so the epoch stays and the next continue lands on it; a rebuild that could not read it answers that Turn's holds check, which gives a conversation-new notice (ADR-0062).
    • The reroll stays closed to continued conversations (ADR-0035): the one recovery jr2 knows is the identical prompt on a fresh conversation, and a continued Turn's prompt ("Continue.") is meaningless fresh. Fault route plus re-brief is the recovery.
    • Two live invocations of one continued id — parallel states — are refused at the registration table, one live surface per address; a Submission that arrives behind a live one on the Harness queues (ADR-0027). Sequential states never race, because ADR-0024 ends the previous Turn before the next state invokes.
  • session, scope, conversation, and instanceId retire. session: "continue" was already this boolean under a longer name — its id was the invoking Machine's actor path plus scope — so what retires there is the name and the suffix; conversation was the cross-Machine pin (a name replacing the path, so two Machine instances — two Pool children — collided by construction, the hazard ADR-0049 recorded and could not refuse); scope suffixed all of them and carried two intents (a subject under continue, a generation counter in task); instanceId bypassed minting for tests. One boolean replaces all four because the id is deterministic when continued, so nothing needs a handle to address it.

  • The author type carries no mechanism. endpoint and sandbox — set only by stub-tier fixtures that have no workspace() to resolve from — move to a mechanism type the tests import. tools, the Menu override, retires: a hand-written Menu can only agree with the invoking state's transitions or lie about them, and the lie is a pick that moves nothing (ADR-0029). The one shape it hid — picks written on the CHILD STATES of the invoking state — is derived: those child states are the state's modes (ADR-0015). A state that allows no picks stays legal, because a state a Gate or a timer moves asks its Agent for text and nothing else. A real run never sets any of them: a Sandbox Agent resolves ambiently (ADR-0016), a workspace: "none" Agent lands on the Instance Harness (ADR-0031).

Considered options ​

  • Prompt-only — tell the model that relative paths resolve at /work and demand absolute ones. Rejected by the incident: ADR-0024 already established that the same prompt produces compliance on one run and not the next, and every other Machine would repeat the sentence.
  • cwd stays on the definition, overridable per Turn. Rejected: a definition-level cwd can only ever say /work, the parent of every checkout, where grep and glob hit the pristine default/ clone beside the worktree. A default that is wrong under every workspace() is not a default.
  • cwd as a third Dial. Rejected on the word: Dials say how hard to run. A directory says where, which is the same kind of fact as the prompt — hence Frame.
  • Default cwd to the first Repo Slot. Rejected: reverses ADR-0051 ("there is no workdir"). The single-slot default is not the same thing — with one slot there is nothing to privilege.
  • Refuse an absent cwd under every Workspace. The safer twin of the single-slot default: one line per invoke, no composition trap. Not taken: the trap's failure is explicit and early, and the line is noise for the common single-repo Machine.
  • A caller-chosen conversation key (conversation: "coder"). Rejected: it either restates the structure or contradicts it. Path-scoping the key was proposed and made the key redundant — once the id is <run>/<machine path>/<agent>, the author has nothing left to name.
  • Continuation scoped to the invoking state. Rejected: splits one conversation across the states that differ only by Menu, which is the case continuation exists for.
  • The cross-Machine pin kept as an opt-in. Rejected: zero users since examples/coding went (ADR-0054), never sound — the two states it joined sat on different Harnesses, and a conversation lives on one server (ADR-0031) — and an unused opt-in is the fourth mechanism back under a new name. If a sound case appears it is a new decision.
  • Continue a faulted conversation, author's choice. Rejected: there is no case. Every fault class leaves either a poisoned context or no server — except a Harness restart, which cuts the Turn and keeps the conversation (above).

Consequences ​

  • AgentTurnInput is { prompt, cwd?, continue?, model?, thinkingLevel? }; AgentDefinition loses cwd. The Harness admit body gains the Frame's cwd; ResolvedDefinition.cwd resolves from it, not the definition; the Harness stops validating description, which the Orchestrator strips before admission (it is Console material).
  • A Frame's cwd is absolute or the admission is refused, naming the Frame. Absent is the one silent case, and it says /work — a stated default. A relative path has no such answer: it would resolve against whatever directory the Harness process sits in, which is the silent wrong directory this ADR was written for, one level further from the author. The refusal is a 400 at admission, before a pod is spent.
  • task passes cwd: worktreeOf(context).path and continue: true, and drops generation (and its scope). It keeps turns, reset on agent.fault, because re-briefing is prompt policy.
  • The ledger record for a continued conversation carries its epoch; the input mapper reads it at mint time, so the minted id persists with the input and restore re-attaches the same conversation (ADR-0016's mapper rule holds — the fresh suffix is still the random component).
  • The @kind fixtures drop tools: and instanceId:; the steps compute the continued id from the actor path, as they already computed today's pin.
  • A regression scenario for the silent success: a task whose model uses relative paths must produce its file inside the worktree, and a multi-slot Machine with no cwd must be refused with the slots named.
  • GLOSSARY.md: Frame enters; Dials loses cwd; Instance ID states the continue scope and that a faulted conversation is not continued. ADR-0016 (continuation), ADR-0018 (identity line), ADR-0031 (placement scenario), ADR-0035 (reroll gate wording), ADR-0049 (persona hazard), and ADR-0054 (task's continuation) are edited to state this truth.

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