Skip to content

Errors and limits ​

Bound every production agent and translate runtime failures at the application boundary. Do not expose raw provider, tool, retrieval, memory, or lifecycle errors directly to users.

1. Set a turn limit ​

The agent constructor defines the default, and one run can override it:

ts
const supportAgent = new Agent({
  id: 'support',
  model,
  instructions: 'Use tools only when needed.',
  maxTurns: 4,
})

const result = await supportAgent.generate({
    prompt: input.message,
    maxTurns: 2
})

maxTurns must be a nonnegative safe integer. It allows the initial model request plus up to maxTurns subsequent model turns: 0 permits one request and 1 permits two. A turn can request several tools; it is not a per-tool limit. Retries within a model invocation use a separate attempt budget. When the model keeps requesting tools beyond the allowed loop, the run rejects with MaxTurnsError.

If runs frequently reach the limit, inspect instructions, tool descriptions, invalid tool inputs, and tool outputs before increasing it. More turns increase latency and cost and can hide a model-tool loop.

2. Handle exported agent errors ​

ts
import {
  AgentRunCancelledError,
  MaxTurnsError,
} from '@anvia/core'

try {
  const result = await supportAgent.generate({
      prompt: input.message
  })
  return toProductResponse(result)
} catch (error) {
  if (error instanceof MaxTurnsError) {
    return {
      status: 503,
      message: 'The assistant could not finish this request safely.',
    }
  }

  if (error instanceof AgentRunCancelledError) {
    return {
      status: 409,
      message: safeCancellationMessage(error.reason),
    }
  }

  throw error
}

MaxTurnsError includes the configured limit, accumulated chat history, and last prompt. AgentRunCancelledError includes the accumulated history and cancellation reason. Keep those details in protected diagnostics unless they are known to be safe.

Other failures can come from unsupported model capabilities, provider authentication or validation, retrieval, memory, middleware, lifecycle callbacks, guardrails, or tool execution. Map them using application-specific error policy.

3. Treat approval as a result ​

Approval does not throw an error. It returns an interaction outcome:

ts
let result = await supportAgent.generate({
    prompt: input.message
})

if (result.type === 'interaction' && result.interaction.type === 'tool-approval') {
  result = await supportAgent.resume(
    result.continuation,
    {
      type: 'tool-approval',
      approved: false,
      reason: 'The operator rejected this action.',
    },
  )
}

The interaction outcome contains the run ID, interaction, continuation, usage, and messages accumulated so far. Persist the trusted continuation before waiting for a human decision, and do not execute the protected action outside the runtime as a shortcut.

4. Retry transient model failures ​

Agents own their default retry policy. A run with no retries value inherits the constructor setting; false disables it for that run; an object replaces it. Direct completions remain opt-in.

ts
const supportAgent = new Agent({
  id: 'support',
  model,
  retries: {
    maxAttempts: 3,
    initialDelayMs: 100,
    maxDelayMs: 1000,
  },
})

await supportAgent.generate({ prompt: input.message })
await supportAgent.generate({ prompt: input.message, retries: false })

Retries apply to the failed model invocation in its current turn. They do not restart the run or replay completed tools. The default policy covers common connection failures, rate limits, timeouts, conflicts, and server errors. maxAttempts is the total number of model attempts for that completion, including the initial attempt.

For streaming, Anvia retries only before provider progress has been exposed. Once output or another provider event has been observed, retrying could duplicate client-visible data, so the failure ends the stream.

Do not retry authentication, permission, invalid-request, schema, or deterministic application errors. Make side-effect tools idempotent before adding any wider request, queue, or job retry.

5. Handle stream errors ​

An agent stream emits an error event with accumulated usage and ends the iterator normally. Its .result promise rejects with the run failure. Await that promise inside the same error boundary:

ts
try {
  const stream = supportAgent.stream({ prompt: input.message })
  for await (const event of stream) {
    if (event.type === 'error') {
      recordRunFailure(event.error, event.usage)
    }
  }
  await stream.result
} catch (error) {
  return mapSupportError(error)
}

Centralize error mapping in the server or worker that owns the run. Return stable, product-safe messages to users and keep diagnostic details in protected logs, traces, or event records.

6. Distinguish outcomes from adapter errors ​

Direct generate() returns blocked and interaction outcomes. An adapter that promises completed output converts those outcomes into errors:

BoundaryBlocked outcomeInteraction outcome
Direct Agent generate/stream resulttype: 'blocked'type: 'interaction' with continuation
agent.asTool({ name, suspension: 'reject' }).call(...)AgentRunBlockedErrorAgentToolSuspensionError
Pipeline .agent({ ..., suspension: 'reject' })AgentRunBlockedErrorPipelineAgentSuspensionError
agentEvalTargetAgentRunBlockedErrorResponder continues it, or AgentEvalSuspensionError

Import the Agent errors from @anvia/core or @anvia/core/agent, Pipeline errors from @anvia/core/pipeline, and eval errors from @anvia/core/evals. Each error in the table retains the outcome in result; it can include messages, tool inputs, and a trusted continuation. Log a small approved summary rather than serializing the entire error.

Calling an agent-tool through parent.callTool(...) adds the local ToolCallError wrapper; its cause retains the adapter error. During an ordinary parent agent run, local tool failures are normally converted to model-visible tool error output. In eval suites, target failures become invalid judgments. Inspect the boundary you actually invoke before choosing a catch policy.

AgentStreamClosedError means stream.steer(...) was called after the stream stopped accepting input. It has no transcript fields. Steering cannot reopen a finished run; create another run or resume a stored interaction through the originating Agent. Continue to await stream.result inside your error boundary, because a normal end of iteration does not establish a successful result.

For provider contract failures, see provider output errors.

Built for Anvia.