@toren-run/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +202 -0
  2. package/dist/apiKeys.d.ts +28 -0
  3. package/dist/apiKeys.js +40 -0
  4. package/dist/approvals.d.ts +30 -0
  5. package/dist/approvals.js +54 -0
  6. package/dist/blobs.d.ts +12 -0
  7. package/dist/blobs.js +17 -0
  8. package/dist/builtins.d.ts +10 -0
  9. package/dist/builtins.js +244 -0
  10. package/dist/conversations.d.ts +38 -0
  11. package/dist/conversations.js +87 -0
  12. package/dist/db.d.ts +3 -0
  13. package/dist/db.js +20 -0
  14. package/dist/digest.d.ts +1 -0
  15. package/dist/digest.js +13 -0
  16. package/dist/events.d.ts +20 -0
  17. package/dist/events.js +4 -0
  18. package/dist/files.d.ts +28 -0
  19. package/dist/files.js +27 -0
  20. package/dist/fold.d.ts +3 -0
  21. package/dist/fold.js +6 -0
  22. package/dist/guardians.d.ts +9 -0
  23. package/dist/guardians.js +14 -0
  24. package/dist/index.d.ts +27 -0
  25. package/dist/index.js +27 -0
  26. package/dist/leases.d.ts +16 -0
  27. package/dist/leases.js +30 -0
  28. package/dist/loop.d.ts +69 -0
  29. package/dist/loop.js +413 -0
  30. package/dist/migrate.d.ts +4 -0
  31. package/dist/migrate.js +167 -0
  32. package/dist/model.d.ts +42 -0
  33. package/dist/model.js +1 -0
  34. package/dist/orchestrator.d.ts +35 -0
  35. package/dist/orchestrator.js +146 -0
  36. package/dist/providers/echo.d.ts +9 -0
  37. package/dist/providers/echo.js +17 -0
  38. package/dist/providers/mock.d.ts +12 -0
  39. package/dist/providers/mock.js +15 -0
  40. package/dist/queue.d.ts +68 -0
  41. package/dist/queue.js +54 -0
  42. package/dist/schedules.d.ts +61 -0
  43. package/dist/schedules.js +128 -0
  44. package/dist/spawn.d.ts +14 -0
  45. package/dist/spawn.js +113 -0
  46. package/dist/store.d.ts +50 -0
  47. package/dist/store.js +65 -0
  48. package/dist/tools.d.ts +98 -0
  49. package/dist/tools.js +14 -0
  50. package/dist/tracing.d.ts +6 -0
  51. package/dist/tracing.js +21 -0
  52. package/dist/worker.d.ts +34 -0
  53. package/dist/worker.js +176 -0
  54. package/dist/workflow.d.ts +91 -0
  55. package/dist/workflow.js +210 -0
  56. package/package.json +53 -0
@@ -0,0 +1,50 @@
1
+ import type pg from "pg";
2
+ import type { NewEvent, RecordedEvent, StreamId } from "./events.js";
3
+ export type AppendResult = {
4
+ ok: true;
5
+ lastSeq: number;
6
+ } | {
7
+ ok: false;
8
+ conflict: true;
9
+ actualSeq: number;
10
+ };
11
+ export interface CreateRun {
12
+ runId: string;
13
+ agent: string;
14
+ input?: unknown;
15
+ codeHash?: string;
16
+ mode?: "task" | "session";
17
+ process?: string;
18
+ }
19
+ export declare class PgStateStore {
20
+ private pool;
21
+ private schema;
22
+ constructor(pool: pg.Pool, schema: string);
23
+ createRun(req: CreateRun): Promise<void>;
24
+ getRun(runId: string): Promise<{
25
+ runId: string;
26
+ agent: string;
27
+ status: string;
28
+ mode: string;
29
+ process: string;
30
+ input: unknown;
31
+ output: unknown;
32
+ error: unknown;
33
+ } | null>;
34
+ updateRun(runId: string, patch: {
35
+ status?: string;
36
+ output?: unknown;
37
+ error?: unknown;
38
+ }): Promise<void>;
39
+ listRuns(limit?: number): Promise<{
40
+ runId: string;
41
+ agent: string;
42
+ status: string;
43
+ mode: string;
44
+ process: string;
45
+ createdAt: Date;
46
+ }[]>;
47
+ listNonTerminalRuns(): Promise<string[]>;
48
+ append(runId: string, streamId: StreamId, expectedSeq: number, events: NewEvent[]): Promise<AppendResult>;
49
+ read(runId: string, streamId: StreamId, fromSeq?: number): Promise<RecordedEvent[]>;
50
+ }
package/dist/store.js ADDED
@@ -0,0 +1,65 @@
1
+ import { tx } from "./db.js";
2
+ export class PgStateStore {
3
+ pool;
4
+ schema;
5
+ constructor(pool, schema) {
6
+ this.pool = pool;
7
+ this.schema = schema;
8
+ }
9
+ async createRun(req) {
10
+ await this.pool.query(`INSERT INTO ${this.schema}.runs (run_id, agent, input, code_hash, mode, process) VALUES ($1,$2,$3,$4,$5,$6)`, [req.runId, req.agent, JSON.stringify(req.input ?? null), req.codeHash ?? null, req.mode ?? "task", req.process ?? "main"]);
11
+ }
12
+ async getRun(runId) {
13
+ const r = await this.pool.query(`SELECT run_id, agent, status, mode, process, input, output, error FROM ${this.schema}.runs WHERE run_id = $1`, [runId]);
14
+ const row = r.rows[0];
15
+ return row ? { runId: row.run_id, agent: row.agent, status: row.status, mode: row.mode, process: row.process, input: row.input, output: row.output, error: row.error } : null;
16
+ }
17
+ async updateRun(runId, patch) {
18
+ await this.pool.query(`UPDATE ${this.schema}.runs
19
+ SET status = COALESCE($2, status),
20
+ output = COALESCE($3, output),
21
+ error = COALESCE($4, error),
22
+ updated_at = now()
23
+ WHERE run_id = $1`, [
24
+ runId,
25
+ patch.status ?? null,
26
+ patch.output !== undefined ? JSON.stringify(patch.output) : null,
27
+ patch.error !== undefined ? JSON.stringify(patch.error) : null,
28
+ ]);
29
+ }
30
+ async listRuns(limit = 50) {
31
+ const r = await this.pool.query(`SELECT run_id, agent, status, mode, process, created_at FROM ${this.schema}.runs ORDER BY created_at DESC LIMIT $1`, [limit]);
32
+ return r.rows.map((row) => ({ runId: row.run_id, agent: row.agent, status: row.status, mode: row.mode, process: row.process, createdAt: row.created_at }));
33
+ }
34
+ async listNonTerminalRuns() {
35
+ const r = await this.pool.query(`SELECT run_id FROM ${this.schema}.runs WHERE status NOT IN ('completed','failed','cancelled') ORDER BY created_at`);
36
+ return r.rows.map((row) => row.run_id);
37
+ }
38
+ async append(runId, streamId, expectedSeq, events) {
39
+ if (events.length === 0)
40
+ throw new Error("append requires at least one event");
41
+ return tx(this.pool, async (c) => {
42
+ await c.query(`INSERT INTO ${this.schema}.streams (run_id, stream_id) VALUES ($1,$2) ON CONFLICT DO NOTHING`, [runId, streamId]);
43
+ const cas = await c.query(`UPDATE ${this.schema}.streams SET head_seq = head_seq + $3
44
+ WHERE run_id = $1 AND stream_id = $2 AND head_seq = $4 RETURNING head_seq`, [runId, streamId, events.length, expectedSeq]);
45
+ if (cas.rowCount === 0) {
46
+ const cur = await c.query(`SELECT head_seq FROM ${this.schema}.streams WHERE run_id = $1 AND stream_id = $2`, [runId, streamId]);
47
+ // COMMIT of a no-op tx is harmless; nothing was written.
48
+ return { ok: false, conflict: true, actualSeq: Number(cur.rows[0].head_seq) };
49
+ }
50
+ let seq = expectedSeq;
51
+ for (const e of events) {
52
+ seq += 1;
53
+ await c.query(`INSERT INTO ${this.schema}.events (run_id, stream_id, seq, type, payload) VALUES ($1,$2,$3,$4,$5)`, [runId, streamId, seq, e.type, JSON.stringify(e.payload)]);
54
+ }
55
+ return { ok: true, lastSeq: seq };
56
+ });
57
+ }
58
+ async read(runId, streamId, fromSeq = 0) {
59
+ const r = await this.pool.query(`SELECT seq, type, payload, recorded_at FROM ${this.schema}.events
60
+ WHERE run_id = $1 AND stream_id = $2 AND seq > $3 ORDER BY seq`, [runId, streamId, fromSeq]);
61
+ return r.rows.map((row) => ({
62
+ seq: Number(row.seq), type: row.type, payload: row.payload, recordedAt: row.recorded_at,
63
+ }));
64
+ }
65
+ }
@@ -0,0 +1,98 @@
1
+ import type { z } from "zod";
2
+ import type { ToolSpec } from "./model.js";
3
+ export type ToolEffects = "external" | "sandbox" | "none";
4
+ /**
5
+ * Background processes: spawn one of the agent's named processes as a durable
6
+ * run and inspect it. Wired by the runtime (like the sandbox); a watcher
7
+ * recorded at spawn time wakes the parent session when the child settles.
8
+ */
9
+ export interface ProcessesCtx {
10
+ /** Process names this agent serves (the workflows map's keys). */
11
+ names: string[];
12
+ defaultProcess?: string;
13
+ start(req: {
14
+ process: string;
15
+ input: string;
16
+ parentRunId: string;
17
+ parentTaskId: string;
18
+ toolUseId: string;
19
+ }): Promise<{
20
+ runId: string;
21
+ started: boolean;
22
+ }>;
23
+ status(runId: string): Promise<null | {
24
+ runId: string;
25
+ process: string;
26
+ status: string;
27
+ output?: string;
28
+ error?: string;
29
+ waves: {
30
+ name: string;
31
+ tasks: number;
32
+ settled: number;
33
+ done: boolean;
34
+ }[];
35
+ }>;
36
+ }
37
+ export interface ToolCtx {
38
+ runId: string;
39
+ taskId: string;
40
+ /** The model's tool-use block id — stable across replays; key derived side effects on it. */
41
+ toolUseId: string;
42
+ /** Env values declared in agent.yaml `env:` — never raw process.env. */
43
+ env: Record<string, string>;
44
+ /** Attached-file access for the read_file builtin; wired by the runtime. */
45
+ files?: {
46
+ get(id: string): Promise<{
47
+ id: string;
48
+ name: string;
49
+ pages: string[];
50
+ } | null>;
51
+ };
52
+ /** Durable per-run workspace execution for the bash builtin; wired by the runtime. */
53
+ sandbox?: SandboxExec;
54
+ /** Background named-process runs for the run_process/check_run builtins; wired by the runtime. */
55
+ processes?: ProcessesCtx;
56
+ }
57
+ /** One run's sandbox: commands and file operations against a persistent workspace. */
58
+ export interface SandboxExec {
59
+ exec(command: string, opts?: {
60
+ timeoutMs?: number;
61
+ }): Promise<{
62
+ stdout: string;
63
+ stderr: string;
64
+ exitCode: number;
65
+ }>;
66
+ readFile(path: string): Promise<string>;
67
+ writeFile(path: string, content: string): Promise<void>;
68
+ /** Cheap park: preserve the workspace, stop paying for compute. Reconnected on next use. */
69
+ pause?(): Promise<void>;
70
+ /** Terminal cleanup: destroy the workspace and forget it. */
71
+ dispose?(): Promise<void>;
72
+ }
73
+ /** Constructs per-run sandbox handles; implemented by the CLI runtime (docker locally). */
74
+ export interface SandboxProvider {
75
+ forRun(runId: string): SandboxExec;
76
+ }
77
+ export interface ToolDef<S extends z.ZodTypeAny = z.ZodTypeAny> {
78
+ name: string;
79
+ description: string;
80
+ input: S;
81
+ effects: ToolEffects;
82
+ idempotency: "keyed" | "none";
83
+ approval: "never" | "always" | ((args: z.infer<S>) => boolean);
84
+ handler: (args: z.infer<S>, ctx: ToolCtx) => Promise<string>;
85
+ }
86
+ /** Type-erased tool shape — what registries, the loop, and AgentSpec consume. */
87
+ export interface ToolDefAny {
88
+ name: string;
89
+ description: string;
90
+ input: z.ZodTypeAny;
91
+ effects: ToolEffects;
92
+ idempotency: "keyed" | "none";
93
+ approval: "never" | "always" | ((args: any) => boolean);
94
+ handler: (args: any, ctx: ToolCtx) => Promise<string>;
95
+ }
96
+ export declare function defineTool<S extends z.ZodTypeAny>(def: ToolDef<S>): ToolDefAny;
97
+ export declare function toolSpecs(tools: ToolDefAny[]): ToolSpec[];
98
+ export declare function needsApproval(t: ToolDefAny, args: unknown): boolean;
package/dist/tools.js ADDED
@@ -0,0 +1,14 @@
1
+ import { zodToJsonSchema } from "zod-to-json-schema";
2
+ export function defineTool(def) {
3
+ return def;
4
+ }
5
+ export function toolSpecs(tools) {
6
+ return tools.map((t) => ({
7
+ name: t.name,
8
+ description: t.description,
9
+ inputSchema: zodToJsonSchema(t.input, { target: "jsonSchema7" }),
10
+ }));
11
+ }
12
+ export function needsApproval(t, args) {
13
+ return t.approval === "always" || (typeof t.approval === "function" && t.approval(args));
14
+ }
@@ -0,0 +1,6 @@
1
+ import { type Span } from "@opentelemetry/api";
2
+ /**
3
+ * Every model call, tool invocation, task, and tick emits a
4
+ * standard span. No-op unless the host registers an OTel SDK/exporter.
5
+ */
6
+ export declare function withSpan<T>(name: string, attributes: Record<string, string | number>, fn: (span: Span) => Promise<T>): Promise<T>;
@@ -0,0 +1,21 @@
1
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
2
+ // Resolved per call: hosts may register their OTel SDK after toren is imported.
3
+ const tracer = () => trace.getTracer("toren");
4
+ /**
5
+ * Every model call, tool invocation, task, and tick emits a
6
+ * standard span. No-op unless the host registers an OTel SDK/exporter.
7
+ */
8
+ export async function withSpan(name, attributes, fn) {
9
+ return tracer().startActiveSpan(name, { attributes }, async (span) => {
10
+ try {
11
+ return await fn(span);
12
+ }
13
+ catch (e) {
14
+ span.setStatus({ code: SpanStatusCode.ERROR, message: e instanceof Error ? e.message : String(e) });
15
+ throw e;
16
+ }
17
+ finally {
18
+ span.end();
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,34 @@
1
+ import { type TickDeps } from "./orchestrator.js";
2
+ export interface WorkerOpts {
3
+ concurrency?: number;
4
+ visibilitySeconds?: number;
5
+ pollMs?: number;
6
+ }
7
+ /**
8
+ * Local worker runtime: in-process pollers over the orchestrator and task
9
+ * queues (local binding). One instance serves one agent or a whole
10
+ * fleet — construct with a single TickDeps or a Record keyed by agent name;
11
+ * messages carry the agent and are routed to that agent's deps (store,
12
+ * leases, and specs are per-agent; the queue tables are shared).
13
+ */
14
+ export declare class LocalWorkerRuntime {
15
+ private stopped;
16
+ private loops;
17
+ private inFlight;
18
+ private readonly opts;
19
+ private readonly byAgent;
20
+ /** Single-agent construction routes every message here, agent label or not. */
21
+ private readonly sole;
22
+ /** Any entry — used for the shared queue (polling, acking, depth). */
23
+ private readonly shared;
24
+ constructor(deps: TickDeps | Record<string, TickDeps>, opts?: WorkerOpts);
25
+ private depsFor;
26
+ /** Fleet workers claim only their own agents' messages; sole workers claim everything (legacy). */
27
+ private get agentScope();
28
+ start(): void;
29
+ stop(): Promise<void>;
30
+ /** Test helper: run until both queues stay empty and nothing is in flight. */
31
+ drain(timeoutMs?: number): Promise<void>;
32
+ private pollLoop;
33
+ private handle;
34
+ }
package/dist/worker.js ADDED
@@ -0,0 +1,176 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { InvalidationStormError, runTaskLoop, TaskLeaseLostError } from "./loop.js";
3
+ import { findTaskSpec, tick } from "./orchestrator.js";
4
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5
+ /**
6
+ * Local worker runtime: in-process pollers over the orchestrator and task
7
+ * queues (local binding). One instance serves one agent or a whole
8
+ * fleet — construct with a single TickDeps or a Record keyed by agent name;
9
+ * messages carry the agent and are routed to that agent's deps (store,
10
+ * leases, and specs are per-agent; the queue tables are shared).
11
+ */
12
+ export class LocalWorkerRuntime {
13
+ stopped = false;
14
+ loops = [];
15
+ inFlight = 0;
16
+ opts;
17
+ byAgent;
18
+ /** Single-agent construction routes every message here, agent label or not. */
19
+ sole;
20
+ /** Any entry — used for the shared queue (polling, acking, depth). */
21
+ shared;
22
+ constructor(deps, opts = {}) {
23
+ this.opts = { concurrency: opts.concurrency ?? 2, visibilitySeconds: opts.visibilitySeconds ?? 60, pollMs: opts.pollMs ?? 20 };
24
+ if ("store" in deps) {
25
+ // Legacy single-deps form: unscoped — claims and serves everything.
26
+ this.sole = deps;
27
+ this.byAgent = new Map();
28
+ this.shared = this.sole;
29
+ }
30
+ else {
31
+ // Fleet form — ALWAYS scoped to its agents, even a fleet of one, so
32
+ // separate fleets can share a queue table without stealing hints.
33
+ const entries = Object.entries(deps);
34
+ if (entries.length === 0)
35
+ throw new Error("worker needs at least one agent's deps");
36
+ this.byAgent = new Map(entries);
37
+ this.sole = null;
38
+ this.shared = entries[0][1];
39
+ }
40
+ }
41
+ depsFor(agent) {
42
+ if (this.sole)
43
+ return this.sole;
44
+ if (agent && this.byAgent.has(agent))
45
+ return this.byAgent.get(agent);
46
+ // Unlabeled (pre-fleet) messages: a fleet of one can safely own them.
47
+ if (!agent && this.byAgent.size === 1)
48
+ return this.shared;
49
+ return null; // unknown agent, or unlabeled on a multi-crew fleet — stale hint
50
+ }
51
+ /** Fleet workers claim only their own agents' messages; sole workers claim everything (legacy). */
52
+ get agentScope() {
53
+ return this.sole ? undefined : [...this.byAgent.keys()];
54
+ }
55
+ start() {
56
+ for (let i = 0; i < this.opts.concurrency; i++)
57
+ this.loops.push(this.pollLoop(`worker-${i}-${randomUUID()}`));
58
+ }
59
+ async stop() {
60
+ this.stopped = true;
61
+ await Promise.all(this.loops);
62
+ this.loops = [];
63
+ }
64
+ /** Test helper: run until both queues stay empty and nothing is in flight. */
65
+ async drain(timeoutMs = 10_000) {
66
+ const deadline = Date.now() + timeoutMs;
67
+ let quiet = 0;
68
+ while (Date.now() < deadline) {
69
+ const busy = this.inFlight > 0 || (await this.shared.queue.depth({ agents: this.agentScope })) > 0;
70
+ quiet = busy ? 0 : quiet + 1;
71
+ if (quiet >= 5)
72
+ return;
73
+ await sleep(this.opts.pollMs);
74
+ }
75
+ throw new Error(`drain timed out after ${timeoutMs}ms`);
76
+ }
77
+ async pollLoop(owner) {
78
+ let errorStreak = 0;
79
+ while (!this.stopped) {
80
+ let d;
81
+ try {
82
+ d =
83
+ (await this.shared.queue.receive("orchestrator", { max: 1, visibilitySeconds: this.opts.visibilitySeconds, agents: this.agentScope }))[0] ??
84
+ (await this.shared.queue.receive("tasks-short", { max: 1, visibilitySeconds: this.opts.visibilitySeconds, agents: this.agentScope }))[0];
85
+ errorStreak = 0;
86
+ }
87
+ catch {
88
+ // Transient infrastructure error (DB restart, network blip). Back off
89
+ // and keep polling — an uncaught throw here would kill this loop for
90
+ // good and leave a worker that looks healthy but processes nothing.
91
+ errorStreak = Math.min(errorStreak + 1, 6);
92
+ await sleep(Math.min(this.opts.pollMs * 2 ** errorStreak, 30_000));
93
+ continue;
94
+ }
95
+ if (!d) {
96
+ await sleep(this.opts.pollMs);
97
+ continue;
98
+ }
99
+ this.inFlight += 1;
100
+ try {
101
+ await this.handle(d, owner);
102
+ }
103
+ finally {
104
+ this.inFlight -= 1;
105
+ }
106
+ }
107
+ }
108
+ async handle(d, owner) {
109
+ const msg = d.message;
110
+ const deps = this.depsFor(msg.agent);
111
+ if (!deps) {
112
+ // A hint for an agent this worker doesn't serve. Messages are hints,
113
+ // never truth — the owning worker's guardians re-derive the work.
114
+ await this.shared.queue.ack(d);
115
+ return;
116
+ }
117
+ try {
118
+ if (msg.kind === "tick") {
119
+ await tick(deps, msg.runId);
120
+ await this.shared.queue.ack(d);
121
+ return;
122
+ }
123
+ // task message
124
+ const taskId = msg.taskId;
125
+ const streamId = `task:${taskId}`;
126
+ const lease = await deps.leases.acquire(msg.runId, streamId, owner, this.opts.visibilitySeconds);
127
+ if (!lease) {
128
+ await this.shared.queue.ack(d); // someone else owns it; message was a hint
129
+ return;
130
+ }
131
+ try {
132
+ const spec = await findTaskSpec(deps.store, msg.runId, taskId);
133
+ if (!spec) {
134
+ await this.shared.queue.ack(d); // stale hint for an invalidated plan
135
+ return;
136
+ }
137
+ const agent = deps.agents[spec.agentRef];
138
+ if (!agent)
139
+ throw new Error(`no agent registered for ref ${spec.agentRef}`);
140
+ const run = await deps.store.getRun(msg.runId);
141
+ await runTaskLoop({
142
+ store: deps.store, provider: deps.provider,
143
+ runId: msg.runId, taskId, agent, input: spec.input, files: deps.files,
144
+ sandbox: deps.sandbox?.forRun(msg.runId),
145
+ processes: deps.processes,
146
+ sessionMode: run?.mode === "session",
147
+ });
148
+ await this.shared.queue.ack(d);
149
+ // Nudge the orchestrator to absorb the terminal/parked state.
150
+ await this.shared.queue.send("orchestrator", { kind: "tick", runId: msg.runId, agent: msg.agent, dedupeKey: `settle-${msg.runId}-${taskId}` });
151
+ }
152
+ finally {
153
+ await deps.leases.release(lease);
154
+ }
155
+ }
156
+ catch (e) {
157
+ try {
158
+ if (e instanceof TaskLeaseLostError) {
159
+ await this.shared.queue.ack(d);
160
+ return;
161
+ }
162
+ if (e instanceof InvalidationStormError) {
163
+ // Mixed worker versions are fighting over this stream. Back off well past
164
+ // a deploy's drain window so the surviving version picks it up.
165
+ await this.shared.queue.nack(d, { delaySeconds: 20 });
166
+ return;
167
+ }
168
+ await this.shared.queue.nack(d, { delaySeconds: 0.2 });
169
+ }
170
+ catch {
171
+ // Queue unreachable too: do nothing. The visibility timeout redelivers
172
+ // the message; hints are never truth, so losing an ack costs a retry.
173
+ }
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,91 @@
1
+ import { type RecordedEvent } from "./events.js";
2
+ import type { PgStateStore } from "./store.js";
3
+ import type { QueueMessage, QueueName } from "./queue.js";
4
+ export interface TaskSpec {
5
+ agentRef: string;
6
+ input: string;
7
+ }
8
+ export interface TaskOutcome {
9
+ taskId: string;
10
+ status: "completed" | "failed";
11
+ output?: string;
12
+ error?: string;
13
+ }
14
+ export interface WaveResult {
15
+ name: string;
16
+ results: TaskOutcome[];
17
+ }
18
+ export interface WaveOpts {
19
+ onTaskFailure?: "fail" | "collect";
20
+ }
21
+ export interface WorkflowCtx {
22
+ input: string;
23
+ task(agentRef: string, input: string): TaskSpec;
24
+ wave(name: string, tasks: TaskSpec[], opts?: WaveOpts): Promise<WaveResult>;
25
+ now(): Promise<number>;
26
+ random(): Promise<number>;
27
+ sleep(ms: number): Promise<void>;
28
+ }
29
+ export type WorkflowFn = (ctx: WorkflowCtx) => Promise<string>;
30
+ export declare class WorkflowBlocked extends Error {
31
+ reason: string;
32
+ constructor(reason: string);
33
+ }
34
+ export declare class WaveFailedError extends Error {
35
+ }
36
+ export declare class RunLeaseLostError extends Error {
37
+ }
38
+ export interface WaveState {
39
+ seq: number;
40
+ waveId: string;
41
+ index: number;
42
+ name: string;
43
+ tasks: {
44
+ taskId: string;
45
+ agentRef: string;
46
+ input: string;
47
+ }[];
48
+ planDigest: string;
49
+ dispatched: boolean;
50
+ settledTasks: Map<string, TaskOutcome>;
51
+ settled: boolean;
52
+ }
53
+ export interface TimerState {
54
+ seq: number;
55
+ timerId: string;
56
+ fireAt: number;
57
+ fired: boolean;
58
+ }
59
+ export interface RunStreamState {
60
+ sideEffects: RecordedEvent[];
61
+ waves: WaveState[];
62
+ timers: TimerState[];
63
+ started: boolean;
64
+ terminal?: {
65
+ status: "completed" | "failed" | "cancelled";
66
+ output?: string;
67
+ error?: string;
68
+ };
69
+ }
70
+ export declare function foldRunStream(eff: RecordedEvent[]): RunStreamState;
71
+ export interface PendingDispatch {
72
+ queue: QueueName;
73
+ msg: QueueMessage;
74
+ delaySeconds?: number;
75
+ }
76
+ export interface RunSession {
77
+ store: PgStateStore;
78
+ runId: string;
79
+ input: string;
80
+ head: number;
81
+ folded: RunStreamState;
82
+ seCursor: number;
83
+ waveCursor: number;
84
+ timerCursor: number;
85
+ invalidated: boolean;
86
+ pendingDispatch: PendingDispatch[];
87
+ /** Tasks parked on an unresolved approval — never re-nudged by ticks. */
88
+ parkedTasks: Set<string>;
89
+ }
90
+ export declare function makeSession(store: PgStateStore, runId: string, input: string, raw: RecordedEvent[], eff: RecordedEvent[], parkedTasks?: Set<string>): RunSession;
91
+ export declare function createWorkflowCtx(s: RunSession): WorkflowCtx;