@boardwalk-labs/runner 0.1.20 → 0.2.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.
- package/dist/daemon/daemon.js +4 -2
- package/dist/runtime/freeze_coordinator.d.ts +5 -0
- package/dist/runtime/freeze_coordinator.js +3 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +20 -40
- package/dist/runtime/program_runner.d.ts +4 -15
- package/dist/runtime/program_runner.js +6 -21
- package/dist/runtime/program_worker.d.ts +0 -18
- package/dist/runtime/program_worker.js +1 -20
- package/dist/runtime/runner_control_client.d.ts +2 -22
- package/dist/runtime/runner_control_client.js +2 -49
- package/dist/runtime/runtime_flusher.d.ts +11 -0
- package/dist/runtime/runtime_flusher.js +20 -0
- package/dist/runtime/suspension.d.ts +12 -107
- package/dist/runtime/suspension.js +18 -111
- package/dist/runtime/wire/human_input.d.ts +1 -1
- package/dist/runtime/wire/human_input.js +2 -2
- package/dist/runtime/workflow_host.d.ts +42 -60
- package/dist/runtime/workflow_host.js +89 -199
- package/package.json +3 -3
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { LeafCheckpoint, LeafResume } from "@boardwalk-labs/engine/core";
|
|
3
|
-
import { AppError } from "./support/index.js";
|
|
1
|
+
import type { LeafCheckpoint } from "@boardwalk-labs/engine/core";
|
|
4
2
|
/**
|
|
5
|
-
* Sleeps at/above this
|
|
6
|
-
*
|
|
7
|
-
* more than it saves.
|
|
8
|
-
* in the same place.
|
|
3
|
+
* Sleeps at/above this boundary SUSPEND on the snapshot substrate (freeze the VM; the wake fires
|
|
4
|
+
* when the sleep is due); shorter ones HOLD the process in-memory, where a snapshot round-trip
|
|
5
|
+
* costs more than it saves. Without a freeze substrate every sleep holds, whatever its length.
|
|
9
6
|
*/
|
|
10
7
|
export declare const SUSPEND_THRESHOLD_MS = 30000;
|
|
11
|
-
/** The durable-seam kinds the journal memoizes (mirrors `run_journal.kind` + the engine's IPC). */
|
|
12
|
-
export type JournalKind = "agent" | "step" | "human_input" | "sleep" | "workflow_call";
|
|
13
8
|
/** Why a seam suspended the run. */
|
|
14
9
|
export type SuspendReason = "human_input" | "sleep" | "workflow_call";
|
|
15
10
|
/** A human-in-the-loop gate carried out of a suspending seam (program-level or the in-leaf tool). */
|
|
16
11
|
export interface HumanInputGate {
|
|
17
|
-
/** The stable key the responder answers by (an author/
|
|
12
|
+
/** The stable key the responder answers by (an author/derived key, or the model's tool-call id). */
|
|
18
13
|
key: string;
|
|
19
14
|
prompt: string;
|
|
20
15
|
/** The response form ({@link import("@boardwalk-labs/workflow").HumanInputSpec}); validated on submit. */
|
|
@@ -29,13 +24,14 @@ export interface HumanInputGate {
|
|
|
29
24
|
/** Everything the broker needs to persist a suspension + the wake condition. */
|
|
30
25
|
export interface SuspendSignal {
|
|
31
26
|
reason: SuspendReason;
|
|
32
|
-
/** The
|
|
27
|
+
/** The suspension's within-run key (a monotonic per-run counter): it keys the HITL gate rows the
|
|
28
|
+
* wake joins answers from. Not a journal seq — there is no journal. */
|
|
33
29
|
seq: number;
|
|
34
|
-
/** The seam's determinism fingerprint (recorded on the pending journal entry). */
|
|
35
|
-
fingerprint: string;
|
|
36
30
|
/** Present for `reason: "human_input"` — the gate to register a request row for. */
|
|
37
31
|
humanInput?: HumanInputGate;
|
|
38
|
-
/** A tool-level gate's leaf transcript checkpoint
|
|
32
|
+
/** A tool-level gate's leaf transcript checkpoint. On the snapshot substrate the transcript
|
|
33
|
+
* rides in the frozen heap — this field is informational for the control plane, not a resume
|
|
34
|
+
* source. */
|
|
39
35
|
leafCheckpoint?: LeafCheckpoint;
|
|
40
36
|
/** Relative wait (ms) for `reason: "sleep"`; the broker computes the absolute wake time. */
|
|
41
37
|
durationMs?: number;
|
|
@@ -43,99 +39,8 @@ export interface SuspendSignal {
|
|
|
43
39
|
* woken when this child finalizes (the sweep wakes a parent whose child is terminal). */
|
|
44
40
|
childRunId?: string;
|
|
45
41
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
* supervisor kills out-of-band), the backend worker runs the program IN-PROCESS, so a suspend is
|
|
49
|
-
* surfaced by the host calling `onSuspend(signal)` and returning a NEVER-resolving promise; the
|
|
50
|
-
* worker races that against the program body and tears down. This class exists so a seam that has no
|
|
51
|
-
* `onSuspend` wired (the local/test path) can still raise a typed, catchable signal.
|
|
52
|
-
*/
|
|
53
|
-
export declare class SuspendError extends Error {
|
|
54
|
-
readonly signal: SuspendSignal;
|
|
55
|
-
constructor(signal: SuspendSignal);
|
|
56
|
-
}
|
|
57
|
-
/** Journal-entry states (mirrors `run_journal.state` + the engine's IPC). */
|
|
58
|
-
export declare const JOURNAL_STATES: readonly ["pending", "suspended", "resolved"];
|
|
59
|
-
export type JournalEntryState = (typeof JOURNAL_STATES)[number];
|
|
60
|
-
/** A memoized journal entry, as the host reads it back on replay (mirrors the engine's IPC shape). */
|
|
61
|
-
export interface JournalLookup {
|
|
62
|
-
seq: number;
|
|
63
|
-
kind: JournalKind;
|
|
64
|
-
fingerprint: string;
|
|
65
|
-
/** `resolved` ⇒ `result` is the memoized value; `suspended` ⇒ a parked agent leaf (result is the
|
|
66
|
-
* {@link LeafResume} the host re-enters with); `pending` ⇒ awaiting an external event (re-suspend). */
|
|
67
|
-
state: JournalEntryState;
|
|
68
|
-
result: unknown;
|
|
69
|
-
}
|
|
70
|
-
/** Validate the broker's journal_get response (the worker's run token doesn't exempt the channel from
|
|
71
|
-
* validation). The result is genuinely heterogeneous JSON (an agent return / a LeafResume / a
|
|
72
|
-
* HumanInputResult), parsed per-kind downstream — so it stays `unknown` here, the validation seam. */
|
|
73
|
-
export declare const journalLookupSchema: z.ZodObject<{
|
|
74
|
-
seq: z.ZodNumber;
|
|
75
|
-
kind: z.ZodEnum<{
|
|
76
|
-
sleep: "sleep";
|
|
77
|
-
agent: "agent";
|
|
78
|
-
step: "step";
|
|
79
|
-
human_input: "human_input";
|
|
80
|
-
workflow_call: "workflow_call";
|
|
81
|
-
}>;
|
|
82
|
-
fingerprint: z.ZodString;
|
|
83
|
-
state: z.ZodEnum<{
|
|
84
|
-
pending: "pending";
|
|
85
|
-
suspended: "suspended";
|
|
86
|
-
resolved: "resolved";
|
|
87
|
-
}>;
|
|
88
|
-
result: z.ZodUnknown;
|
|
89
|
-
}, z.core.$strip>;
|
|
90
|
-
/** The journal the host reads (replay lookup) + writes (resolved seam results). Backed by the broker
|
|
91
|
-
* over the run token on hosted runs; absent on the local/test path (no durable suspension). */
|
|
92
|
-
export interface JournalSeam {
|
|
93
|
-
/** The memoized entry for a seam, or null on a replay miss. */
|
|
94
|
-
get(seq: number): Promise<JournalLookup | null>;
|
|
95
|
-
/** Record a RESOLVED seam result (idempotent on the run + seq; a resolved entry is immutable). */
|
|
96
|
-
put(entry: {
|
|
97
|
-
seq: number;
|
|
98
|
-
kind: JournalKind;
|
|
99
|
-
fingerprint: string;
|
|
100
|
-
label: string;
|
|
101
|
-
result: unknown;
|
|
102
|
-
}): Promise<void>;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* The synchronous, monotonic durable-seam counter. Incremented at each journaled seam's ENTRY:
|
|
106
|
-
* because a program's synchronous call order is deterministic (even under `Promise.all`, whose
|
|
107
|
-
* `.map(...)` runs left-to-right synchronously), the same logical call gets the same `seq` on every
|
|
108
|
-
* execution — the journal key that lets a resumed run return a memoized result.
|
|
109
|
-
*
|
|
110
|
-
* It also drives SILENT REPLAY: a resume starts suppressed (observability — console output, phase
|
|
111
|
-
* markers — was already emitted last segment) and goes `live` the moment it reaches the suspending
|
|
112
|
-
* seam (the frontier = the highest journaled seq). A fresh run (frontier 0) is live immediately.
|
|
113
|
-
*/
|
|
114
|
-
export declare class SeamSequencer {
|
|
115
|
-
private readonly replayFrontier;
|
|
42
|
+
/** A monotonic per-run counter for suspension/gate keys (the `seq` on {@link SuspendSignal}). */
|
|
43
|
+
export declare class SuspensionCounter {
|
|
116
44
|
private count;
|
|
117
|
-
private liveFlag;
|
|
118
|
-
constructor(replayFrontier?: number);
|
|
119
45
|
next(): number;
|
|
120
|
-
/** True once execution has crossed the replay frontier — output after this point is NEW. */
|
|
121
|
-
get isLive(): boolean;
|
|
122
|
-
/** True while re-running already-journaled seams on a resume (observability suppressed). */
|
|
123
|
-
isReplaying(): boolean;
|
|
124
46
|
}
|
|
125
|
-
/** The child run id a `workflow_call` seam journals while it waits (parsed back on resume). */
|
|
126
|
-
export declare const childRunIdSchema: z.ZodString;
|
|
127
|
-
/** A stable content hash of a seam's salient args — the determinism check on replay. Identical
|
|
128
|
-
* construction to the engine's `seamFingerprint` so a journal written by one runtime validates in
|
|
129
|
-
* the other (the conformance promise). */
|
|
130
|
-
export declare function seamFingerprint(parts: readonly unknown[]): string;
|
|
131
|
-
/** A seam reached on replay didn't match what the journal recorded at that seq — the workflow's code
|
|
132
|
-
* on the path to a suspend changed (a different prompt/model/step name, or a different seam kind).
|
|
133
|
-
* Fails the run loudly rather than returning a stale memoized result for the wrong call. */
|
|
134
|
-
export declare function determinismError(seq: number, got: JournalKind, recorded: JournalKind): AppError;
|
|
135
|
-
/**
|
|
136
|
-
* The shape of a SUSPENDED agent leaf's journal result on resume: the transcript checkpoint plus the
|
|
137
|
-
* answers the broker joined from the resolved request rows, keyed by tool-call id. `messages` is the
|
|
138
|
-
* engine's own serialized transcript round-tripping through JSON — handed straight back to the leaf,
|
|
139
|
-
* not re-validated field-by-field. Structurally a {@link LeafResume}.
|
|
140
|
-
*/
|
|
141
|
-
export declare const leafResumeSchema: z.ZodType<LeafResume>;
|
|
@@ -1,120 +1,27 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Suspension primitives for the JS-body worker (the snapshot-substrate model).
|
|
2
2
|
//
|
|
3
|
-
// A run SUSPENDS at a
|
|
4
|
-
// `human_input` tool — by
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// and replays through the journal (a hit returns the memoized value instantly; we never replay an
|
|
8
|
-
// LLM turn — we memoize whole results, the `findChildCall` pattern generalized). This is NOT
|
|
9
|
-
// Temporal-style turn replay, so it does not violate the no-replay-engine rule (CLAUDE.md).
|
|
3
|
+
// A run SUSPENDS at a waiting seam — a long `sleep`, a `humanInput()` gate, or the in-leaf
|
|
4
|
+
// `human_input` tool — by freezing the WHOLE microVM: the freeze coordinator quiesces platform
|
|
5
|
+
// streams, the host snapshots memory, and the wake restores the literal heap, so the seam's
|
|
6
|
+
// `await` resolves in place. There is no journal and no replay — the heap is the durable unit.
|
|
10
7
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
// A runtime with NO freeze substrate (a self-hosted runner daemon, unit tests, the Fargate
|
|
9
|
+
// break-glass) HOLDS instead: the seam blocks the live process until its condition is met. One
|
|
10
|
+
// mechanism serves every non-snapshot surface; the run pays for the idle wait.
|
|
11
|
+
//
|
|
12
|
+
// This file holds the SHARED pieces the host + worker + broker client agree on: the suspend
|
|
13
|
+
// signal a seam raises (what the broker persists as the wake condition) and the human-input gate
|
|
14
|
+
// it carries.
|
|
18
15
|
/**
|
|
19
|
-
* Sleeps at/above this
|
|
20
|
-
*
|
|
21
|
-
* more than it saves.
|
|
22
|
-
* in the same place.
|
|
16
|
+
* Sleeps at/above this boundary SUSPEND on the snapshot substrate (freeze the VM; the wake fires
|
|
17
|
+
* when the sleep is due); shorter ones HOLD the process in-memory, where a snapshot round-trip
|
|
18
|
+
* costs more than it saves. Without a freeze substrate every sleep holds, whatever its length.
|
|
23
19
|
*/
|
|
24
20
|
export const SUSPEND_THRESHOLD_MS = 30_000;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
* supervisor kills out-of-band), the backend worker runs the program IN-PROCESS, so a suspend is
|
|
28
|
-
* surfaced by the host calling `onSuspend(signal)` and returning a NEVER-resolving promise; the
|
|
29
|
-
* worker races that against the program body and tears down. This class exists so a seam that has no
|
|
30
|
-
* `onSuspend` wired (the local/test path) can still raise a typed, catchable signal.
|
|
31
|
-
*/
|
|
32
|
-
export class SuspendError extends Error {
|
|
33
|
-
signal;
|
|
34
|
-
constructor(signal) {
|
|
35
|
-
super(`run suspended (${signal.reason}) at seam ${String(signal.seq)}`);
|
|
36
|
-
this.name = "SuspendError";
|
|
37
|
-
this.signal = signal;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/** Journal-entry states (mirrors `run_journal.state` + the engine's IPC). */
|
|
41
|
-
export const JOURNAL_STATES = ["pending", "suspended", "resolved"];
|
|
42
|
-
/** Validate the broker's journal_get response (the worker's run token doesn't exempt the channel from
|
|
43
|
-
* validation). The result is genuinely heterogeneous JSON (an agent return / a LeafResume / a
|
|
44
|
-
* HumanInputResult), parsed per-kind downstream — so it stays `unknown` here, the validation seam. */
|
|
45
|
-
export const journalLookupSchema = z.object({
|
|
46
|
-
seq: z.number().int().positive(),
|
|
47
|
-
kind: z.enum(["agent", "step", "human_input", "sleep", "workflow_call"]),
|
|
48
|
-
fingerprint: z.string(),
|
|
49
|
-
state: z.enum(JOURNAL_STATES),
|
|
50
|
-
result: z.unknown(),
|
|
51
|
-
});
|
|
52
|
-
/**
|
|
53
|
-
* The synchronous, monotonic durable-seam counter. Incremented at each journaled seam's ENTRY:
|
|
54
|
-
* because a program's synchronous call order is deterministic (even under `Promise.all`, whose
|
|
55
|
-
* `.map(...)` runs left-to-right synchronously), the same logical call gets the same `seq` on every
|
|
56
|
-
* execution — the journal key that lets a resumed run return a memoized result.
|
|
57
|
-
*
|
|
58
|
-
* It also drives SILENT REPLAY: a resume starts suppressed (observability — console output, phase
|
|
59
|
-
* markers — was already emitted last segment) and goes `live` the moment it reaches the suspending
|
|
60
|
-
* seam (the frontier = the highest journaled seq). A fresh run (frontier 0) is live immediately.
|
|
61
|
-
*/
|
|
62
|
-
export class SeamSequencer {
|
|
63
|
-
replayFrontier;
|
|
21
|
+
/** A monotonic per-run counter for suspension/gate keys (the `seq` on {@link SuspendSignal}). */
|
|
22
|
+
export class SuspensionCounter {
|
|
64
23
|
count = 0;
|
|
65
|
-
liveFlag;
|
|
66
|
-
constructor(replayFrontier = 0) {
|
|
67
|
-
this.replayFrontier = replayFrontier;
|
|
68
|
-
this.liveFlag = replayFrontier === 0;
|
|
69
|
-
}
|
|
70
24
|
next() {
|
|
71
|
-
|
|
72
|
-
if (seq >= this.replayFrontier)
|
|
73
|
-
this.liveFlag = true;
|
|
74
|
-
return seq;
|
|
75
|
-
}
|
|
76
|
-
/** True once execution has crossed the replay frontier — output after this point is NEW. */
|
|
77
|
-
get isLive() {
|
|
78
|
-
return this.liveFlag;
|
|
25
|
+
return ++this.count;
|
|
79
26
|
}
|
|
80
|
-
/** True while re-running already-journaled seams on a resume (observability suppressed). */
|
|
81
|
-
isReplaying() {
|
|
82
|
-
return !this.liveFlag;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/** The child run id a `workflow_call` seam journals while it waits (parsed back on resume). */
|
|
86
|
-
export const childRunIdSchema = z.string().min(1);
|
|
87
|
-
/** A stable content hash of a seam's salient args — the determinism check on replay. Identical
|
|
88
|
-
* construction to the engine's `seamFingerprint` so a journal written by one runtime validates in
|
|
89
|
-
* the other (the conformance promise). */
|
|
90
|
-
export function seamFingerprint(parts) {
|
|
91
|
-
return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
92
27
|
}
|
|
93
|
-
/** A seam reached on replay didn't match what the journal recorded at that seq — the workflow's code
|
|
94
|
-
* on the path to a suspend changed (a different prompt/model/step name, or a different seam kind).
|
|
95
|
-
* Fails the run loudly rather than returning a stale memoized result for the wrong call. */
|
|
96
|
-
export function determinismError(seq, got, recorded) {
|
|
97
|
-
const detail = got === recorded
|
|
98
|
-
? `the same "${got}" seam but with different arguments (a changed prompt, model, or step name)`
|
|
99
|
-
: `a "${recorded}" call, but this execution reached a "${got}" call`;
|
|
100
|
-
return new AppError(ErrorCode.VALIDATION_FAILED, `Nondeterministic replay at seam ${String(seq)}: the journal recorded ${detail}. A workflow's ` +
|
|
101
|
-
`code on the path to a suspend/resume must be deterministic — route nondeterministic I/O ` +
|
|
102
|
-
`through agent(), step.run(), or workflows.call so it is journaled.`, { kind: "nondeterministic_replay", seq });
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* The shape of a SUSPENDED agent leaf's journal result on resume: the transcript checkpoint plus the
|
|
106
|
-
* answers the broker joined from the resolved request rows, keyed by tool-call id. `messages` is the
|
|
107
|
-
* engine's own serialized transcript round-tripping through JSON — handed straight back to the leaf,
|
|
108
|
-
* not re-validated field-by-field. Structurally a {@link LeafResume}.
|
|
109
|
-
*/
|
|
110
|
-
export const leafResumeSchema = z.object({
|
|
111
|
-
checkpoint: z.object({
|
|
112
|
-
messages: z.array(z.custom()),
|
|
113
|
-
iteration: z.number().int(),
|
|
114
|
-
totals: z.object({
|
|
115
|
-
inputTokens: z.number().int(),
|
|
116
|
-
outputTokens: z.number().int(),
|
|
117
|
-
}),
|
|
118
|
-
}),
|
|
119
|
-
answers: z.record(z.string(), z.unknown()),
|
|
120
|
-
});
|
|
@@ -28,7 +28,7 @@ export type ParsedHumanInputSpec = z.infer<typeof humanInputSpecSchema>;
|
|
|
28
28
|
export declare function parseHumanInputSpec(raw: unknown): ParsedHumanInputSpec;
|
|
29
29
|
/** Re-hydrate a stored (jsonb) human-input result into the exact-optional SDK {@link HumanInputResult}
|
|
30
30
|
* (the host returns it from a resumed `humanInput()` seam). The value was validated on submit, so a
|
|
31
|
-
* parse failure here is a corrupt
|
|
31
|
+
* parse failure here is a corrupt stored answer — surfaced as a clear program error. */
|
|
32
32
|
export declare function normalizeHumanInputResult(raw: unknown): HumanInputResult;
|
|
33
33
|
/** Convert a validated {@link HumanInputResult} to a {@link JsonValue} for storage (the result is an
|
|
34
34
|
* interface with optional props, which TS won't structurally accept as JsonValue; an explicit object
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// The `HumanInputSpec` is the single source of truth for BOTH the UI form and server-side
|
|
4
4
|
// validation, so a `humanInput()` gate needs no JSON Schema. A responder's raw submission (from
|
|
5
5
|
// REST / MCP / CLI / web) is validated here against the gate's stored spec into a typed
|
|
6
|
-
// `HumanInputResult`,
|
|
6
|
+
// `HumanInputResult`, the value the seam returns to the program. Pure + exhaustively testable.
|
|
7
7
|
//
|
|
8
8
|
// These mirror the SDK's `@boardwalk-labs/workflow` types exactly (the wire contract); we re-derive
|
|
9
9
|
// them as Zod schemas because the stored `input_spec` is `unknown` jsonb that must be parsed at the
|
|
@@ -51,7 +51,7 @@ export function parseHumanInputSpec(raw) {
|
|
|
51
51
|
}
|
|
52
52
|
/** Re-hydrate a stored (jsonb) human-input result into the exact-optional SDK {@link HumanInputResult}
|
|
53
53
|
* (the host returns it from a resumed `humanInput()` seam). The value was validated on submit, so a
|
|
54
|
-
* parse failure here is a corrupt
|
|
54
|
+
* parse failure here is a corrupt stored answer — surfaced as a clear program error. */
|
|
55
55
|
export function normalizeHumanInputResult(raw) {
|
|
56
56
|
const parsed = storedResultSchema.safeParse(raw);
|
|
57
57
|
if (!parsed.success) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { WorkflowHost, AgentOptions, ArtifactBody, ArtifactRef, BrowserSession, BrowserSessionOptions, CallOptions, HumanInputOptions, HumanInputResult, PhaseOptions, SleepArg } from "@boardwalk-labs/workflow/runtime";
|
|
2
2
|
import type { BrowserSessionManager } from "./browser_session.js";
|
|
3
3
|
import { type LeafResume } from "@boardwalk-labs/engine/core";
|
|
4
|
-
import { type
|
|
4
|
+
import { type SuspendSignal } from "./suspension.js";
|
|
5
5
|
import type { FreezeCoordinator } from "./freeze_coordinator.js";
|
|
6
6
|
/** Parse a `humanInput({ timeout })` string (`"48h"`, `"30m"`, `"90s"`, `"7d"`) to milliseconds, or
|
|
7
7
|
* null when absent/unparseable (the gate then waits indefinitely). */
|
|
@@ -36,10 +36,11 @@ export interface ChildResult {
|
|
|
36
36
|
status: string;
|
|
37
37
|
output: unknown;
|
|
38
38
|
}
|
|
39
|
-
/** Dispatches child runs: `call` holds + returns
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* schedule id. The `signal` lets a hold/start abort
|
|
39
|
+
/** Dispatches child runs: `call` holds in-process + returns the completed child's output (the
|
|
40
|
+
* hold path); `start`/`poll` back the snapshot-substrate callWorkflow seam (start once, freeze
|
|
41
|
+
* `waiting_for_child` on a non-terminal child); `run` is fire-and-forget → run id; `schedule`
|
|
42
|
+
* provisions a durable future/recurring run → schedule id. The `signal` lets a hold/start abort
|
|
43
|
+
* promptly when the parent run is cancelled. */
|
|
43
44
|
export interface ChildDispatcher {
|
|
44
45
|
call(slug: string, input: unknown, opts: CallOptions | undefined, signal?: AbortSignal): Promise<unknown>;
|
|
45
46
|
/** Start (or idempotently re-attach to) a child run; resolves its current state. */
|
|
@@ -53,9 +54,10 @@ export interface ChildDispatcher {
|
|
|
53
54
|
export interface SecretAccessor {
|
|
54
55
|
get(name: string): Promise<string>;
|
|
55
56
|
}
|
|
56
|
-
/** Register
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
/** Register a HELD HITL gate so it is answerable while the run keeps its process, and poll for
|
|
58
|
+
* the answer. Backed by the broker's `inputs` endpoints. On the snapshot substrate this is the
|
|
59
|
+
* register-without-release half of a freeze; on a no-freeze runtime (a self-hosted daemon) it is
|
|
60
|
+
* the WHOLE mechanism — the seam registers, then holds and polls until answered. */
|
|
59
61
|
export interface HeldInputPort {
|
|
60
62
|
register(seq: number, gate: SuspendSignal["humanInput"]): Promise<unknown>;
|
|
61
63
|
poll(seq: number): Promise<Record<string, unknown>>;
|
|
@@ -130,39 +132,21 @@ export interface WorkerWorkflowHostDeps {
|
|
|
130
132
|
now?: () => number;
|
|
131
133
|
/** Override the 7-day hold ceiling (tests). */
|
|
132
134
|
maxSleepMs?: number;
|
|
133
|
-
/**
|
|
134
|
-
* Durable-suspension journal (the durable-suspension design): the host memoizes each `agent`/`step`/`sleep`/
|
|
135
|
-
* `humanInput` seam here, so a resumed run replays journaled seams instantly instead of re-running
|
|
136
|
-
* them. Backed by the broker over the run token on hosted runs. Absent ⇒ no memoization (the
|
|
137
|
-
* local/test path): seams run live every time and a suspend can't be resumed.
|
|
138
|
-
*/
|
|
139
|
-
journal?: JournalSeam;
|
|
140
|
-
/**
|
|
141
|
-
* Raise a durable suspension: the host calls this from a suspending seam, then returns a
|
|
142
|
-
* never-resolving promise; the worker races that against the program body and tears the task down
|
|
143
|
-
* (the program's own `try/catch` can't swallow a suspend this way). Absent ⇒ a suspending seam
|
|
144
|
-
* rejects with {@link SuspendError} instead (the local/test path).
|
|
145
|
-
*/
|
|
146
|
-
onSuspend?: (signal: SuspendSignal) => void;
|
|
147
135
|
/**
|
|
148
136
|
* Snapshot-substrate suspension (the microVM freeze model): when present, a suspending seam
|
|
149
|
-
* BLOCKS on the coordinator
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
137
|
+
* BLOCKS on the coordinator — the platform freezes the whole VM and a wake resolves the seam in
|
|
138
|
+
* place, heap intact. Every hook also runs under the coordinator's quiescence gate: a freeze
|
|
139
|
+
* never captures a live platform stream, and work arriving while a freeze is pending queues
|
|
140
|
+
* until the wake. Absent ⇒ the no-substrate HOLD path: a waiting seam blocks the live process
|
|
141
|
+
* for the whole wait (self-hosted daemons, the Fargate break-glass, unit tests).
|
|
153
142
|
*/
|
|
154
143
|
freeze?: FreezeCoordinator;
|
|
155
|
-
/** Register
|
|
156
|
-
*
|
|
144
|
+
/** Register + poll for HELD human-input gates (see {@link HeldInputPort}). Absent alongside
|
|
145
|
+
* `freeze` ⇒ a held gate is answerable only once it freezes; absent WITHOUT `freeze` ⇒
|
|
146
|
+
* humanInput is unsupported in this runtime and rejects clearly. */
|
|
157
147
|
heldInput?: HeldInputPort;
|
|
158
148
|
/** Poll interval for a held gate's answer (default 3s). */
|
|
159
149
|
heldPollIntervalMs?: number;
|
|
160
|
-
/**
|
|
161
|
-
* The highest journaled seq at claim (0 on a fresh run). While re-running seams up to this
|
|
162
|
-
* frontier on a resume, the host is REPLAYING and observability (phase markers, program logs) is
|
|
163
|
-
* suppressed — those lines were emitted in the prior segment.
|
|
164
|
-
*/
|
|
165
|
-
replayFrontier?: number;
|
|
166
150
|
}
|
|
167
151
|
export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
168
152
|
private readonly deps;
|
|
@@ -170,18 +154,11 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
|
170
154
|
private readonly now;
|
|
171
155
|
private readonly maxSleepMs;
|
|
172
156
|
private readonly heldPollIntervalMs;
|
|
173
|
-
/**
|
|
157
|
+
/** Monotonic per-run counter keying suspensions + their HITL gate rows. */
|
|
174
158
|
private readonly seq;
|
|
175
159
|
/** Run context + on-demand public-API bearer the SDK `runtime` accessor reads off the host. */
|
|
176
160
|
readonly runtime: RuntimeContext;
|
|
177
161
|
constructor(deps: WorkerWorkflowHostDeps);
|
|
178
|
-
/** True while re-running already-journaled seams on a resume (the worker suppresses program-log +
|
|
179
|
-
* phase observability during this window — those lines were emitted in the prior segment). */
|
|
180
|
-
isReplaying(): boolean;
|
|
181
|
-
/** Raise a durable suspension. With `onSuspend` wired (the real worker) the host signals out-of-band
|
|
182
|
-
* and returns a never-resolving promise, so the program's own try/catch can't swallow it; without
|
|
183
|
-
* it (the local/test path) it rejects with {@link SuspendError}. */
|
|
184
|
-
private suspend;
|
|
185
162
|
/** Run `fn` only if the run isn't aborted; otherwise REJECT (never throw synchronously) with the
|
|
186
163
|
* signal's RunAbortedError — every Promise-returning hook funnels through this so callers always
|
|
187
164
|
* get a rejected promise on abort, not a sync throw. On the snapshot substrate the body also runs
|
|
@@ -207,6 +184,13 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
|
207
184
|
* the answer value; never rejects the run (a transient poll error just retries next tick). Runs
|
|
208
185
|
* OUTSIDE the quiescence gate (it is not run work — it must not block a freeze). */
|
|
209
186
|
private pollHeldAnswer;
|
|
187
|
+
/**
|
|
188
|
+
* The no-freeze HOLD for a human-input gate: register it with the broker (it surfaces in the
|
|
189
|
+
* inbox/API at once), then poll until the answer arrives — the process stays alive and pays
|
|
190
|
+
* for the wait. Rejects promptly when the run's abort signal fires (cancel / credit stop),
|
|
191
|
+
* which is also how a server-side gate expiry that fails the run unwinds this loop.
|
|
192
|
+
*/
|
|
193
|
+
private holdForAnswer;
|
|
210
194
|
/** Map a froze/aborted freeze outcome to the gate's answer (or throw on an unexpected abort). */
|
|
211
195
|
private resolveFreezeAnswer;
|
|
212
196
|
/** The wake's answer for one gate key. A wake whose value is missing the parked gate means the
|
|
@@ -215,26 +199,23 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
|
215
199
|
private gateAnswer;
|
|
216
200
|
setPhase(name: string, opts: PhaseOptions | undefined): void;
|
|
217
201
|
agent(prompt: string, opts: AgentOptions | undefined): Promise<unknown>;
|
|
218
|
-
/** The `agent()`
|
|
219
|
-
* `
|
|
220
|
-
*
|
|
202
|
+
/** The `agent()` seam: run the leaf to completion. A leaf that PARKS (the model called the
|
|
203
|
+
* `human_input` tool) waits for the answer — a freeze on the snapshot substrate, a held poll
|
|
204
|
+
* otherwise — and re-enters from its in-memory checkpoint. Nothing is memoized: a crash-restart
|
|
205
|
+
* re-runs the leaf (restart-from-top semantics). */
|
|
221
206
|
private agentSeam;
|
|
222
|
-
/** Program-level `humanInput()`:
|
|
223
|
-
* SDK marks this optional; the hosted
|
|
207
|
+
/** Program-level `humanInput()`: wait on a gate — a freeze on the snapshot substrate, a held
|
|
208
|
+
* poll otherwise — and return the validated answer. The SDK marks this optional; the hosted
|
|
209
|
+
* host always implements it. */
|
|
224
210
|
humanInput(opts: HumanInputOptions): Promise<HumanInputResult>;
|
|
225
211
|
private humanInputSeam;
|
|
226
212
|
/** Absolute wake time for a `humanInput({ timeout })`, or null when there is none / it's unparseable. */
|
|
227
213
|
private timeoutExpiry;
|
|
228
|
-
/** `step.run(name, fn)`: run `fn` exactly once across restarts, memoizing its result in the journal
|
|
229
|
-
* (the escape hatch for nondeterministic work on a suspend/resume path). */
|
|
230
|
-
step(name: string, fn: () => unknown): Promise<unknown>;
|
|
231
|
-
private stepSeam;
|
|
232
214
|
callWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<unknown>;
|
|
233
|
-
/** The `workflows.call`
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* local/test path) it falls back to the in-process hold-and-poll. */
|
|
215
|
+
/** The `workflows.call` seam: start the child once (idempotently — the child's run row is the
|
|
216
|
+
* durable memo, so a crash-restarted parent re-attaches instead of re-spawning). A non-terminal
|
|
217
|
+
* child suspends the parent `waiting_for_child` on the snapshot substrate (the wake carries the
|
|
218
|
+
* finalized child, heap intact); without one the parent HOLDS in-process and polls. */
|
|
238
219
|
private callWorkflowSeam;
|
|
239
220
|
/** Fire-and-forget trigger of another workflow; resolves to the new run's id (no hold/poll). */
|
|
240
221
|
runWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise<string>;
|
|
@@ -245,7 +226,7 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
|
245
226
|
writeArtifact(name: string, contentType: string, body: ArtifactBody, metadata: Record<string, unknown> | undefined): Promise<ArtifactRef>;
|
|
246
227
|
/** `computer.openBrowser()`: open a program-owned, in-VM browser session (the browser tier of
|
|
247
228
|
* computer use). Not a durable seam — a session is a live resource, reaped at run end, never
|
|
248
|
-
*
|
|
229
|
+
* persisted. Absent backend ⇒ a clear "not available" error. */
|
|
249
230
|
openBrowserSession(opts: BrowserSessionOptions | undefined): Promise<BrowserSession>;
|
|
250
231
|
/** Translate `agent({ session })` into the leaf's `mcp`: append the browser session's in-VM
|
|
251
232
|
* Playwright MCP (its http ref, which passes assertHostedMcpAllowed and reaches localhost without
|
|
@@ -253,9 +234,10 @@ export declare class WorkerWorkflowHost implements WorkflowHost {
|
|
|
253
234
|
* no session is bound. */
|
|
254
235
|
private bindBrowserSession;
|
|
255
236
|
sleep(arg: SleepArg): Promise<void>;
|
|
256
|
-
/** A short sleep HOLDS the task in-process (cheaper than a
|
|
257
|
-
* (≥ {@link SUSPEND_THRESHOLD_MS}) SUSPENDS
|
|
258
|
-
*
|
|
237
|
+
/** A short sleep HOLDS the task in-process (cheaper than a snapshot round-trip); a long one
|
|
238
|
+
* (≥ {@link SUSPEND_THRESHOLD_MS}) SUSPENDS on the snapshot substrate — the VM freezes and the
|
|
239
|
+
* wake resolves this very await, heap intact. Without a freeze substrate EVERY sleep holds,
|
|
240
|
+
* whatever its length (the no-substrate rule: snapshot or hold, never replay). */
|
|
259
241
|
private sleepSeam;
|
|
260
242
|
/** Resolve any {@link SleepArg} shape to a millisecond duration from now. */
|
|
261
243
|
private resolveSleepMs;
|