/** * Mission runner — Phase 1.c of the AgenticROS strategy. * * A mission is a declarative sequence of capability invocations. Each step * names a capability (from the registry exposed by `ros2_list_capabilities`) * with literal or templated inputs, where templates can reference outputs * from earlier steps via `{{stepId.outputs.fieldName}}`. * * The runner here is transport-agnostic — it takes a `dispatcher` callback * that knows how to invoke a single tool for the host adapter. Adapters * pass their existing tool dispatch path (handleToolCall in claude-code, * tool.execute() in OpenClaw, executeTool() in Gemini), and the runner * stays the same across all three. * * Phase 1.d adds multi-robot routing: `mission.robot_id` (optional) and * per-step `inputs.robot_id` are auto-injected into every dispatched * tool call when the binding's tool args don't already specify one. The * adapter-side tool handlers then resolve the robot via * `resolveRobotFromArgs(config, args)` and route the underlying * publish/subscribe/etc. through that robot's namespace. * * Precedence (highest wins): * 1. Tool args produced by the binding (`binding.buildArgs(inputs)`) * — if it sets `robot_id`, that's final. * 2. Per-step `inputs.robot_id` (lets a mission mix robots step-by-step). * 3. Mission-level `mission.robot_id` (the default for every step). * 4. None — the adapter falls back to the active robot. * * Phase 1.f extends the runner with two additive capabilities: * - `cancellation`: a token the runner checks BEFORE each step. The * adapter holds the token in its `MissionRegistry` so a sibling * tool call (`mission_cancel(mission_id)`) can flip * `cancellation.cancelled` mid-run and stop the mission gracefully. * Steps that have already completed keep their results; the current * step finishes naturally (the runner doesn't preempt * `await dispatcher(...)` because that requires per-tool cancel * support — out of scope for Phase 1.f); subsequent steps are * marked `"cancelled"` and skipped. * - `transcript`: a callback invoked after every step with the * step's `MissionStepResult`. Adapters wire this to the shared * memory subsystem so a second agent can `recall(namespace= * "mission:")` and inspect what's run so far. The runner * itself owns no I/O — the callback can be a no-op when memory * is disabled. * * What's still deferred: * - Parallel step execution (today: sequential only). * * Shipped since the original Phase 1.f header: * - Natural-language plan compilation (`compileGoalToMission` + goal arg). * - Pause / resume via the same control token (`paused` flag). * - Step-level retry / backoff (`MissionStep.retry`). * - Mid-step cancel for interruptible capabilities via AbortSignal * on the dispatcher (non-interruptible steps still finish naturally). * * See: docs/strategy-ai-agents-plus-ros.md §4 (Phase 1.c / 1.f). */ import type { Capability } from "./capabilities.js"; /** * Cancellation / pause token consumed by `runMission`. * * Plain object (not AbortController) so it's easy to share across * processes via a registry without pulling Web platform shims in. The * runner reads `cancelled` and `paused` at each step boundary. */ export interface MissionCancellationToken { cancelled: boolean; /** * When true, the runner waits at the next step boundary until * `paused` is cleared or `cancelled` is set. Phase 1 pause/resume. */ paused?: boolean; /** Optional free-text reason — bubbled up into cancelled / paused results. */ reason?: string; } /** Alias — control token is the same object as the cancellation token. */ export type MissionControlToken = MissionCancellationToken; /** * Per-step transcript sink. Called immediately after a step finishes * (including cancelled / skipped steps) so an external store sees the * whole timeline, not just the post-mortem `MissionResult`. * * The callback is best-effort: thrown errors are logged but never * propagate up — losing a transcript entry must not abort an * otherwise-healthy mission. */ export type MissionTranscriptSink = (entry: MissionTranscriptEntry) => Promise | void; /** One transcript entry per executed step. */ export interface MissionTranscriptEntry { /** Unique id of the running mission (assigned by the adapter, not the runner). */ mission_id: string; /** Free-text mission label. */ mission_name?: string; /** Adapter that ran the mission (e.g. "claude-code", "openclaw", "gemini"). */ adapter?: string; /** When the step started (ms since epoch). */ started_at: number; /** Mission-level robot id (empty when unset). */ robot_id?: string; /** Index of this step in mission.steps (0-based). */ step_index: number; /** Total steps in the mission. */ step_total: number; /** Snapshot of the per-step result. */ result: MissionStepResult; } /** One step in a mission. */ export interface MissionStep { /** Unique id within the mission, used by later steps for `{{id.outputs.x}}`. */ id: string; /** * Capability id (e.g. `drive_base`, `find_object`). The runner looks this * up in the capability registry and uses a dispatch map to find the * underlying tool to invoke. */ capability: string; /** * Inputs to the capability. Values can be literals or `{{stepId.outputs.x}}` * templates that resolve from prior steps' outputs. */ inputs?: Record; /** * Optional behaviour when this step fails. Defaults to "stop". * - "stop": halt the mission, mark it failed * - "continue": record the failure and run the next step anyway * Applied only after retry attempts (if any) are exhausted. */ on_fail?: "stop" | "continue"; /** * Optional retry / backoff when the step errors. Default is a single * attempt (`max_attempts: 1`). Retries re-invoke the same tool with * the same resolved args; `on_fail` applies only after the last attempt. */ retry?: { /** Total attempts including the first (minimum 1). */ max_attempts: number; /** Delay before the second attempt (ms). Default 0. */ backoff_ms?: number; /** Multiply backoff after each failed attempt. Default 1. */ backoff_multiplier?: number; }; } /** A complete mission plan. */ export interface Mission { /** Free-text label for logs / chat replies (e.g. "find chair and approach"). */ name?: string; /** * Optional planning notes from the agent — surfaced in the result for * downstream summarisation. The runner ignores it. */ goal?: string; /** * Optional default robot id for every step. Each step's tool args get * `robot_id` auto-injected from this when the step doesn't supply its * own and the binding's tool args don't already include one. Unknown * robot ids surface as a tool error from the adapter, not from the * runner. Empty/whitespace is ignored (uses the active robot). */ robot_id?: string; steps: MissionStep[]; } /** Result of a single step. */ export interface MissionStepResult { id: string; capability: string; /** * Final step outcome: * - "ok": tool returned cleanly, outputs (if any) parsed. * - "error": tool returned an error or the binding/build threw. * - "skipped": earlier step failed with on_fail=stop. * - "cancelled": mission was cancelled (Phase 1.f) before this step ran. * - "paused": transcript-only marker emitted when the runner entered * a pause wait before this step (not a final step outcome * in `MissionResult.steps` — those use ok/error/skipped/cancelled). */ status: "ok" | "error" | "skipped" | "cancelled" | "paused"; /** Resolved inputs (with `{{...}}` templates substituted). */ inputs: Record; /** Outputs parsed from the tool response (may be `undefined` for fire-and-forget). */ outputs?: Record; /** Free-form text the tool returned (e.g. for human display). */ message?: string; /** Error message when status is "error". */ error?: string; /** Wall time in ms. */ duration_ms: number; /** Number of dispatcher attempts used (including the successful one). */ attempts?: number; } /** Result of a mission run. */ export interface MissionResult { /** * Overall mission outcome: * - "ok": every step succeeded (or skipped on_fail=continue). * - "error": one or more steps errored with on_fail=stop. * - "cancelled": mission was cancelled mid-run (Phase 1.f). Some * steps may have completed successfully — see steps[]. */ status: "ok" | "error" | "cancelled"; /** Number of steps that ran (skipped/cancelled steps are NOT counted as run). */ steps_run: number; steps_total: number; /** Per-step results, in declaration order. */ steps: MissionStepResult[]; /** Total wall time across all steps. */ duration_ms: number; /** Brief one-line summary suitable for chat reply. */ summary: string; /** * Mission-level default robot id used during this run (surfaced for * traceability — useful in chat replies and logs). Empty string when * the mission didn't pin a robot. */ robot_id?: string; /** * Phase 1.f — the mission id the adapter registered before running. * Empty when the adapter didn't pass an id (e.g. ad-hoc dispatcher * tests). Use this with `mission_cancel(mission_id)` to abort a * mission mid-run, or `memory_recall(namespace="mission:")` to * read the transcript later. */ mission_id?: string; /** * Phase 1.f — set when the mission was cancelled. Carries the free-text * reason the canceller supplied (or "cancelled" when none was given). */ cancellation_reason?: string; } /** Optional context passed to each dispatcher invocation. */ export interface MissionDispatchContext { /** * AbortSignal aborted when the mission is cancelled mid-step and the * capability is `interruptible`. Long-running tools should poll * `signal.aborted` (or listen for `abort`) and stop cleanly. */ signal?: AbortSignal; } /** * A dispatcher invokes a single MCP tool by name and returns: * - `text`: the tool's text response (free-form, for human display) * - `outputs`: optional structured outputs (parsed JSON when the text * payload is a JSON blob). The runner uses these for * `{{...}}` template resolution by later steps. * * Adapters wrap their existing tool entry points to satisfy this contract. * The optional third argument carries an AbortSignal for mid-step cancel. */ export type MissionToolDispatcher = (toolName: string, args: Record, ctx?: MissionDispatchContext) => Promise<{ text: string; outputs?: Record; isError?: boolean; }>; /** Thrown (or returned via isError) when a tool stops due to AbortSignal. */ export declare class MissionStepAbortedError extends Error { constructor(message?: string); } /** * Capability → MCP tool mapping. Each entry says "to satisfy capability X, * call tool Y with these arguments". The transform receives the step's * (already template-resolved) inputs and returns the args object the tool * expects. * * Why a separate map (not a field on the capability itself)? * - Some capabilities map onto different tools per adapter * (claude-code's `ros2_find_object` is a single tool call; the OpenClaw * adapter equivalent runs in a different process). * - Skill-declared capabilities will eventually carry their own * implementation hint (`implementation.kind = "in_process"|"external_ros_node"`), * but until that wire-up is real, mapping happens here. */ export interface CapabilityToolBinding { /** The MCP tool name to invoke. */ tool: string; /** Transform the step's resolved inputs into the tool's expected arguments. */ buildArgs: (inputs: Record) => Record; /** * Optional: extract structured outputs from the tool's text response * for `{{...}}` template references by later steps. When omitted, the * runner attempts to JSON-parse the text and use the result. */ parseOutputs?: (text: string) => Record | undefined; } export type CapabilityToolBindings = Record; /** Phase 1.f optional behaviour bundle for `runMission`. */ export interface RunMissionOptions { /** * Stable identifier for this mission run. Adapters generate it and * register a cancellation token under this key, so a sibling * `mission_cancel(mission_id)` tool call can flip the token mid-run. * Surfaced in the result + every transcript entry. */ mission_id?: string; /** * Cancellation token the runner checks BEFORE each step. When * `cancelled` is true at a step boundary, the remaining steps are * marked "cancelled" and the mission returns with * `status: "cancelled"`. */ cancellation?: MissionCancellationToken; /** * Best-effort transcript sink — called after every step finishes * (including skipped/cancelled). Adapters wire this to the memory * subsystem so a downstream agent can read the timeline. Thrown * errors are swallowed; transcripts are not gated by the mission's * success. */ transcript?: MissionTranscriptSink; /** Adapter label (e.g. "claude-code", "openclaw", "gemini") — copied into transcripts. */ adapter?: string; /** * Optional override for the "capability not in registry" step error. * Adapters use this to explain hardware-profile mismatches * (`missing features: arm`) instead of a generic unknown-id message. */ unavailableMessage?: (capabilityId: string) => string | undefined; } /** * Execute a mission sequentially. * * @param mission The declarative plan. * @param capabilities Capability registry — used to validate that each * requested capability id is real. * @param bindings Capability → tool mapping; entries missing here yield * an "unsupported capability" error for that step. * @param dispatcher Adapter-supplied function that invokes a tool by name. * @param options Phase 1.f options (cancellation token, transcript sink, mission id). * Omit for legacy callers. */ export declare function runMission(mission: Mission, capabilities: Capability[], bindings: CapabilityToolBindings, dispatcher: MissionToolDispatcher, options?: RunMissionOptions): Promise; //# sourceMappingURL=mission.d.ts.map