Concepts
How turns end
What each terminal status means, why an interrupted turn is completed, why a turn that hit its limit is incomplete, and when to read error instead.
Most agent APIs give you two endings: it worked, or it broke. nvoken has four, because "it broke" was hiding three different situations that call for three different responses in your code.
Read status first. Then read stop_reason for a non-failure ending, or
error for a failure. Never infer the ending from whether text arrived.
The four terminal statuses
| Status | What happened | What your app should do |
|---|---|---|
completed | The turn ended the way it was asked to | Use the answer |
incomplete | A limit you set stopped coherent work | Show the partial work as unfinished, or continue |
failed | The turn could not finish; error says why | Handle the error |
cancelled | Your app stopped it and discarded its work | Nothing to use |
Everything else — queued, running, waiting, paused — is still live.
An interrupted turn is completed
POST /v1/invocations/{id}/interrupt asks a running turn to stop at its next
safe boundary and keep what it produced. That turn settles completed, with
stop_reason: "interrupted".
This surprises people, so it is worth being explicit about why: the caller asked the turn to end there, so ending there is the requested result, not a failure. The work stays in the Session and the next turn builds on it.
If you want the work thrown away instead, that is a different verb.
POST /v1/invocations/{id}/cancel settles cancelled and excludes the
unfinished messages from later model context. Both stop the turn. Only one keeps
the work.
A turn that hit its limit is incomplete
You set the ceilings: max_iterations, max_output_tokens,
max_estimated_cost_usd, the deadlines, and any Session cost budget. When a
turn reaches one at a coherent execution seam, it settles incomplete — not
failed.
The distinction is deliberate. Nothing malfunctioned; the turn ran into a rule
you wrote. Its transcript is valid and stays in later turns' context exactly as
a completed turn's does. What it is not is a finished answer, and status says
so without your code having to guess.
That also keeps completed meaning one thing. A host reading completed never
has to check stop_reason to discover the turn was actually cut off.
stop_reason names the ceiling: max_iterations, deadline,
max_output_tokens, max_estimated_cost, or session_budget.
Exhaustion that cannot reach a coherent seam still settles failed — there is
no valid transcript to hand back in that case.
Admitting with on_budget_exhausted: "pause" changes this: the turn parks as
paused instead of settling, and resumes once you raise the ceiling. Deadlines
never pause. See Pause at a budget instead of
ending.
stop_reason, in full
stop_reason spans both non-failure terminals and is partitioned between them.
It is null on failed and cancelled, where error is the authority.
stop_reason | Status |
|---|---|
end_turn | completed — the model finished |
interrupted | completed — a caller's graceful stop |
max_iterations | incomplete or paused |
max_output_tokens | incomplete or paused |
max_estimated_cost | incomplete or paused |
session_budget | incomplete or paused |
deadline | incomplete only — deadlines never pause |
When error is the field to read
On failed, error.code names the class of problem and error.details carries
a bounded, code-specific object. The codes worth knowing:
context_window_exceeded— the conversation outgrew the model's window.detailscarriesinput_tokens,context_window_tokens, andrequested_output_tokenswhen known, so you can see how far over you were rather than guessing from a provider string. Most competitors surface this as an opaque provider error.structured_output_unsatisfied— the turn promised a validated object and never produced one.details.reasonismissing,invalid, oroversized.budget_exceeded— an exhaustion that could not settle coherently, orestimated_cost_unavailable, which means nvoken could not price the model at all.provider_key_unavailable— no usable model account for the selected provider.provider_error— the provider itself refused or failed.details.provider_failure_classtells you whether to retry:upstream_rejectedmeans do not retry as-is, whilethrottledandupstream_unavailablemean retry with backoff.deadline_exceeded,input_media_rejected,mcp_discovery_failed,internalcover the rest.
No raw provider error text crosses into details.
Which message is the answer
Do not assume the last assistant message is the answer — it is wrong in exactly
the cases that matter here. Every assistant message carries a phase:
final_answermarks the message that settled a turncompletedwithend_turn.commentaryis everything else: narration between tool calls, and all output from an interrupted, incomplete, or cancelled turn.
A turn with no final_answer is visibly unfinished. nvoken derives phase at
read time, so nothing is stored and no host supplies it.
The one-paragraph version
Check status. completed means the turn ended as asked, including a graceful
interrupt. incomplete means your own limit stopped it and the partial work is
real. failed means read error. cancelled means throw it away. Then render
the message whose phase is final_answer.