The outcome of an action: what became of it, who can see it, whether that was checked, how to find it again and what to run next, in one shape over a CLI's --json, an HTTP route and an MCP tool.
npx shadcn@latest add ourostack/ouro-ui/ax-outcome#v1.3.10It installs lib/ouro/ax/outcome.ts, with ax-effects, ax-exit-codes, ax-problem. Its tests are ax-outcome-tests. Every item runs on Node 22.18 or later as it is copied: the files import each other as ./<file>.ts. The Ouro AX guide shows how they fit together.
Exports
AxRef
A thing an agent holds a reference to: a conversation, a message, a meeting, a request. id is what the tool takes back.
interface AxRef {
kind: string;
id: string;
name?: string;
}DEFAULT_OUTCOME_EXITS
The default: what was done, queued, planned or handed to the person exits ok (the outcome says which); a refusal exits usage; a failure or an uncertain result exits error. A tool with its own codes (such as "possibly done") passes its own names.
const DEFAULT_OUTCOME_EXITS: OutcomeExitsisGenericAudience
Whether an audience's who names no one in particular ("other people", "someone", "everyone"), so an agent reading the JSON learns less than one reading the text.
function isGenericAudience(who: string): booleanisOutcome
Whether a value is an outcome of this version (its shape only; validateOutcome checks its meaning).
function isOutcome(v: unknown): v is OutcomenextStep
An exact next step.
function nextStep(command: string, why: string): NextStepNextStep
One next valid action: an exact command an agent can run, and why it would.
interface NextStep {
command: string;
why: string;
sourceCheckout?: true;
}outcome
Builds an outcome with honest defaults: private, only you, nothing checked, no receipt and no next steps. Pass what is known; validateOutcome says what is missing.
function outcome(fields: OutcomeFields): OutcomeExample:
outcome({ action: "message.send", effect: "others", audience: { only: false, who: 'everyone in "Design sync"' },
state: "queued", summary: 'Queued for "Design sync".',
receipt: { id: "a1b2c3", at: new Date().toISOString(), by: "agent:planner", why: "Jo asked" },
next: [nextStep("tool outcome a1b2c3 --timeout 60s", "wait until it is sent")] });Outcome
The envelope an action returns.
interface Outcome {
ax: typeof OUTCOME_VERSION;
action: string;
effect: Effect;
audience: Audience;
target: AxRef | null;
state: OutcomeState;
summary: string;
postcondition: Postcondition;
receipt: Receipt | null;
next: NextStep[];
error?: AxErrorBody;
human?: OutcomeHuman;
}OUTCOME_STATES
What became of an action.
done: it happened; the postcondition says how that is known.queued: accepted, not performed yet;receipt.idandnextfollow it.planned: a dry run; nothing happened.needs-human: only a person can take the next step;humansays why and how to resume.uncertain: it may or may not have happened; reconcile before any retry.refused: checked and not done, with the reason; nothing happened.failed: tried and proven not done.
const OUTCOME_STATES: readonly ["done", "queued", "planned", "needs-human", "uncertain", "refused", "failed"]OUTCOME_VERSION
The outcome of an action: what became of it, who can see it, whether that was checked, how to find it again and what to run next, in one shape over a CLI's --json, an HTTP route and an MCP tool.
const OUTCOME_VERSION: "outcome/1"outcomeExit
The exit code an outcome gives: a refusal's or failure's own error exit, else its state's name in the table.
function outcomeExit(o: Outcome, exits: ExitTable, names?: Readonly<Record<"planned" | "done" | "queued" | "needs-human" | "uncertain" | "refused" | "failed", string>>): numberOutcomeExits
Exit code names by outcome state, in the tool's table.
type OutcomeExits = Readonly<Record<OutcomeState, string>>;OutcomeFields
What outcome takes: the action, its state and summary, and whatever else is known.
type OutcomeFields = Pick<Outcome, "action" | "state" | "summary"> & {
effect?: Effect;
audience?: Audience;
target?: AxRef | null;
postcondition?: Postcondition;
receipt?: Receipt | null;
next?: NextStep[];
error?: AxErrorShape;
human?: OutcomeHuman;
};OutcomeHuman
Why only a person can go on, and the step that resumes once they have.
interface OutcomeHuman {
reason: string;
resume: NextStep;
}OutcomeState
type OutcomeState = (typeof OUTCOME_STATES)[number];outcomeText
The outcome as text for a person: the summary, what was checked (or why it is not checked yet), what only the person can do, then the next steps. A refusal or failure with an error is the one error line, as every Ouro AX error is.
function outcomeText(o: Outcome, tool: string): stringPostcondition
The check that the effect holds, made apart from the transport's answer. holds is null while the effect cannot be observed yet; how names what was read.
interface Postcondition {
checked: boolean;
holds: boolean | null;
how: string;
}Receipt
A durable record of the request: the id that finds it again, when, by whom and why.
interface Receipt {
id: string;
at: string;
by: string;
why: string | null;
}validateOutcome
Checks an outcome's meaning, not only its shape:
donenever rests on a postcondition that failed; one not checked is a warning.refusedandfailedcarry an error and never claim the effect holds.queuedanduncertaincarry a receipt and a next step, anduncertaincannot claim to know.- an effect other people see carries a receipt with a why, unless nothing happened, and an audience that is not only you.
needs-humancarries the reason and the step that resumes.- a next step is an exact command with its why.
function validateOutcome(v: unknown): ValidationValidation
Problems that make an envelope untrustworthy are errors; honest gaps are warnings.
interface Validation {
ok: boolean;
errors: string[];
warnings: string[];
}