nvoken
DocumentationHow turns end

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

StatusWhat happenedWhat your app should do
completedThe turn ended the way it was asked toUse the answer
incompleteA limit you set stopped coherent workShow the partial work as unfinished, or continue
failedThe turn could not finish; error says whyHandle the error
cancelledYour app stopped it and discarded its workNothing 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_reasonStatus
end_turncompleted — the model finished
interruptedcompleted — a caller's graceful stop
max_iterationsincomplete or paused
max_output_tokensincomplete or paused
max_estimated_costincomplete or paused
session_budgetincomplete or paused
deadlineincomplete 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. details carries input_tokens, context_window_tokens, and requested_output_tokens when 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.reason is missing, invalid, or oversized.
  • budget_exceeded — an exhaustion that could not settle coherently, or estimated_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_class tells you whether to retry: upstream_rejected means do not retry as-is, while throttled and upstream_unavailable mean retry with backoff.
  • deadline_exceeded, input_media_rejected, mcp_discovery_failed, internal cover 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_answer marks the message that settled a turn completed with end_turn.
  • commentary is 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.