v2.0.0next
Agent harnesses

Fallback and escalation

Where a launch goes next when a harness is limited or unavailable, and which harness runs an escalation turn.

A task can list other harnesses or models to try when its first choice is rate-limited or unavailable. An escalation turn runs on the harness that escalation_harness names.

Fallback

A fallback: list lets a task that must not wait out a rate limit move to another launch. It is opt-in: see Opt-in and limits.

Which harnesses can fall back

Only a harness that declares rate_limit_signal can trigger a switch on a rate limit. Any harness can be a fallback target.

HarnessTriggers a switch on a rate limitCan be a target
claude, codex, opencode, antigravityyesyes
gemini, amp, cursorno: a rate limit fails the launch as it does without a listyes, and Kraft skips it when it is unavailable

Entry keys

A fallback: list is ordered. Each entry may name a harness: (a harness profile id from harnesses.yaml) plus one route, the same either-or a task obeys: an agent profile:, or a model: and effort: (either or both). An entry keeps whatever it omits from the task's own launch.

The list lives on an agent profile, as the tier's default. A task may set its own, which replaces the agent profile's whole. fallback: [] means none, even under an agent profile that has one. For example:

config/harnesses.yaml
profiles:
  deep:
    providers:
      claude: { model: opus, effort: high }
      codex: { model: gpt-5.6-sol, effort: high }
    fallback:
      - { harness: codex }            # same tier, other harness
      - { profile: strong }           # other tier, same harness
  strong:
    providers:
      claude: { model: sonnet, effort: high }
      codex: { model: gpt-5.6-terra, effort: high }
config/library.yaml
tasks:
  legacy_task:
    kind: agent
    harness: claude
    model: opus
    effort: high
    fallback:
      - { model: sonnet }                          # claude, sonnet, high
      - { harness: codex, model: gpt-5.6-terra }   # codex, gpt-5.6-terra, high

How an entry resolves

Each entry resolves against the launch it replaces:

Entryon claude + profile: deepon claude + model: opus, effort: high
{ harness: codex }codex + deep: gpt-5.6-sol, highcodex + opus: refused, codex takes no opus
{ profile: strong }claude + strong: sonnet, highclaude + strong
{ model: sonnet }claude + sonnet, no effortclaude + sonnet, high

Which lists apply

  • Lists do not chain: Kraft does not follow the list of an agent profile that an entry names.
  • An agent profile's list is read at each launch, so an edit applies to the next launch.
  • With extends, a child task's fallback: replaces its parent's.

Validation

Every entry of every list a chain's task uses must pair with its harness, the same check a task's own route gets. Kraft runs it in:

  • Settings › Harnesses, which shows each agent profile's list with its problems per entry;
  • the guard on a harness save;
  • kraft admin doctor.

A problem names the task, the list's source and the entry's index:

chain 'default' task 'implementation.main.implement': fallback entry 0 (profile 'deep''s list): ...

A disabled harness is not a pairing problem, since the launch skips it.

When Kraft moves on

The candidates are the task's own launch, then each entry in order. Kraft moves to the next candidate, in the same dispatch, when:

  • a launch ends rate-limited (a harness that declares rate_limit_signal reported a rejected request). The next launch is a fresh session in the same worktree, with the task's instruction and a note that the earlier attempt may have left partial work (git status, git diff);
  • a candidate is unavailable before anything launches. Kraft checks this again at every launch, so fixing it takes effect at once. A candidate is unavailable when:
    • its harness profile is missing, disabled or sets a default Kraft cannot apply;
    • its agent profile is missing or has no model for its provider;
    • its executable is not on PATH. A sandbox skips this check, because the executable lives in the container;
  • a candidate is known to be limited: Kraft remembers, from the event log, which harness and model a rate_limit_hit limited, on any work item, and skips that pair until its reset. One account-wide limit therefore costs one quickly-refused launch per model before each is remembered.

When no candidate is left and one was limited, the item parks as rate_limited until the earliest reset among them. Its relaunch starts from the top of the list, so the first choice is used again as soon as it is back. When every candidate is unavailable, the task stops and waits for you, with a reason naming each one and why.

What carries over

  • The work item's set-overrides values and a node's model and effort override pick the task's own launch only, and so does a fix loop's escalation model. A fallback runs exactly as its entry says. A node's extra_prompt is part of the instruction and does reach it.
  • Switching spends none of rate_limit_retries, which still counts parks.
  • The spend caps in budget: are checked before every launch, fallbacks included. A task's time cap covers all of its attempts together.
  • Every entry's harness must be in the task's allowed_harnesses. Kraft checks a task's own list when the chain materializes. An agent profile's list is read at each launch, so Kraft checks it at the launch, which skips a refused entry as unavailable.
  • deny_tools, allowed_tools and the item's sandbox apply to a fallback as to the task's own launch. A sandboxed fallback onto Cursor or Codex with a tool policy therefore stops as a configuration error, rather than skipping to the next entry.

Opt-in and limits

  • A task with no list (none of its own, none on its agent profile, or fallback: []) launches, parks and stops exactly as it would without one, and never consults the record of limited models.
  • No shipped task or agent profile declares a list.
  • A gate's auto_review task cannot declare a fallback: list of its own. Kraft refuses it at its own launch, naming the gate and the agent profile, when its profile: carries one: a gate review launches once and never walks either list.

What is logged

Every skip or switch is logged:

  • one launch_fallback event (kraft view events --type launch_fallback), with from, to (null when nothing was left), reason (rate_limit_hit, known_limited or unavailable, with a detail), resets_at_iso and override_not_carried;
  • one sentence on the item's timeline, such as "Ran on claude / sonnet instead of claude / opus: claude / opus is rate-limited until 15:40.";
  • one INFO line in the server log;
  • a "fallback" marker on the board card while the item's latest launch is a fallback's, with the same sentence as its tooltip.

Session rows record the model that actually ran, so each session's cost is priced on that model, and the Analytics By node cost reflects it.

Escalation turns

An escalation turn runs on the harness profile in harnesses.yaml that the policy key escalation_harness names: claude unless you set it, or item, which follows the harness the item's own latest agent task ran on.

Where to set itHow
policy.yamlescalation_harness: under defaults:
A repository, chain or nodeescalation_harness: in its policy:
One itemkraft item set-policy --policy escalation_harness=codex

Kraft reads the thread id the next turn resumes with that harness's own log reader, and a resumed turn records only what it added to the thread. See escalation_harness.

A gate's automated review is not an escalation turn: its auto_review task names its own harness: in the chain.

Copyright © 2026