Skip to content

Runtime Errors

Orca failures flow through a single discriminated union, RuntimeError, carried in the E channel of every Result (see Errors and Results). Because the error type is a closed set of tagged variants, agents and humans can pattern-match exhaustively on error._tag instead of string-matching messages.

RuntimeError is a z.discriminatedUnion("_tag", ...) defined in src/model/schemas.ts. Its members:

type RuntimeError =
| { _tag: "NothingToCommit" }
| { _tag: "BranchAlreadyExists"; branch: string }
| { _tag: "PushRejected"; remote?: string; stderr: string }
| { _tag: "CommandFailed"; command: string; exitCode: number | null; stdout: string; stderr: string }
| { _tag: "StructuredOutputValidationFailed"; issues: readonly string[]; raw: unknown }
| { _tag: "UnsupportedFeature"; feature: string; reason: string }
| { _tag: "BackendFailed"; backend: BackendTag; message: string }
| { _tag: "TypecheckFailed"; stdout: string; stderr: string; exitCode: number | null }
| { _tag: "FileSystemError"; path: string; message: string }
| { _tag: "IoFailed"; seam: "source" | "sink" | "tool"; kind: string; message: string };

Five variants have dedicated constructor functions in src/model/errors.ts. The remaining five (NothingToCommit, BranchAlreadyExists, PushRejected, TypecheckFailed, FileSystemError) are constructed inline at their call sites as tagged object literals.

export function unsupportedFeature(feature: string, reason: string): RuntimeError;
export function backendFailed(backend: BackendTag, message: string): RuntimeError;
export function commandFailed(args: {
command: string;
exitCode: number | null;
stdout: string;
stderr: string;
}): RuntimeError;
export function structuredOutputValidationFailed(args: {
issues: readonly string[];
raw: unknown;
}): RuntimeError;
export function ioFailed(seam: "source" | "sink" | "tool", kind: string, message: string): RuntimeError;

This table is the operation→tag map. When you pattern-match a Result’s error, the _tag tells you what kind of operation failed.

_tagProduced byTypical recovery
NothingToCommitgit.commit() when the index is emptySkip the commit; not an error condition for the caller.
BranchAlreadyExistsgit branch creation when the branch existsReuse the existing branch or pick a new name.
PushRejectedgit push rejected by the remoteRebase or force-with-lease after reviewing the remote.
CommandFailedCommandTool.run() and shell subprocessesInspect command/exitCode/stderr; retry or surface.
StructuredOutputValidationFailedStructured-output parsing when the schema rejectsRe-prompt or fall back; issues lists the violations.
UnsupportedFeatureFeature gates that refuse a request (unsupportedFeature())The request is not supported in this configuration — do not retry as-is.
BackendFailedAn LLM backend returned a fatal error (backendFailed())Check backend/message; may be transient — retry with backoff.
TypecheckFailedThe typecheck runner after a code changeRead stderr, fix the typing errors, re-run.
FileSystemErrorFsTool, snapshot store, and file sinks on IO failureCheck path/message; permissions, missing dir, disk.
IoFailedLoop source/sink/tool seams (ioFailed())seam says where (source/sink/tool), kind the adapter; often retried by the loop supervisor.

Because RuntimeError is discriminated on _tag, a switch is exhaustive at the type level. Within a flow, errors arrive in the E channel of a Result; within a loop run, they are wrapped as LoopRunError = RuntimeError | TerminationContractError (see Loop API).

import { git } from "@twelvehart/orcats";
const result = await git().commit("ship it");
if (result.isErr()) {
switch (result.error._tag) {
case "NothingToCommit":
// not an error for us — nothing staged
break;
case "CommandFailed":
console.error(result.error.stderr);
break;
// ...remaining tags — the compiler flags any you miss
}
}
  • RuntimeError values are plain data — constructing one does not throw or perform IO. The throw/return boundary is the caller’s: tools return Result<_, RuntimeError> rather than throwing.
  • _tag is the discriminant and is stable across versions; the payload fields may grow but existing fields are not renamed without a major version. When matching, branch on _tag and read fields inside the matched arm.
  • BackendTag on BackendFailed is one of claude, codex, opencode, pi (see Backend Matrix).