Skip to content

Fallback to the cloud

A fallback is a function from a FallbackRequest to { text }. Goliath calls it when the loop stalls or the model fails, and marks the result handledBy: "cloud".

const agent = createAgent({
model: apple(),
tools,
fallback: async ({ ask, summary, recent, steps, reason }) => {
const text = await cloudAgent.turn({ ask, summary, recent, steps, reason });
return { text };
},
});

The request carries the ask, the memory brief, the recent exchanges, the step log, and the reason. It never carries a transcript: the cloud agent gets the same summary the conductor had.

For the common case, post it as JSON and read { text } back:

import { httpFallback } from "@hellohelen-ai/goliath";
createAgent({
model: apple(),
tools,
fallback: httpFallback({
url: "https://api.example.com/assistant/turn",
headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
}),
});

See the reference for readText and fetch overrides.

Three turns in a row that die on the device with a model error flip agent.sessionFallback to true. From then on, turns go straight to the fallback without paying on-device latency. Claude Code does the same after three overloads.

Session fallback runs through the same lifecycle as other turns: start, recall, fallback policy, answer processing, memory, and finalization. After a model-error handoff, Goliath keeps the prior summary and latest three exchanges without asking the failed device to summarize; older exchanges are dropped on that route.

Use an extension’s beforeFallback hook to redact a copy of the outbound request or stop the run before any cloud call. It covers normal and session fallback. Hook failures reject instead of triggering model-error fallback, and a failing fallback is not invoked a second time. Memory-save and tool-formatting failures preserve completed work without a cloud handoff. See Lifecycle extensions.

Without one, a stalled turn still returns a best-effort answer written from the step log, with bestEffort: true on the result. The example app deliberately has no fallback so that escalation stays visible. Nonempty best-effort replies and their tool records are saved to memory with bestEffort: true on the exchange, and shown to later model calls as partial replies. This retains useful evidence without declaring unfinished actions complete. An empty failed reply or provider refusal is not persisted through this path.

A guardrail hit does not escalate. Sending the flagged text to a server is the wrong reflex, and most guardrail hits on tool output are false positives. The turn ends on the device with the reason guardrail.