/** * Engine-facing types: the run event log and the `HostPort` seam. * * The engine (run-workflow.ts) imports only from here and from the pure * flow types — never from PI, `node:fs`, or any network lib. */ import type { TSchema } from "typebox"; import type { Questionnaire } from "../flow/questionnaire.ts"; /** Why a step is being retried (spec §9.2/§9.3). Input-schema violations are never retried. */ export type RetryReason = "thrown-error" | "invalid-output" | "budget-exceeded" | "agent-error"; /** * Append-only lifecycle events (spec §8.1, §12.1), each tagged with the run id and an ISO timestamp. * * Every event that names a step or node carries its `path` — the full DYNAMIC node path (spec §8.5), * indices included (`until-valid#3/design`, `batch#7/review`), formatted by `formatPath` * (node-path.ts). Under concurrency the log's order is not deterministic (spec §4.2/§8.1), so * consumers reconstruct per-step history by path, not by adjacency. This is a P2 log-format change: * a log written before this phase has no `path` field and is not readable by this build (no * migration, by design) — `parsePath` fails loudly rather than mis-parsing it. */ export type RunEvent = { type: "run-meta"; runId: string; workflowFilePath: string; at: string; } | { type: "run-started"; runId: string; workflowName: string; input: unknown; at: string; } | { type: "run-resumed"; runId: string; fromPath?: string; at: string; } | { type: "step-started"; runId: string; path: string; input: unknown; at: string; } | { type: "step-retry"; runId: string; path: string; attempt: number; reason: RetryReason; error: string; at: string; } | { type: "agent-steer"; runId: string; path: string; attempt: number; violation: string; at: string; } | { type: "agent-error"; runId: string; path: string; attempt: number; kind: AgentTurnErrorKind; message: string; terminal: boolean; at: string; } | { type: "agent-usage"; runId: string; path: string; totalTokens: number; at: string; } | { type: "step-completed"; runId: string; path: string; output: unknown; at: string; } | { type: "step-failed"; runId: string; path: string; error: string; at: string; } | { type: "node-started"; runId: string; path: string; nodeKind: "branch" | "loop" | "foreach" | "parallel" | "workflow"; at: string; } | { type: "node-completed"; runId: string; path: string; output: unknown; at: string; } | { type: "branch-arm"; runId: string; path: string; taken: boolean; at: string; } | { type: "loop-iteration"; runId: string; path: string; iteration: number; at: string; } | { type: "foreach-started"; runId: string; path: string; count: number; at: string; } | { type: "foreach-item-started"; runId: string; path: string; index: number; at: string; } | { type: "foreach-item-completed"; runId: string; path: string; index: number; output: unknown; at: string; } | { type: "step-log"; runId: string; path: string; level: "info" | "warn" | "error"; message: string; data?: Record; at: string; } | { type: "run-completed"; runId: string; output: unknown; at: string; } | { type: "run-crashed"; runId: string; path?: string; error: string; at: string; } | { type: "run-cancelled"; runId: string; path?: string; at: string; } | { type: "questionnaire-asked"; runId: string; path: string; questionnaire: Questionnaire; conversation: readonly ConversationMessage[]; violation?: string; elapsedMs?: number; tokensUsed?: number; at: string; } | { type: "answers-provided"; runId: string; path: string; answers: Record; at: string; } | { type: "step-cancelled"; runId: string; path: string; at: string; }; /** One message of an agent conversation. Opaque to the engine — the host defines the concrete shape. */ export type ConversationMessage = unknown; /** * What an agent step needs from the host to run (spec §2.2). Model is `provider/modelId`; omit for * the session default. `history` seeds a resumed session with the blocked step's prior messages so it * continues the same loop (spec §8.4). */ export interface AgentRequest { readonly model?: string; readonly history?: readonly ConversationMessage[]; /** The step this session belongs to. Lets a host attribute cost/telemetry — and lets a test double script replies per step. */ readonly stepName: string; /** * Which run, and which execution within it, this session belongs to (spec §8.5/§8.9). * * The engine needs none of it — it is passed because a host that PERSISTS a session has to name the * file, and only these four make a name that is both unique and readable: two executions of one step * differ by `attempt`, two items of a `.foreach` differ by `path` (the full dynamic node path, indices * included — `stepName` alone is the same string for all of them), and two runs of one workflow differ * by `runId`. A host that keeps nothing (the test double, an in-memory host) simply ignores them. */ readonly runId: string; readonly workflowName: string; readonly path: string; /** 1-based retry attempt (spec §9.1) this session was opened for; an answer-continuation stays on the attempt it resumes. */ readonly attempt: number; /** * True for a `background` agent step (spec §2.2): the host must run this as an isolated PI subagent * — its own context window and tool loop, no access to the parent session's history — rather than * continuing whatever session ordinary agent steps use. `history` is always undefined alongside this: * a background step can never be Q&A-capable (`.commit()` rejects `background` + `asks`, spec * §10.1), so it never blocks and is never resumed with a seeded conversation. */ readonly background?: boolean; /** * True when this step CAN run concurrently with another agent step — inside a `.parallel` arm, or * inside a `.foreach` whose declared concurrency exceeds 1 — decided **statically** from the workflow * definition and tagged onto the step at `.commit()` (spec §2.2 "Overlap implies isolation", * `flow/isolation.ts`), never from what happens to be in flight. Independent of `background`: a step * can be `isolated` without being `background` * (an ordinary agent step that merely happens to sit in a fan-out), and a host must treat the two as * the same instruction — run this isolated, exactly like `background` — even though they arise for * different reasons. A session hosts one conversation at a time, so a host MUST route an `isolated` * (or `background`) request through its isolated/subagent path (spec §12.2) rather than the shared * in-session one; sharing one session between two turns that can overlap is exactly the cross-talk * spec §2.2 exists to rule out. */ readonly isolated?: boolean; /** * A stable identity for a conversation this step CONTINUES across separate executions (spec §2.2). * * An isolated step is a one-shot subprocess, so by default every execution of it starts cold — which * is right for a verifier (fresh eyes are the point) and wrong for a worker that was interrupted: * a step time-boxed out of one round and re-run in the next re-derives everything it already knew. * When this is set, a host that runs the step out-of-process is asked to persist the conversation * under this key and resume it next time, so the second execution continues the first. * * Set from `AgentStep.resumable`; absent otherwise, which keeps the default behaviour (and the small, * cheap contexts that make a chain of isolated steps far cheaper than one long session). */ readonly resumeKey?: string; /** * The step's declared output contract, when it has one (spec §9.2). * * Passed so a host that runs this request out-of-process can register a `submit_result` tool typed by * it inside that process — the payload then travels as a tool call, which a later message cannot * displace. A host with nowhere to register tools leaves this undefined, and every step under a * contract then fails — there is no text channel behind it. */ readonly outputSchema?: TSchema; /** True when the step may answer with a questionnaire instead of a result (spec §10.1) — enables `submit_questions`. */ readonly asks?: boolean; /** * The attempt's abort signal — the run's cancel signal (spec §8.8) combined with the step's wall-time * budget (spec §9.4). A host that runs this request as an out-of-process subagent MUST honour it, or * the two mechanisms become lies: `/workflow cancel` would report a stopped run while its subagent * kept spending tokens and writing files, and a budget would fail the attempt while orphaning the * process it was meant to bound. Worse, with no budget declared — the default — an unresponsive * subagent would hang the run with nothing able to interrupt it. The in-session path needs nothing * here: the engine already stops at turn boundaries, and that session belongs to PI, not to us. */ readonly signal?: AbortSignal; } /** Token usage reported by a single agent turn (spec §9.3). */ export interface TokenUsage { readonly totalTokens: number; } /** A `submit_*` tool call the agent made, as seen in the turn's transcript. */ export interface SubmittedOutput { /** Which output tool was called — the discriminator (see engine/output-tools.ts). */ readonly tool: string; /** The call's arguments, already an object (`ToolCall.arguments`); no JSON extraction involved. */ readonly arguments: Record; } /** * Why a turn produced nothing, when the reason was the provider rather than the model. * * A refused request still completes a turn: the harness records an assistant message with no content * and ends the loop, so from the outside it is shaped exactly like a model that declined to submit. * Without this distinction the engine reports "the turn ended without calling submit_result" — blaming * a model that was never asked — and spends a repair turn correcting an omission that never happened. * * - `context-window-exceeded` — the request was larger than the model's context. Terminal for a session * that is RESUMED (spec §2.2's `resumable`): every later turn re-sends the same transcript plus more, * so it can only fail again. A non-resumable step gets a new session file per attempt, so a retry * there is a genuinely different request. * - `provider-error` — anything else the provider refused or failed on. Retryable like a thrown error. */ export type AgentTurnErrorKind = "context-window-exceeded" | "provider-error"; /** A turn that ended because the request failed, not because the model chose what to say. */ export interface AgentTurnError { readonly kind: AgentTurnErrorKind; /** The provider's own message, verbatim — the only place the real cause is stated. */ readonly message: string; } /** The result of one agent turn: the final assistant text plus optional token usage for budgeting. */ export interface AgentTurn { readonly text: string; readonly usage?: TokenUsage; /** * The last `submit_*` call of the turn, if any — the ONLY channel a step under a contract reports * through, since a later message cannot displace a tool call. A step with no `outputSchema` has no * contract to submit against and reports its text instead, so this is ignored there. */ readonly submitted?: SubmittedOutput; /** * Set when the turn ended on a failed request. The engine treats it as a failure of the ATTEMPT, not * of the reply: no steering correction is sent, because there is no reply to correct. */ readonly error?: AgentTurnError; } /** * A single agent conversation opened by the host. The engine sends the built prompt and awaits the * agent loop's end, receiving the turn's submission + optional usage (which it validates against the * step's schema and sums against `maxTokens`). All network and PI coupling lives behind this seam. */ export interface AgentSession { /** Send `message`, run the agent loop to `agent_end`, and resolve with the turn's submission + usage. */ sendAndAwaitEnd(message: string): Promise; /** The full conversation after the last turn — captured when a Q&A step blocks, to resume it (spec §8.4). */ getConversation(): readonly ConversationMessage[]; /** Release any listeners/resources for this session. Called by the engine in a `finally`. */ dispose(): void; } /** * The one seam the engine depends on. A host adapter (or a fake, in tests) implements this to * supply the non-deterministic engine inputs (run identity, time, waiting, agent runs) and to * receive the event stream; it decides what to do with events (persist, print, collect in memory). */ export interface HostPort { /** Generate a unique id for a new run. */ generateRunId(): string; /** Current time. Engine calls this once per emitted event/checkpoint. */ now(): Date; /** * Wait `ms` milliseconds — the engine's only source of delay (retry backoff, time-budget timers). * If `signal` aborts first, resolve early (so a completed step can cancel its budget timer). Keeps * the engine pure + fast in tests. */ sleep(ms: number, signal?: AbortSignal): Promise; /** Open an agent conversation (spec §2.2). Hosts without agent support throw here; function steps never call it. */ startAgent(request: AgentRequest): AgentSession; /** Receive a lifecycle event, in emission order. */ emit(event: RunEvent): void | Promise; } /** Per-invocation run options (spec §8.6): an external `AbortSignal` for cooperative cancellation. */ export interface RunOptions { /** When aborted, the engine stops at the next step boundary and marks the run `cancelled`. */ signal?: AbortSignal; } /** * Outcome of a single run/resume call (spec §5.1). Only `completed` is terminal; `crashed`, * `cancelled`, and `blocked` are resumable. `blocked` carries the pending `questionnaire` + `path`. */ export interface RunResult { readonly runId: string; readonly status: "completed" | "crashed" | "cancelled" | "blocked"; readonly output?: unknown; readonly error?: string; /** Present when `blocked`: the questionnaire batch the step asked, and the step that asked it. */ readonly questionnaire?: Questionnaire; /** The step's full node path (spec §8.5) — present when `blocked`, and when `crashed`/`cancelled` named a step. */ readonly path?: string; /** Present when a questionnaire step RE-blocked: why the delivered answers were rejected (spec §2.4). */ readonly violation?: string; } //# sourceMappingURL=types.d.ts.map