Skip to content
Blog

When the Tool Call Fails: Shipping AI That Says I Don't Know

Foundation Models can now call remote models, so your on-device AI feature has a network failure path. How to make it admit it couldn't check.

Jorge Valbuena7 min read

The dangerous failure in an AI feature isn’t a wrong answer. It’s a fluent, well-formatted, correctly-structured answer produced after the data source went dark — and nothing in your logs saying so.

Foundation Models grew a network failure path

Through iOS 26, a Foundation Models feature had a property that made a lot of engineering decisions for you: it ran on device. No network call, no per-request quota, no third-party outage, no rate limit. If the model was available, it answered. The failure modes you had to design for were local ones.

That changed this year. Apple’s developer site, dated 10 June 2026, states that Foundation Models can now target any conforming language model, including Claude and Gemini. The API surface you already know now fronts something that can be on the other side of a network connection, behind an account, behind a quota.

That’s the real story for anyone shipping an AI feature. Every remote hop — the model itself, or a tool the model calls to retrieve data — is a place where the request fails and the generation keeps going. The language model does not stop when its inputs disappear. It fills the shape it was asked for.

So the question stops being “is the model good enough” and becomes “what does this feature render when the retrieval path is dead?” If you don’t answer that deliberately, the answer is: something plausible.

A worked example from our own tooling

On 3 September 2026, a scheduled scan of Apple developer news and OS release notes returned nothing usable. A loop bug issued every query twice. That doubled consumption against the session’s web search budget, and once the budget was gone, every subsequent call came back with the same server error: tool use limit exceeded. The budget did not recover within the session.

The honest part of the outcome is that the assistant refused to write up events it could not retrieve, and said explicitly that anything it produced would be invented dates attached to fabricated developer.apple.com links. That’s the correct behaviour, and it’s worth noticing what it depended on: the error was visible to the thing doing the reasoning. The tool didn’t fail silently and return an empty result set that looks like “no news today.”

Also worth being precise about the actual defect. The quota wasn’t the bug. The duplicate-query loop was the bug; the quota was where it surfaced. A deduplication key in front of the budget would have absorbed it entirely, and the scan would have completed on half the calls it thought it needed.

That’s the shape of most tool-call incidents. A cheap correctness problem upstream consumes a hard resource downstream, and the symptom you see is the resource exhaustion.

There are two budgets, and the second one lies to you

Everyone counts tool calls. Fewer teams count tokens, and the token budget is the one that produces confusing incident reports.

On the default Foundation Models model, system instructions, prior conversation turns, tool outputs, and the new prompt all share a single 4,096-token ceiling. A retrieval tool that returns verbose output — full article bodies instead of extracted fields, unpruned JSON, raw HTML — spends that ceiling fast. When it runs out, you get .exceededContextWindowSize.

Here’s the trap. That error arrives as a generation failure. Your retrieval succeeded. Your network was fine. Your quota was fine. The thing that broke was the model’s ability to hold what retrieval handed it, and unless you’re logging tool output sizes alongside generation errors, it will be filed in your dashboards under “the model failed” rather than “the tool was too chatty.” Teams then go tuning prompts for a problem that lives in the retrieval layer.

Budget tool output the way you’d budget a payload over a metered connection. Trim server-side, return fields rather than documents, and cap the total bytes a single tool can contribute to a session.

@Generable constrains structure, not truth

Guided generation is the feature that makes Foundation Models pleasant to build against: declare the shape you want, get that shape back. It’s also the feature most likely to launder a fabrication into something your UI trusts.

@Generable
struct NewsItem {
    let headline: String
    let publishedOn: String
    let sourceURL: String
}

A required sourceURL will be filled with a well-formed URL. It will look like the URLs in your prompt. If your prompt mentions Apple developer news, it will look like a developer.apple.com link. Whether anything was actually retrieved has no bearing on this, because the schema constrains structure and nothing else. That is precisely the failure the 3 September scan would have produced if it had pressed on: correct field types, valid URLs, invented dates.

The fix is to stop asking the model where it got something. Treat provenance as data you own. Record every successful tool call in a ledger keyed by request, and when a generated item claims a source, resolve that claim against the ledger before it renders. Anything unmatched is a fabrication, regardless of how well-formed it is.

struct CallLedger {
    private var completed: Set<String> = []
    mutating func shouldIssue(_ key: String) -> Bool {
        completed.insert(key).inserted
    }
    func retrieved(_ key: String) -> Bool { completed.contains(key) }
}

That same key is your deduplication guard. One structure prevents the loop bug and validates provenance. (These sketches are illustrative and uncompiled — see the last section.)

Three UI states, not two

Most AI features ship with two states: loading and answer. Sometimes a third for a hard error. What’s usually missing is the state this whole post is about — the feature worked, the model responded, and the answer is unverified because retrieval didn’t happen.

That state needs its own rendering, and it needs to be visibly different from a normal answer rather than an answer with a small disclaimer under it. “I couldn’t check the source” is not a footnote on a result; it replaces the result. In a research context, an empty panel that says the data source was unreachable and offers a retry is more valuable than a filled panel that’s 90% likely to be right.

It also needs to be reachable from your prompt design. Give the model an explicit permitted output for “retrieval failed, I am not answering,” and make it a first-class branch in your schema rather than something it has to express in prose that your parser then has to detect.

This is the part that costs product arguments rather than engineering time. Abstention looks like the feature not working. It’s the only version where the feature is trustworthy under failure, which is the only condition under which trust is worth anything.

Log the difference, or you can’t debug it

In production you will get a bug report that says the AI made something up. To act on it, you need to know which of four things happened, and your logs need to separate them at the point of failure.

Did the tool call get issued at all, or did a budget guard swallow it? Did it get issued and return an error, and which error — quota, network, auth? Did it succeed and return output too large for the remaining context, surfacing later as .exceededContextWindowSize? Or did everything work and the model simply generated a claim with no matching ledger entry?

Those four produce identical user-visible symptoms and completely different fixes. Log the call key, whether it was deduplicated, the error string verbatim, the token size of the tool output, and the remaining context estimate at generation time. Then log every provenance mismatch as its own event class, because that’s your fabrication rate and it’s the number worth watching over releases.

The 3 September incident was diagnosable in minutes for exactly one reason: the transcript recorded the tool errors as tool errors, so the doubled queries were visible as doubled queries rather than as an inexplicably early quota wall.

What isn’t verified here

The same session that supplied this post’s example also ran out of search budget mid-run, which left real gaps. Being explicit about them is cheaper than being wrong.

App Store Review Guideline 5.1.2(i) and the June 2026 guidelines update were queued and never retrieved. This post therefore makes no claim about what current review policy requires of AI features around data sourcing or disclosure — check the guidelines directly before you rely on anything. The academic literature on model abstention was also unverified, so nothing here characterises “the research” on when models decline to answer. No App Store pricing figures were retrieved, so none appear.

iOS 27 was announced on 8 June 2026 and is in beta. As of 3 September 2026 it is not released, and no confirmed release date had surfaced. If you’re reading this well after publication, treat the platform baseline as stale and re-check it.

The code above is illustrative and uncompiled. Several Foundation Models symbols date from the iOS 26 cycle and warrant a pass through Xcode 27 before you trust them — in particular, @Generable on enums with associated values is something to verify yourself rather than take on faith from any blog post, including this one.