@sema-agent/core
Version:
Stateless, task-oriented AI agent core
92 lines (91 loc) • 5.7 kB
TypeScript
/**
* A background agent's CURRENT state — the one reading every read face renders.
*
* A parent model answers "how far along is it?" from what its read faces show. Three faces read a
* background agent: AgentTranscript (its recent steps), ListAgents (the roster) and TaskOutput (the poll).
* Each one projects {@link readAgentState}'s result and renders it with {@link agentStateSummary} /
* {@link agentActivityClause} / {@link agentApprovalClause}; none derives status, activity time or approval
* facts on its own, so the three cannot disagree about one agent at one instant.
*
* Sources:
* - the registry row — the live handle this engine process runs ({@link agentStateSourceOfHandle}), or the
* durable record when no live handle answers ({@link agentStateSourceOfRecord});
* - for a row whose status is `parked` only, the approval checkpoint the row names, read through the run's
* checkpoint seat (the same seat the Agent tool parks children into). The checkpoint token is the resume
* capability and never leaves this module: a face renders the checkpoint's non-secret `checkpointId`.
*
* Absence is stated, never left silent: a missing activity time renders as "last activity unknown (…why)",
* and a parked row whose card cannot be read carries `approval.state` `missing` / `unreadable`. A face that
* goes quiet on a fact invites the reader to fill the gap with a guess.
*/
import type { SemaTaskStatus } from "./task-registry-shared.js";
import { type CheckpointSeat } from "./checkpoint-seat.js";
/** The approval card's state: the checkpoint row's own `status`, plus `missing` (the row names no card, or
* the store holds none under its token) and `unreadable` (no checkpoint seat on this face, or the store
* threw). The same five answers the parked-row reconciliation sweep gives the same question. */
export type AgentApprovalState = "pending" | "resolved" | "expired" | "missing" | "unreadable";
/** The approval checkpoint a parked agent waits on. */
export interface AgentApproval {
state: AgentApprovalState;
/** The checkpoint's non-secret identity (the card id a person sees in the approval inbox). Present when
* the card was read and carries one. */
checkpointId?: string;
/** The card's SLA / abandonment deadline, epoch ms. Present when the card was read and carries one. */
deadline?: number;
}
/** Where the row was read from: the live handle this engine process runs, or the durable record (the
* process that wrote it may be another one, or gone). */
export type AgentStateRow = "live_row" | "durable_row";
/** One agent's state at {@link readAt}. Times are epoch ms; relative ages appear only in rendered text. */
export interface AgentState {
/** The registry row's own status word. A word outside {@link SemaTaskStatus} reads `unknown`. */
status: SemaTaskStatus | "unknown";
/** The instant this state was read (epoch ms) — the base every rendered age is computed from. */
readAt: number;
row: AgentStateRow;
/** The row's last recorded change (epoch ms). Live row: every state transition and every tool start /
* end of the direct child moves it. Durable row: when it parked (`parked`) or settled (terminal).
* ABSENT when the source holds no such time — a durable row still marked running (activity in another
* process is not observable here), or a row that never recorded one. */
lastActivityAt?: number;
/** Present exactly when {@link status} is `parked`. */
approval?: AgentApproval;
}
/** The row facts {@link readAgentState} reads. Built by the two projections below — never by a face. */
export interface AgentStateSource {
status: string;
row: AgentStateRow;
lastActivityAt?: number;
parkedCheckpointToken?: string;
}
/** The live handle's facts: `updatedAt` is moved by every state transition and by each direct-child tool
* lifecycle beat, so it is the row's last activity. */
export declare function agentStateSourceOfHandle(h: {
status: string;
updatedAt: number;
parkedCheckpointToken?: string;
}): AgentStateSource;
/** The durable record's facts. Its `updatedAt` is the writer's liveness lease renewal, not activity, so it
* is never read as such: a parked record answers with `parkedAt`, a terminal one with `settledAt`, and a
* record still marked running answers nothing. */
export declare function agentStateSourceOfRecord(r: {
status: string;
parkedAt?: number;
settledAt?: number;
parkedCheckpointToken?: string;
}): AgentStateSource;
/**
* THE state of one background agent at `now`. Synchronous unless the row is parked on a card the seat can
* read — only that row takes an async frame (the store read); every other row is answered in place.
* The caller must already have passed the row through its face's access check: this reads the card the
* row names and authorizes nothing.
*/
export declare function readAgentState(src: AgentStateSource, seat: CheckpointSeat | undefined, now: number): AgentState | Promise<AgentState>;
/** `Ns` / `Nm` / `Nh` / `Nd` — a bounded age word; a negative span reads as `0s`. */
export declare function formatAgo(ms: number): string;
/** `last activity <age> ago`, or the reason the time is unknown. */
export declare function agentActivityClause(s: AgentState): string;
/** `approval <card id> <state>[ (deadline in <age> | deadline passed <age> ago)]` — facts only. */
export declare function agentApprovalClause(a: AgentApproval, readAt: number): string;
/** The one-line state sentence: `<status> · <activity clause>[ · <approval clause>]`. */
export declare function agentStateSummary(s: AgentState): string;