@sema-agent/core
Version:
Stateless, task-oriented AI agent core
69 lines (68 loc) • 4.1 kB
TypeScript
import type { BackgroundAgentRecord } from "./background-agent-store.js";
/**
* The protocol's task states, as ONE list — the type is derived from it, so a value and its type can
* never drift apart, and a consumer validating wire input iterates the same source the mapping does.
*/
export declare const A2A_TASK_STATES: readonly ["submitted", "working", "input-required", "completed", "canceled", "failed", "rejected", "auth-required", "unknown"];
/** One A2A task state (see {@link A2A_TASK_STATES}). */
export type A2ATaskState = (typeof A2A_TASK_STATES)[number];
/**
* Durable row → protocol state. TOTAL and pure: every (status, stoppedBy) pair has an answer, and the
* answer depends on nothing else.
*
* The four direct arms:
* - `running` → `working`;
* - `completed` → `completed`;
* - `failed` → `failed`;
* - `parked` → `input-required`. This engine's `parked` is durably suspended ON AN APPROVAL CHECKPOINT
* — a NARROWING of the protocol's "waiting for the caller to supply something": the something is an
* approval decision, and it is redeemed through the durable approval inbox, not by a message reply.
*
* `killed` splits on the attribution, because the protocol distinguishes what this engine records in a
* second field: a stop by `user` or `parent` is a deliberate cancellation (`canceled`), while a
* `system` stop, an unrecognized source, or NO attribution at all is not — those are terminations the
* requester never asked for, and `canceled` would assert an intent nobody expressed. `failed` is the
* honest terminal for them.
*
* FOUR PROTOCOL STATES HAVE NO ARM HERE, as a fact of the type rather than an omission:
* - `submitted`, `rejected`, `auth-required` describe moments BEFORE a durable row exists (registration
* and start are one step in this engine; rejection and an authorization demand are admission-time
* outcomes of the serving layer, which has no row to project yet);
* - `unknown` belongs to the READ-PROJECTION layer one level up — the poll face's `not_local` arm,
* where a `running` row was written by ANOTHER engine instance and this instance genuinely cannot
* speak to its outcome. That arm has no record-level answer to give and never calls this function; a
* caller holding a record, by contrast, always has the state. Keeping the two apart is the boundary
* this function is drawn on.
* The switch is exhaustive over `status` with NO default arm, so tsc — not this comment — is what fails
* when a sixth durable status is added.
*/
export declare function toA2ATaskState(record: Pick<BackgroundAgentRecord, "status" | "stoppedBy">): A2ATaskState;
/** A reversal that keeps everything the protocol state carried. */
export interface A2ATaskStateReversalFaithful {
lossy: false;
status: BackgroundAgentRecord["status"];
}
/**
* A reversal that DROPPED something. `note` is required on this arm by construction: the whole purpose
* of the discriminant is that a consumer which branches on `lossy` cannot reach the lossy case without
* text describing what it lost.
*/
export interface A2ATaskStateReversalLossy {
lossy: true;
status: BackgroundAgentRecord["status"];
note: string;
}
/** The result of {@link fromA2ATaskState}: discriminated on `lossy`. */
export type A2ATaskStateReversal = A2ATaskStateReversalFaithful | A2ATaskStateReversalLossy;
/**
* Protocol state → durable row status. Nine onto five, so five arms lose information; each of those
* says so (`lossy: true`) and names what it dropped, rather than returning a bare status that reads as
* an exact translation.
*
* `lossy: false` holds EXACTLY for the states that survive `toA2ATaskState(fromA2ATaskState(s))`
* unchanged — that biconditional is pinned as a property test (`test/a2a-task-state.test.ts`), so the
* flag is tied to an observable fact and not to an author's intent.
*
* The exhaustive switch (no default arm) makes a newly-declared protocol state a compile error here.
*/
export declare function fromA2ATaskState(state: A2ATaskState): A2ATaskStateReversal;