Skip to content

Agents and multimodal systemsGuide

Failover mid-agent-run: what happens to tool calls and state

Failover protects a single request. An agent run is dozens of requests plus side effects your gateway never sees. This guide covers where a provider switch mid-run corrupts state, duplicates actions or loses context, and the checkpointing and idempotency patterns that contain it.

By INFRO Editorial

Published

9 min read

A glowing autonomous agent core coordinating tools, memory, and multiple AI model routes

In short

A provider switch mid-run breaks the agent's history, not the in-flight tool. Tool calls run in your code, so the real risks are replayed side effects, provider-specific message blocks and a cold prompt cache. Checkpoint every step, key every side effect, and normalize history.

Key takeaways

  • A tool call is never in flight at the provider: it runs in your code, so failover changes the next model request, not the tool that is running.
  • Most duplicate side effects come from crashes and mid-stream cut-offs that make your agent re-ask the model, not from the gateway retrying.
  • Derive the idempotency key from run id, step and call position, never from the model's tool call id, and reject a reused key whose arguments differ.
  • Store history in your own neutral format and render it per provider, so a switch is a re-render and not a repair.
  • Expect a cold prompt cache after every switch and treat it as a cost and latency event on the next turn.

What failover does, and does not do, to a run

Failover is a per-request mechanism. In INFRO's documented behavior, connection failover tries the same model on another of your connections, and model fallbacks try an ordered list of other models, and both operate strictly before the first token.1 If the failing provider returns a 5xx, times out or answers 429 before the response starts, the next target is tried and your client sees only a higher time to first token.1

An agent run is a different shape: a loop of model requests with your code executing tools between them. That gives three consequences worth stating plainly:

  • A tool call is never "in flight" at the provider. The model emits a request to call a tool, and your application runs it. Anthropic's docs draw the same line: client tools run in your application, and your code sends back a tool_result.2
  • Failover decides who answers the next request. Whatever that request carries (the whole history, tool definitions, prior tool results) is now read by a different model, possibly through a different message format.
  • The things that actually break are outside the request: side effects that ran twice, state that lived only in memory, and a prompt cache that no longer applies.

So the question to ask is not "does the tool survive failover" but "what does the run look like if any single model request is answered by a different provider than the one before it, or not answered at all".

Five failure modes, ranked by damage

FailureWhat happensDamage
Replayed side effectProcess crashes or a stream is cut after a tool ran; the resumed run asks the model again and it calls the tool againDuplicate email, refund, ticket, deploy
Orphaned tool callHistory contains a tool call with no result (or a result with no call)The next request is rejected or the model misreads state
Provider-specific blocksThinking blocks, cache markers or tool ids from provider A sent to provider B400 errors, or silently dropped context
Lost in-memory stateScratchpad, plan or counters held in the worker processRun restarts from step one, or diverges
Cold cachePrefix recomputed after the switchSlower and costlier next turn
Failure modes when the provider changes during an agent run.

The first one is the only one that costs money or trust outside your own systems, so it gets the most attention below. The rest are correctness and cost problems you can test for.

Checkpoint every step, in your own format

Do not let the provider's response object be your run state. Write a journal that is append-only and survives a worker restart. Two writes per step are enough:

  1. Before executing any tool: persist the assistant message exactly as received, with its tool calls.
  2. After each tool finishes: persist that tool's result, keyed by the call's position in the step.

The "exactly as received" rule matters for a concrete reason. INFRO's tool-calling docs say to append the assistant message exactly as received because it carries the tool_calls your results answer, and to answer each call with a tool message carrying the matching tool_call_id.3 Anthropic is stricter about ordering: tool results must immediately follow their tool use blocks, and the tool_result blocks must come first in the user message.4 A journal that stores the pair intact lets you rebuild a valid history after a crash.

journal.py
from dataclasses import dataclass, field

@dataclass
class Step:
    index: int
    assistant: dict                      # message as received, untouched
    results: dict[int, str] = field(default_factory=dict)  # call position -> result

def resume(run_id: str, store) -> list[dict]:
    """Rebuild neutral history; return pending call positions for the last step."""
    steps = store.load_steps(run_id)
    history = []
    for s in steps:
        history.append(s.assistant)
        calls = s.assistant.get("tool_calls", [])
        for pos, call in enumerate(calls):
            if pos in s.results:
                history.append({"role": "tool",
                                "tool_call_id": call["id"],
                                "content": s.results[pos]})
    return history

On resume, find the last step whose assistant message has calls without stored results. Those are the only calls you may need to execute, and each must go through the idempotency check described next. Never re-ask the model for a step whose assistant message is already in the journal; replaying the stored message is both free and deterministic.

Idempotency keys for side-effecting tools

The model's tool call id is the wrong key. If a run is retried from the previous request, a new model answer carries a new id, and sometimes different arguments. Derive the key from things your journal controls: run id, step index and the call's position within the step.

effects.py
import hashlib, json

def effect_key(run_id: str, step: int, pos: int) -> str:
    return f"{run_id}:{step}:{pos}"

def args_hash(args: dict) -> str:
    canon = json.dumps(args, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canon.encode()).hexdigest()

def run_effect(db, run_id, step, pos, tool, args, do):
    key, h = effect_key(run_id, step, pos), args_hash(args)
    row = db.get_effect(key)
    if row is None:
        db.insert_effect(key, tool, h, status="started")   # unique on key
        result = do(args, idempotency_key=key)
        db.finish_effect(key, result)
        return result
    if row.args_hash != h:
        return "error: same step re-planned with different arguments; needs review"
    if row.status == "finished":
        return row.result                                  # replay, do not execute
    return do(args, idempotency_key=key)                   # crashed mid-flight: retry with same key

Three details carry the design. The unique insert makes two workers racing on the same step safe. The arguments hash turns "the model re-planned differently after the switch" into an explicit refusal instead of a second action. And the final branch, a crash between starting and finishing, only works if the downstream system honors the key. For your own services, enforce it with a unique constraint. For third-party APIs, use their idempotency mechanism where one exists; where none exists, mark the tool non-retryable and surface the ambiguity to a human rather than guessing.

Tools exposed over MCP deserve the same treatment. The MCP docs describe tools as functions the model decides to call, which can write to databases, call external APIs and modify files.5 Nothing in that description makes a call idempotent, so the key has to travel in the tool's own arguments or be enforced by the server.

Where mid-stream failures fall outside failover

Because failover happens only before the first token, a stream that dies halfway is your agent's problem. INFRO's streaming docs state that after a mid-stream error event the connection closes without the [DONE] sentinel, so connection close must be treated as terminal.6 For an agent that streams tool-call arguments, that leaves a partial arguments string.

The safe handling is short:

  • Discard the partial assistant message. Do not parse half a JSON argument string and execute it. The tool-calling docs already warn that function.arguments is model-generated JSON in a string, to be parsed defensively and validated before anything with side effects runs.3
  • Journal nothing for that step, then re-request it. Because no tool ran, replay is safe.
  • Count the re-request against the run's step and retry budget, so an unstable provider cannot loop the agent. The loop-bounding article covers those limits.

The dangerous variant is a stream that completes with tool calls, your code runs them, and the process dies before the result is journaled. That is the case the effect table above exists for.

Message-format translation hazards

When the next request lands on another provider, the history is translated. INFRO's docs say that for Anthropic connections it translates tools, tool_choice, tool calls and tool results to and from Anthropic's tool_use blocks.3 That covers the common shapes. It does not make provider-specific extras portable. Check these in your own history:

  • Thinking blocks. Claude's thinking blocks carry a signature, an encrypted copy of the reasoning that is passed back unchanged in multi-turn and tool-use conversations.7 Treat it as meaningful only to the provider that issued it. When history crosses providers, keep a plain-text summary in your journal if you want continuity, and do not assume another provider accepts the block.
  • Tool call ids. Ids are opaque and provider-issued. Always pair results to calls by position in your journal, then render the id the destination expects.
  • Parallel calls. One assistant turn may hold several calls. Claude expects all matching tool_result blocks in one user message, first in the content array;4 OpenAI-style history uses one tool message per call.3 Your renderer must emit the right grouping.
  • Tool result content. Claude accepts nested text, image and document blocks in a result.4 An OpenAI-style tool message takes a string. A result containing an image needs a fallback representation on the string side.
  • System content and caching markers. Anthropic cache breakpoints are explicit cache_control markers, while OpenAI caching is automatic.8 Neutral history should not store markers; add them at render time.

The pattern that holds up: store neutral messages (role, text, calls, results, attachments), and write one renderer per provider family. A switch is then a re-render of the same journal, not a repair job. Whether the fallback model is competent on your tool schemas is a separate question; the fallback equivalence test covers it.

The cold cache after a switch

Provider-side caches belong to the upstream that served the request. INFRO's docs say hits depend on consecutive requests reaching the same connection, and that a failover to another connection resets the cache for that prefix, which is then billed as a cold prefix.8 For an agent this compounds, because every step re-sends the whole growing history. The step after a switch pays full input price on the entire transcript, and a long run pays it again if traffic flips back.

Two things limit the damage. Keep the prefix stable and append-only (system prompt, then tools in a fixed order, then history), which the docs recommend for cache hits in general.8 And do not flap: a route that fails over and fails back on every health blip pays the cold cost repeatedly. The failover bill article works through the cost side.

What the gateway can show you, and what stays yours

On INFRO, failover and health-aware routing work on a named route over your own connections, and apply to chat completions including streaming and tool calling. Each request's trace records which connection served it, so you can see in one place that step 19 ran on a different provider than step 18. INFRO executes no tool, so idempotency and the journal stay in your application. If your tools are MCP servers registered through the MCP gateway, their calls land in a call log on the same trace as the model calls.

A test plan before you ship

  • Kill the worker after a write tool returns but before its result is journaled, then resume. Exactly one external effect should exist.
  • Cut a stream after half of a tool call's arguments. Nothing should execute, and the step should be re-requested.
  • Force a provider switch at every step index of a recorded run, one at a time. History must render validly for each provider.
  • Re-plan a step with different arguments on resume. The effect layer must refuse and flag it.
  • Include a run with parallel tool calls and one with an image in a tool result across the switch.
  • Compare per-step input cost around a switch against steady state, and confirm the spike matches your cold-cache estimate.

The failover drills article describes how to stage provider failure; run these cases inside a drill, with real tools replaced by recording stubs.

Questions readers ask

Should an agent run stay pinned to one provider once it starts?
Pinning removes translation and cache hazards but trades away availability. A reasonable middle: pin by default, allow a switch only at a checkpoint boundary, and record the switch on the run so you can compare outcomes of switched and unswitched runs.
Can I resume a run on a different model after a long outage?
Yes, if history is stored in your own neutral format. Resume from the last checkpoint with the run's journal of completed tool results, and re-render the messages for the new provider. Re-test the fallback model on your tool schemas first.

From the team that builds it

Where this shows up in INFRO

INFRO is a control plane in front of the provider accounts you already hold. The capabilities below are the parts of it this article touches, each with the documentation that defines its behaviour.

INFRO accounts are self-serve. Provider usage is billed to you by your providers; INFRO charges a software subscription and takes 0% of model spend. See plans.

Sources

Numbered in order of first citation. Each was opened and checked against the claim on the date shown.

  1. 1.Failover & fallbacks (opens in a new tab) — INFRO docs. Accessed Nov 4, 2026.
  2. 2.Tool use with Claude (opens in a new tab) — Anthropic. Accessed Nov 4, 2026.
  3. 3.Tool calling (opens in a new tab) — INFRO docs. Accessed Nov 4, 2026.
  4. 4.Handle tool calls (opens in a new tab) — Anthropic. Accessed Nov 4, 2026.
  5. 5.Understanding MCP servers (opens in a new tab) — Model Context Protocol. Accessed Nov 4, 2026.
  6. 6.Streaming (opens in a new tab) — INFRO docs. Accessed Nov 4, 2026.
  7. 7.Thinking (opens in a new tab) — Anthropic. Accessed Nov 4, 2026.
  8. 8.Prompt caching (opens in a new tab) — INFRO docs. Accessed Nov 4, 2026.

About this article

Read on 2026-11-04: INFRO's failover, tool calling, prompt caching and streaming docs; the MCP server concepts page; and Anthropic's tool use, handle-tool-calls and thinking pages. OpenAI's docs page returned HTTP 403, so no OpenAI-specific claim is made. Numbers in the example block are illustrative.

Last checked against its sources on . The next review is due by . No separate named reviewer is recorded for this article.

How articles are written, sourced and corrected is set out in the editorial standards.