@sema-agent/core
Version:
Stateless, task-oriented AI agent core
78 lines (77 loc) • 5.72 kB
TypeScript
import type { WorkflowRunStore, WorkflowRunSummary } from "../core/workflow-run-store.js";
import type { WorkflowEvent, WorkflowItemStatus, WorkflowRun, WorkflowRunStatus } from "./workflow.js";
/** design/97 S1c: mark a run RUNNING (called by `runWorkflow` before it emits `run_start`).
* B-3 (BREAKING): carries the run's SCOPE — {@link subscribeWorkflow} gates on it. */
export declare function markWorkflowActive(runId: string, scope: string): void;
/** T2A-9 (2026-07-05 soak): is this runId currently ACTIVE in-process? Used by `startWorkflow` to reject an
* explicit duplicate `opts.runId` up front — two runs sharing one observation channel let the first finisher
* close the second's event stream (and best-effort-swallow its store writes). Synchronous, race-free with
* markWorkflowActive/closeWorkflowChannel (same tick). */
export declare function isWorkflowRunActive(runId: string): boolean;
/**
* design/97 S1c: fan one {@link WorkflowEvent} to every live subscriber of `runId`. Called by `runWorkflow`
* for EVERY emitted event. BEST-EFFORT — a `push` never throws (the {@link PushQueue} just buffers), and a
* missing channel (no subscriber) is a no-op; nothing here can break the workflow. A subscriber that
* registered AFTER the run started receives only events from that point on (live progress, not a replay).
*/
export declare function publishWorkflowEvent(runId: string, event: WorkflowEvent): void;
/**
* design/97 S1c: close the live channel for `runId` (called by `runWorkflow` at `run_end`). Closes every
* subscriber's queue so their `for await` loops END cleanly, and drops the channel entry (no leak). A
* subscriber's queue is also pruned individually if it stops iterating before the run ends (see
* {@link subscribeWorkflow}). Idempotent — closing a missing/already-closed channel is a no-op.
*/
export declare function closeWorkflowChannel(runId: string): void;
/**
* design/97 S1c: subscribe to a RUNNING workflow's live {@link WorkflowEvent} progress (the `/workflows` live
* view). Returns an `AsyncIterable<WorkflowEvent>` that yields every event emitted AFTER subscription and ends
* when the run finishes (its channel closes at `run_end`). **In-process + opt-in**: subscribing registers a
* queue in the module registry; `runWorkflow` only fans events to a runId that HAS a subscriber, so this is
* pay-for-what-you-use.
*
* Subscribe BEFORE or DURING the run: a queue registered before `run_start` catches the whole stream; one
* registered mid-run catches the rest (no replay of past events — the historical view is {@link getWorkflowRun}).
* If the run already ended (or never ran in this process), the returned iterable simply yields nothing and ends.
*
* The queue is pruned from the registry when the consumer's iteration ENDS (loop completes, `break`, or throw)
* — so a consumer that stops early does not leak a buffer (the `finally` removes it).
*/
export declare function subscribeWorkflow(runId: string, scope: string): AsyncIterable<WorkflowEvent>;
/**
* design/97 S1c: list a scope's workflow runs (newest first) as lightweight {@link WorkflowRunSummary}
* projections — **history included** (completed / failed, not just running), the "看之前的" query. A thin
* pass-through to {@link WorkflowRunStore.listByScope}; `opts.status` filters to one lifecycle state and
* `opts.limit` caps the count (newest N).
*/
export declare function listWorkflowRuns(store: WorkflowRunStore, scope: string, opts?: {
status?: WorkflowRunStatus;
limit?: number;
}): Promise<WorkflowRunSummary[]>;
/**
* design/97 S1c: fetch ONE full {@link WorkflowRun} (phases / agents / stats) by id — the `/workflows` detail
* view. 🔐 SCOPED (CORE-1~9 audit BLOCKER): returns the run ONLY when its `scope` equals the caller's `scope`
* (the tenant) — else `null`, just like {@link listWorkflowRuns}/the journal load. A run id is unguessable, but
* without this check a cross-tenant id leaks the run's metadata / groups / redacted prompts+outputs / stats; the
* store's lower-level `get(id)` carries no scope, so the isolation MUST be enforced here at the observe layer.
*/
export declare function getWorkflowRun(store: WorkflowRunStore, id: string, scope: string): Promise<WorkflowRun | null>;
/** design/99 MF-W display status. */
export type AgentDisplayStatus = "queued" | "running" | "done" | "failed" | "interrupted" | "parked";
/**
* design/99 MF-W (design-review DoR ⑤+⑥): the SHARED, anti-drift projection of a workflow agent's record-level
* status → a CC-style display status, derived PURELY from the persisted record (the agent's `status` + `startedAt`
* + the run's status), so the shell + service render IDENTICALLY (no per-shell drift — the WPe port lives once,
* here, not copied N times). Covers 5 of the 8 contract states from the durable record:
* - `completed` → `done`; `failed` → `failed`.
* - `running` but `startedAt` ABSENT → `queued` (enqueued, still waiting for a concurrency slot — the DoR ⑥ fix:
* a queued agent no longer lies as `running`).
* - `running` + started, while the RUN still runs → `running`; started but the RUN terminated (the agent never
* finished) → `interrupted`.
* The other contract states are intentionally NOT here: `idle` (last-activity age) is a SERVICE live aggregation
* (core's `ToolActivity` has no timestamp); `skipped`/`paused`/`stopped` are not core states (no engine producer —
* see the DoR "don't model an enum arm the engine never emits").
*/
export declare function deriveAgentDisplayStatus(agent: {
status: WorkflowItemStatus;
startedAt?: number;
}, runStatus: WorkflowRunStatus): AgentDisplayStatus;