Skip to main content
// Props
import { EscalationChain } from "smithers-orchestrator";

type EscalationChainProps = {
  id?: string; // default "escalation"
  levels: EscalationLevel[];
  humanFallback?: boolean; // default false
  humanRequest?: ApprovalRequest;
  escalationOutput: z.ZodObject | { $inferSelect: Record<string, unknown> } | string;
  skipIf?: boolean;
  children?: ReactNode; // prompt forwarded to every level
};

type EscalationLevel = {
  agent: AgentLike;
  output: z.ZodObject | { $inferSelect: Record<string, unknown> } | string;
  label?: string;
  escalateIf?: (result: unknown) => boolean; // true -> next level
};
<Workflow name="support-ticket">
  <EscalationChain
    id="support"
    escalationOutput={outputs.escalation}
    humanFallback
    humanRequest={{ title: "Ticket needs human support", summary: "Agents could not resolve." }}
    levels={[
      { agent: fastAgent, output: outputs.tier1, label: "Tier 1", escalateIf: (r) => r.confidence < 0.7 },
      { agent: powerAgent, output: outputs.tier2, label: "Tier 2", escalateIf: (r) => r.confidence < 0.9 },
    ]}
  >
    Resolve this ticket: {ctx.input.ticketBody}
  </EscalationChain>
</Workflow>

Notes

  • Each level uses continueOnFail; failures propagate to the next level.
  • escalateIf is evaluated at render time. The chain re-renders reactively as each level’s output becomes available and calls the predicate to decide whether the next level mounts.

Source

The <EscalationChain> implementation and the files it imports, straight from the package source. This section is generated; edit the source, not this block.
// @smithers-type-exports-begin
/** @typedef {import("./EscalationChainProps.ts").EscalationChainProps} EscalationChainProps */
/** @typedef {import("./EscalationLevel.ts").EscalationLevel} EscalationLevel */
// @smithers-type-exports-end

import React from "react";
import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
import { Sequence } from "./Sequence.js";
import { Branch } from "./Branch.js";
import { Task } from "./Task.js";
import { Approval } from "./Approval.js";
/**
 * Default escalation predicate: escalate when the previous level has no result
 * yet, or its result signals a failure (`error`/`failed` truthy or `ok === false`).
 * @param {unknown} result
 * @returns {boolean}
 */
function defaultEscalateIf(result) {
    if (result == null)
        return true;
    if (typeof result === "object") {
        const row = /** @type {Record<string, unknown>} */ (result);
        if (row.error != null && row.error !== false)
            return true;
        if (row.failed === true)
            return true;
        if (row.ok === false)
            return true;
    }
    return false;
}
/**
 * Resolve whether the previous level escalated by invoking its `escalateIf`
 * predicate (or the default) against its actual result.
 * @param {EscalationLevel} prevLevel
 * @param {unknown} prevResult
 * @returns {boolean}
 */
function didEscalate(prevLevel, prevResult) {
    const predicate = prevLevel.escalateIf ?? defaultEscalateIf;
    return Boolean(predicate(prevResult));
}
/**
 * Escalation chain: tries agents in order, escalating on failure or when
 * `escalateIf` returns `true`. Optionally ends with a human approval fallback.
 *
 * Composes Sequence + Task (with `continueOnFail`) + Branch + Approval.
 * @param {EscalationChainProps} props
 */
export function EscalationChain(props) {
    if (props.skipIf)
        return null;
    const ctx = React.useContext(SmithersContext);
    const prefix = props.id ?? "escalation";
    const { levels, children, humanFallback, humanRequest, escalationOutput } = props;
    // Build the chain from the last level forward, nesting each level inside a
    // Branch that gates on the previous level's escalation condition.
    // We construct the elements bottom-up so the final element is a single
    // Sequence that evaluates top-down at runtime.
    const levelElements = [];
    for (let i = 0; i < levels.length; i++) {
        const level = levels[i];
        const levelId = `${prefix}-level-${i}`;
        const isFirst = i === 0;
        const taskEl = React.createElement(Task, {
            id: levelId,
            output: level.output,
            agent: level.agent,
            continueOnFail: true,
            label: level.label ?? `Escalation level ${i}`,
            children: children,
        });
        if (isFirst) {
            // First level always runs.
            levelElements.push(taskEl);
        }
        else {
            // Subsequent levels are gated by a Branch that checks whether the
            // previous level needs escalation. The chain re-renders reactively as
            // outputs become available, so we read the previous level's actual
            // result from the workflow context and run its `escalateIf` predicate
            // (or the default failure predicate) to decide whether this level runs.
            const prevLevel = levels[i - 1];
            const prevLevelId = `${prefix}-level-${i - 1}`;
            const prevResult = ctx?.outputMaybe(prevLevel.output, { nodeId: prevLevelId });
            const escalated = didEscalate(prevLevel, prevResult);
            const checkId = `${prefix}-check-${i - 1}`;
            const checkTask = React.createElement(Task, {
                id: checkId,
                output: escalationOutput,
                continueOnFail: true,
                label: `Check escalation from level ${i - 1}`,
                children: () => {
                    // Record the escalation decision for the prior level so it is
                    // visible in the escalation output stream.
                    return {
                        escalated,
                        fromLevel: i - 1,
                        toLevel: i,
                    };
                },
            });
            // Gate the current level on the previous level's escalation decision:
            // it only mounts when the prior level actually escalated.
            const gatedLevel = React.createElement(Branch, {
                if: escalated,
                then: taskEl,
            });
            levelElements.push(checkTask);
            levelElements.push(gatedLevel);
        }
    }
    // Append human fallback if requested. It only mounts when every automated
    // level escalated (i.e. all automated levels were exhausted). A single
    // level resolving without escalation stops the chain and the fallback, even
    // if later levels never ran and therefore have no recorded result.
    if (humanFallback && levels.length > 0) {
        const humanId = `${prefix}-human-fallback`;
        const request = humanRequest ?? {
            title: "Escalation requires human review",
            summary: `All ${levels.length} automated levels have been exhausted.`,
        };
        const allEscalated = levels.every((level, idx) => {
            const levelResult = ctx?.outputMaybe(level.output, {
                nodeId: `${prefix}-level-${idx}`,
            });
            return didEscalate(level, levelResult);
        });
        const approvalEl = React.createElement(Approval, {
            id: humanId,
            output: escalationOutput,
            request,
            continueOnFail: true,
            label: request.title,
        });
        levelElements.push(React.createElement(Branch, {
            if: allEscalated,
            then: approvalEl,
        }));
    }
    return React.createElement(Sequence, {}, ...levelElements);
}