---
title: "ADR-281: A run waits durably for a dependency or a decision"
manual: "LLM Foundation"
version: "main"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-281@main"
source: "Adr/Adr281ARunWaitsDurablyForADependencyOrADecision.rst"
rendered: "2026-11-12T15:41:35+00:00"
---

# ADR-281: A run waits durably for a dependency or a decision {#adr-281}

-   *Status:* Accepted
-   *Date:* 2026-11-10
-   *Amends:*

    [ADR-081](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-081@main) (the run status vocabulary gains a waiting
    state that no person decides); [ADR-276](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-276@main) (a decision can
    also be stored and continued by the worker)

-   *Authors:* Netresearch DTT GmbH

## Context {#context}

A task delegated to a remote agent ([ADR-278](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-278@main)) may take
minutes. A tool is synchronous: `ToolInterface::execute()` returns a
`ToolResult`, which has no pending variant, and the loop waits for it while
holding a request or a worker and its lease ([ADR-141](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-141@main)).

`AgentRunStatus` has two waiting states, `waiting_for_approval` and
`waiting_for_input`, and `AgentRunStatus::awaitingValues()` returns
exactly those; both mean a person decides. [ADR-081](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-081@main) introduced
the enum as "the full lifecycle the later epics need". The reaper
([ADR-104](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-104@main), `ReapStaleAgentRunsCommand`) acts on RUNNING
rows with an expired lease only.

nr_mcp_agent reads the status. At its `main` 8e301436,
`ChatService::reconcile()` handles the two waiting states, RUNNING and
QUEUED, and treats every other value as settled: it marks the conversation
failed with "This run finished outside the chat". A new status reaches that
branch. nr_mcp_agent sets no `origin` on the runs of its chat.

Sending a message to a remote agent creates state on the other side. A
response can be lost after the request arrived, and A2A makes duplicate
detection by `messageId` optional (section 3.3.1 of the specification).

A decision on a waiting run is executed in the deciding request:
[ADR-276](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-276@main) keeps the continuation there and streams its progress,
and rejected handing it to the worker partly because a recorded, unexecuted
decision needed a state of its own. A decision that arrives over A2A (stage 4)
has no backend request to continue in.

## Decision {#decision}

**A new run status:** `AgentRunStatus::WAITING_FOR_DEPENDENCY`
(`waiting_for_dependency`). The run waits for something outside it that no
person of the installation decides; a remote A2A task is the first such
dependency. It is not terminal and not in `awaitingValues()`: nothing in the
inbox can decide it. A run in it holds no lease, so the reaper leaves it alone,
as it leaves the human waiting states. `AgentRunOutcome` gains
`AWAITING_DEPENDENCY` for the segment that suspends. Both are additive to the
`@api` surface: `Stability.rst` classes a new enum case as additive, and the
`AgentRunOutcome` docblock reserves new cases for minor releases.
`AgentRunStatus` carries no such note. The existing synchronous signatures
stay; new behaviour comes through additive interfaces.

**Every consumer understands the value before it is written.** The inbox, the
run views, `nrllm:agent:status`, the reaper and nr_mcp_agent learn the status
first; nr-llm writes it only after that. Since nr_mcp_agent's chat runs cannot
be told apart, no delegation tool is offered at all while nr_mcp_agent is
loaded in a version that does not handle the status, and the remote-agent
module says why. The release that activates the status names the nr_mcp_agent
version under "Upgrading".

**The suspension is a checkpoint, and it resolves once.** A suspended segment
stores, sealed, the tool-call id the dependency answers, the external operation
reference, the actor, the workspace, the deadlines and the budget state. The
resume answers exactly that tool-call id and never executes the delegation tool
again. A model turn that calls a delegation tool together with another tool is
rejected before any of them runs, so at most one dependency is pending per
segment.

**Resume is fenced.** The run is claimed for resume with the atomic claim and
lease of an approval resume. A worker that lost its lease writes nothing; of two
workers that see the same remote result, one resolves the dependency.

**Budgets persist.** Deadline, delegation count, transferred bytes and the
local token and cost budgets survive every suspension. The delegation limit
covers only delegations nr-llm makes; what a remote agent delegates further is
outside its control.

**Every send is journalled before it leaves, and an unknown outcome is never
resent automatically.** A row in `tx_nrllm_a2a_send` is committed before the
network call, holding the owner, the remote agent, the run, the `messageId`
and the SHA-256 of the request; the same owner, id and digest are the same
operation, the same id with another digest is refused. A lost response leaves
the row `outcome_unknown`; the run shows it, and only an operator resends,
with a new id. A duplicate task on the remote side is worse than a run that
asks a person.

**The run is resumed from the worker.** A coordinator polls `GetTask` for
waiting runs at the remote record's interval inside `WorkerVaultScope`;
push ([ADR-285](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-285@main)) only makes it look sooner. At the deadline the
run resumes with a timeout result and the remote task is cancelled. Cancelling
a waiting run sends `CancelTask` once and settles the run CANCELLED whatever
the answer.

**A decision can be stored and continued by the worker.** An additive
interface takes in an input, an approval or a denial, checks it and stores it,
and returns a storage receipt instead of continuing the run. It checks that the
pause is still pending with the pause generation the decision was made for, the
run's owner and the deciding actor, the input against its schema, the approved
content against the turn digest, and the decider's current permission. The run
goes to QUEUED with a reference to the stored decision; the worker checks the
rights again before the first effect. The synchronous path of ADR-276 stays and
uses the same checks and the same atomic claim: exactly one of the two wins a
pause. The recorded, unexecuted decision is the QUEUED run with its continuation
reference; it needs no further status.

## Consequences {#consequences}

-   ●● A delegated task holds no request, no worker and no lease while it runs.
-   ●● A lost response cannot create a second remote task, and a remote result
    cannot run the delegation twice.
-   ● A decision that arrives over A2A is executed by the worker, with the same
    checks as one from the inbox.
-   ◑ A run can sit in the new status until its deadline; the inbox and the run
    list show it as waiting, not as needing a decision.
-   ◑ On an installation with nr_mcp_agent, delegation waits for an
    nr_mcp_agent release.
-   ✕ Any other consumer that maps "not waiting, not running, not queued" to
    "settled" misreads the status, although the enum change is additive.
-   ✕ Polling adds requests to the remote for the duration of a task.

Net Score: +4

## Alternatives considered {#alternatives-considered}

**Reuse** `waiting_for_input`. Rejected: the inbox would offer a person a
decision nobody can make, and nr_mcp_agent would show a question.

**A status named after A2A.** Rejected: the state describes waiting on any
external operation; a second kind of dependency would otherwise need another
value and another round through every consumer.

**Keep the run RUNNING with a long lease.** Rejected: it holds a worker or
makes the reaper requeue a run that is fine, and a requeue would resend.

**Retry an unknown outcome with the same** `messageId`. Rejected: duplicate
detection is optional in A2A, so a retry may start a second task.

**Continue A2A decisions in the A2A request, as the inbox does.** Rejected: the
request would execute the run ([ADR-283](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-283@main)), and the remote client
does not wait for it.
