Skip to content

Adaptive recovery

OMA keeps task graphs fixed by default. Opt into recovery.mode: 'repairable' when an application needs to revise the not-yet-executed part of a run after a task succeeds, fails, or is rejected by consensus verification.

Adaptive recovery is an outcome barrier, not another retry:

  1. A task produces an outcome.
  2. The configured Replanner (or onTaskOutcome callback) may propose a PlanPatch.
  3. OMA validates agent eligibility, limits, task states, references, and the resulting DAG.
  4. The optional onPlanPatch gate approves or rejects the proposal.
  5. OMA applies the patch atomically, persists a checkpoint when checkpointing is enabled, then publishes newly-ready work.
  6. Only after that barrier does the triggering task complete or fail and release or cascade its original dependents.

This ordering prevents an original downstream task from starting while its replacement is still being decided.

import {
OpenMultiAgent,
type Replanner,
type TaskOutcome,
} from '@open-multi-agent/core'
const replanner: Replanner = {
name: 'fallback-search',
replan(outcome: TaskOutcome) {
if (outcome.kind !== 'failure' || outcome.task.title !== 'Search') {
return undefined
}
const oldAnalysis = outcome.tasks.find((task) => task.title === 'Analysis')
if (!oldAnalysis) return undefined
return {
reason: 'Primary search failed; use the fallback source.',
supersedePending: [oldAnalysis.id],
addTasks: [
{
key: 'fallback-search',
title: 'Fallback Search',
description: 'Fetch the source through the fallback path.',
assignee: 'researcher-b',
},
{
key: 'replacement-analysis',
title: 'Replacement Analysis',
description: 'Analyze the fallback result.',
assignee: 'analyst',
dependsOn: ['fallback-search'],
},
],
}
},
}
const result = await oma.runTasks(team, tasks, {
recovery: {
mode: 'repairable',
replanner,
maxPlanRevisions: 3,
maxAddedTasks: 20,
onPlanPatch: async (patch, outcome) => {
return await applicationPolicyApproves(patch, outcome)
},
},
})

onTaskOutcome is a shorthand for applications that do not need a named Replanner object. Configure one or the other, not both. A custom replanner may call an LLM or another service, but that external I/O and its usage accounting remain owned by the application.

  • addTasks appends tasks. Every appended task has a patch-local key. Dependencies may refer to another key in the same patch or to an existing task ID.
  • retargetPending changes the assignee of a pending or blocked task after the new agent passes the same eligibility checks used by scheduling.
  • supersedePending marks a pending or blocked task skipped. Replacement tasks are appended rather than rewriting or deleting history.

Started or terminal tasks cannot be retargeted or superseded. A failure or verification rejection is classified as recovered only when the accepted patch appends at least one replacement task. Successful tasks may append or reshape downstream work without being marked recovered.

Patch references use task IDs, not titles. This avoids ambiguity when multiple runtime tasks have the same title.

Without an accepted patch, existing behavior is unchanged: failed tasks cascade failure to their dependents while independent branches may continue.

Accepted revisions are stored in queue snapshot version 2. Snapshot version 1 remains the fixed-DAG format and is still readable. When checkpointing is enabled, OMA saves the patched graph before it dispatches appended work. If that save fails, the unpublished patch is rolled back and the original failure path continues.

Restore resets an interrupted task for execution while retaining durable plan revision history. A revision with the same trigger task and outcome is not appended twice after a crash.

Historical tasks remain truthful in result.tasks: a repaired failure is still failed with recoveredByRevision, and a replaced branch is skipped with supersededByRevision. Those historical records do not make the overall run fail when the active repaired graph finishes successfully. Accepted revisions are returned in result.planRevisions.

  • Recovery is opt-in. Existing callers remain fixed-DAG.
  • runFromPlan() is exact replay and rejects repairable recovery.
  • Repairable recovery is incompatible with legacy round-based onApproval. Use onTaskDispatch and/or onPlanPatch gates.
  • Limits reject further patches; they never silently truncate a patch.
  • Policy, validation, approval, revision, and checkpoint decisions are exposed through progress events and observability spans.
  • A repair is forward-only. OMA does not undo external side effects already performed by a task.