# ADR 0108: What a stopped run already said is a declared policy, not a default

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0108-what-a-stopped-run-said-is-a-declared-policy.md

---

# ADR 0108 — What a stopped run already said is a declared policy

## Context

`AgentInputPolicy` had two values. `queue` finishes the run in flight first;
`interrupt` ends it. `interrupt` ended the run and kept everything it had
produced: the partial assistant was committed with `status: 'interrupted'`, and
the next run's projection sent that partial to the provider as an ordinary
assistant turn.

Worse than including a stale sentence, it included one that did not look stale.
`interruptedCandidate` appends a `control: 'run-interrupted'` part, and
`assistantMessages` rendered `text`, `reasoning`, `tool-call` and `tool-result`
— `control` was not among them. The marker was dropped on the way upstream. The
model received a confident half-sentence with nothing to say it had been cut
off, and continued a thought the user had already redirected.

So a third behaviour was wanted: end the run **and** discard what it produced.
The tempting shape was a projection rule — "omit interrupted assistants" — and
it is wrong, because two interruptions that are identical inside the runtime
have opposite correct answers:

- The user pressed **stop**. The partial was streamed to their screen and they
  read it. Dropping it makes the conversation lie to the model about what the
  human has seen.
- A newer input **superseded** the run. Whether the partial reached anyone
  depends entirely on the delivery surface: a token stream shows it as it is
  produced, a surface that sends nothing until the run is done never showed it.

## Decision

**Whether a stopped run's output stays in the conversation is declared by the
application, because the fact it turns on is not observable from inside the
runtime.**

That fact is delivery, and delivery belongs to the transport. The runtime sees
an abort; it cannot see a screen.

Three consequences follow.

**A third input policy, not a flag on the second.** `supersede` ends the run
like `interrupt` and marks the terminal record so the projection can tell them
apart: `terminalReason: 'superseded'`, run state `'superseded'`, assistant
status `'superseded'`. A boolean beside `'interrupted'` would collapse exactly
the two states this separation exists to hold apart, in the field an operator
reads first. The separation is the deliverable; encoding it as a modifier on the
value it must be distinguished from returns the ambiguity one layer down.

**Discarded from the prompt, never from the record.** A superseded run keeps its
parts in the snapshot. Run identity, admission receipts and the terminal CAS all
depend on the record existing, and an operator reconstructing a conversation
needs to see what was thrown away. "Discard" is a statement about the projection
only.

**The projection states what it did.** An interrupted turn that *is* projected
says so to the model, by default. Any part of a projected record that no
projected content stands for is named in that record's
`AgentHistoryProjectionDecision`. There is deliberately no setting that
reproduces the previous output: a silent drop is the defect, not a behaviour to
stay compatible with.

The form of the marker is itself a choice, and the reason is structural. An
assistant turn in provider history is a **commitment** — the model reads its own
previous turn as something it said and stays consistent with it. A system line
is context. `'assistant-marked'` is right when the human read the text;
`'system-note'` is right when the fragment reached nobody. That is the same
axis, restated at the level of one message.

## Consequences

The core learns no product policy. Which conversation surface gets which mode is
expressed through the function `inputPolicy` already accepts — one application
can hold a surface that always supersedes and one that queues, and the runtime
never learns which is which. → ADR 0002.

`AgentTerminalReasonSchema`, `AgentRunStateSchema` and
`AgentMessageStatusSchema` each gain a member, which is breaking for an
exhaustive switch, and the minor moves. The authority for breaking it is
ADR 0103: `stitchkit/agent-runtime` is declared **evolving**, so its shape may be
redefined in any minor with a marked breaking change and a migration section.
That is a property of the surface and does not depend on who has installed the
package — deliberately not "no consumer depends on it yet", which is true today,
cheaper to say, and expires the first time someone runs `bun add`.

A fourth behaviour stays out of scope: handing the input to the loop *between
tool calls* without ending the run. It needs pending inputs delivered to
`prepareStep` and attached to a running run's `inputMessageIds`, which the
accept path does today only for a run still `queued` — a durable-record change
of its own size, tracked separately.

## Alternatives considered

**A projection rule with no new policy.** "Omit interrupted assistants" is one
line and answers the wrong question — it cannot distinguish the stop button from
a supersede, which is the entire distinction.

**A boolean on the interrupted state.** Additive, and it puts the answer back
out of reach in the field an operator reads first.

**Letting the core infer delivery.** It would have to know whether the transport
streams, which is the domain knowledge ADR 0002 keeps out of the core.
