@boardwalk-labs/runner 0.1.2
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/LICENSE +202 -0
- package/README.md +68 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +214 -0
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +196 -0
- package/dist/daemon/daemon.d.ts +45 -0
- package/dist/daemon/daemon.js +129 -0
- package/dist/daemon/identity.d.ts +15 -0
- package/dist/daemon/identity.js +44 -0
- package/dist/daemon/index.d.ts +3 -0
- package/dist/daemon/index.js +4 -0
- package/dist/daemon/pool_client.d.ts +31 -0
- package/dist/daemon/pool_client.js +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -0
- package/dist/runtime/agent/budget.d.ts +123 -0
- package/dist/runtime/agent/budget.js +174 -0
- package/dist/runtime/agent/events.d.ts +46 -0
- package/dist/runtime/agent/events.js +8 -0
- package/dist/runtime/agent/model_rates.d.ts +7 -0
- package/dist/runtime/agent/model_rates.js +21 -0
- package/dist/runtime/agent/secret_redactor.d.ts +42 -0
- package/dist/runtime/agent/secret_redactor.js +98 -0
- package/dist/runtime/broker_artifact_store.d.ts +17 -0
- package/dist/runtime/broker_artifact_store.js +46 -0
- package/dist/runtime/broker_child_dispatcher.d.ts +31 -0
- package/dist/runtime/broker_child_dispatcher.js +88 -0
- package/dist/runtime/broker_event_publisher.d.ts +25 -0
- package/dist/runtime/broker_event_publisher.js +69 -0
- package/dist/runtime/broker_tool_host.d.ts +49 -0
- package/dist/runtime/broker_tool_host.js +240 -0
- package/dist/runtime/cancel_watcher.d.ts +31 -0
- package/dist/runtime/cancel_watcher.js +77 -0
- package/dist/runtime/credit_watcher.d.ts +30 -0
- package/dist/runtime/credit_watcher.js +78 -0
- package/dist/runtime/direct_inference.d.ts +29 -0
- package/dist/runtime/direct_inference.js +78 -0
- package/dist/runtime/index.d.ts +85 -0
- package/dist/runtime/index.js +525 -0
- package/dist/runtime/inference_transport.d.ts +5 -0
- package/dist/runtime/inference_transport.js +9 -0
- package/dist/runtime/leaf_executor.d.ts +104 -0
- package/dist/runtime/leaf_executor.js +333 -0
- package/dist/runtime/lease_renewer.d.ts +31 -0
- package/dist/runtime/lease_renewer.js +80 -0
- package/dist/runtime/main.d.ts +1 -0
- package/dist/runtime/main.js +7 -0
- package/dist/runtime/phase_tracker.d.ts +26 -0
- package/dist/runtime/phase_tracker.js +45 -0
- package/dist/runtime/program_log_capture.d.ts +20 -0
- package/dist/runtime/program_log_capture.js +79 -0
- package/dist/runtime/program_runner.d.ts +82 -0
- package/dist/runtime/program_runner.js +116 -0
- package/dist/runtime/program_worker.d.ts +177 -0
- package/dist/runtime/program_worker.js +272 -0
- package/dist/runtime/recording_secret_resolver.d.ts +9 -0
- package/dist/runtime/recording_secret_resolver.js +22 -0
- package/dist/runtime/run_abort.d.ts +16 -0
- package/dist/runtime/run_abort.js +38 -0
- package/dist/runtime/run_event_emitter.d.ts +24 -0
- package/dist/runtime/run_event_emitter.js +63 -0
- package/dist/runtime/runner_control_client.d.ts +184 -0
- package/dist/runtime/runner_control_client.js +501 -0
- package/dist/runtime/runtime_flusher.d.ts +41 -0
- package/dist/runtime/runtime_flusher.js +90 -0
- package/dist/runtime/sandbox_config.d.ts +21 -0
- package/dist/runtime/sandbox_config.js +34 -0
- package/dist/runtime/support/index.d.ts +53 -0
- package/dist/runtime/support/index.js +114 -0
- package/dist/runtime/suspension.d.ts +141 -0
- package/dist/runtime/suspension.js +120 -0
- package/dist/runtime/testing_artifact_build.d.ts +27 -0
- package/dist/runtime/testing_artifact_build.js +96 -0
- package/dist/runtime/tools/artifacts.d.ts +146 -0
- package/dist/runtime/tools/artifacts.js +125 -0
- package/dist/runtime/tools/types.d.ts +114 -0
- package/dist/runtime/tools/types.js +61 -0
- package/dist/runtime/tools/web_search.d.ts +66 -0
- package/dist/runtime/tools/web_search.js +165 -0
- package/dist/runtime/wire/artifact_storage.d.ts +37 -0
- package/dist/runtime/wire/artifact_storage.js +93 -0
- package/dist/runtime/wire/artifact_verify.d.ts +1 -0
- package/dist/runtime/wire/artifact_verify.js +8 -0
- package/dist/runtime/wire/human_input.d.ts +62 -0
- package/dist/runtime/wire/human_input.js +156 -0
- package/dist/runtime/wire/inference_proxy.d.ts +70 -0
- package/dist/runtime/wire/inference_proxy.js +157 -0
- package/dist/runtime/wire/manifest.d.ts +16 -0
- package/dist/runtime/wire/manifest.js +18 -0
- package/dist/runtime/wire/run.d.ts +54 -0
- package/dist/runtime/wire/run.js +2 -0
- package/dist/runtime/workflow_host.d.ts +194 -0
- package/dist/runtime/workflow_host.js +382 -0
- package/dist/runtime/workspace_store.d.ts +73 -0
- package/dist/runtime/workspace_store.js +118 -0
- package/package.json +70 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Budget } from "../wire/manifest.js";
|
|
2
|
+
/** Per-million-token rates. */
|
|
3
|
+
export interface ModelRate {
|
|
4
|
+
/** USD per million input tokens. */
|
|
5
|
+
inputPerMillion: number;
|
|
6
|
+
/** USD per million output tokens. */
|
|
7
|
+
outputPerMillion: number;
|
|
8
|
+
/** USD per million cache-read tokens (Anthropic). Defaults to inputPerMillion / 10. */
|
|
9
|
+
cacheReadPerMillion?: number;
|
|
10
|
+
/** USD per million cache-write tokens (Anthropic). Defaults to inputPerMillion * 1.25. */
|
|
11
|
+
cacheWritePerMillion?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface UsageDelta {
|
|
14
|
+
inputTokens?: number;
|
|
15
|
+
outputTokens?: number;
|
|
16
|
+
cacheReadTokens?: number;
|
|
17
|
+
cacheWriteTokens?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Cumulative usage carried forward from PRIOR worker sessions of the same run. Seeded from the
|
|
21
|
+
* checkpoint on resume so budget caps bound the WHOLE run, not just the current session — a run
|
|
22
|
+
* that sleeps / waits on a child / recovers from a crash must not get a fresh budget each time
|
|
23
|
+
* (review #2). Zero for a fresh run.
|
|
24
|
+
*/
|
|
25
|
+
export interface PriorUsage {
|
|
26
|
+
inputTokens: number;
|
|
27
|
+
outputTokens: number;
|
|
28
|
+
cacheReadTokens: number;
|
|
29
|
+
cacheWriteTokens: number;
|
|
30
|
+
totalUsd: number;
|
|
31
|
+
/**
|
|
32
|
+
* Active execution time from prior sessions (ms). Only on-CPU turn time counts toward the
|
|
33
|
+
* duration cap — sleep/wait pauses don't burn it (a run intentionally parked for a day must
|
|
34
|
+
* not blow its duration budget on resume).
|
|
35
|
+
*/
|
|
36
|
+
activeMs: number;
|
|
37
|
+
}
|
|
38
|
+
export interface BudgetMeterOptions {
|
|
39
|
+
budget?: Budget;
|
|
40
|
+
/** Rate for the model the agent is configured with. */
|
|
41
|
+
rate: ModelRate;
|
|
42
|
+
/** This SESSION's start time (ms). Per-session active duration is measured from here. */
|
|
43
|
+
startedAt: number;
|
|
44
|
+
/** The RUN's original start time (ms) — the basis for `deadline_seconds`, a WALL-CLOCK cap that
|
|
45
|
+
* INCLUDES suspended idle (unlike max_duration_seconds, which is active compute). Omit to leave
|
|
46
|
+
* the deadline cap unenforced (e.g. a run that hasn't started yet). */
|
|
47
|
+
deadlineStartedAt?: number;
|
|
48
|
+
/** Cumulative usage from prior sessions of this run (resume). Defaults to zero. */
|
|
49
|
+
priorUsage?: PriorUsage;
|
|
50
|
+
/** Injected clock for tests. */
|
|
51
|
+
now?: () => number;
|
|
52
|
+
}
|
|
53
|
+
export interface BudgetSnapshot {
|
|
54
|
+
inputTokens: number;
|
|
55
|
+
outputTokens: number;
|
|
56
|
+
cacheReadTokens: number;
|
|
57
|
+
cacheWriteTokens: number;
|
|
58
|
+
totalTokens: number;
|
|
59
|
+
totalUsd: number;
|
|
60
|
+
elapsedMs: number;
|
|
61
|
+
}
|
|
62
|
+
export declare class BudgetMeter {
|
|
63
|
+
private readonly budget;
|
|
64
|
+
private readonly rate;
|
|
65
|
+
private readonly startedAt;
|
|
66
|
+
private readonly deadlineStartedAt;
|
|
67
|
+
private readonly prior;
|
|
68
|
+
private readonly now;
|
|
69
|
+
private inputTokens;
|
|
70
|
+
private outputTokens;
|
|
71
|
+
private cacheReadTokens;
|
|
72
|
+
private cacheWriteTokens;
|
|
73
|
+
private totalUsd;
|
|
74
|
+
constructor(opts: BudgetMeterOptions);
|
|
75
|
+
/**
|
|
76
|
+
* Add a usage delta to the accumulator + recompute USD. `realCostUsd`, when provided, is the EXACT
|
|
77
|
+
* upstream cost the broker observed for this turn (OpenRouter's per-request `usage.cost`) — used
|
|
78
|
+
* verbatim so the `max_usd` cap tracks ACTUAL spend (already cache-discounted, model-correct).
|
|
79
|
+
* Omitted for a BYO turn (no upstream price) or a managed turn whose cost the broker couldn't read,
|
|
80
|
+
* which fall back to the representative-rate {@link costFor} estimate. Token counts accumulate on
|
|
81
|
+
* both paths (they drive the `max_tokens` cap + the snapshot); only the USD basis differs.
|
|
82
|
+
*/
|
|
83
|
+
addUsage(delta: UsageDelta, realCostUsd?: number): void;
|
|
84
|
+
/**
|
|
85
|
+
* THIS SESSION's accumulator snapshot (excludes prior-session usage). Read by per-session token
|
|
86
|
+
* metering — which reports token deltas to Stripe (deferred; not yet wired into the brokered
|
|
87
|
+
* loop) — and by audit/post-run cost rollups. Cap enforcement uses {@link cumulative} instead.
|
|
88
|
+
*/
|
|
89
|
+
snapshot(): BudgetSnapshot;
|
|
90
|
+
/**
|
|
91
|
+
* RUN-CUMULATIVE snapshot: this session's usage PLUS the prior sessions' usage seeded at
|
|
92
|
+
* construction. Cap enforcement ({@link assertWithinCaps}) and checkpoint persistence use
|
|
93
|
+
* this so a run that resumes after a sleep/wait/crash is bounded by its declared caps across
|
|
94
|
+
* the whole run — not once per session (review #2). `snapshot()` stays session-local because
|
|
95
|
+
* per-session token metering reports token deltas; seeding it would double-report to Stripe.
|
|
96
|
+
*/
|
|
97
|
+
cumulative(): BudgetSnapshot;
|
|
98
|
+
/**
|
|
99
|
+
* Throws `AppError(BUDGET_EXCEEDED)` when any cap is breached. Call BETWEEN
|
|
100
|
+
* turns (after `addUsage` reflects the latest delta) — that way the meter
|
|
101
|
+
* tears the loop down before another LLM call is dispatched.
|
|
102
|
+
*/
|
|
103
|
+
assertWithinCaps(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Predicate variant for callers that prefer to handle the cap-hit path
|
|
106
|
+
* inline (e.g., the sleep tool rejects a sleep that would breach the
|
|
107
|
+
* duration cap). Returns the first breach reason or null.
|
|
108
|
+
*/
|
|
109
|
+
capBreachReason(): "tokens" | "usd" | "duration" | "deadline" | null;
|
|
110
|
+
/**
|
|
111
|
+
* The declared `max_duration_seconds` cap, or null when unset. Lets callers that
|
|
112
|
+
* reason about wall-clock ahead of time (e.g. the sleep tool rejecting a sleep that
|
|
113
|
+
* would overrun the run) read the cap without reaching into the meter's internals.
|
|
114
|
+
*/
|
|
115
|
+
durationCapSeconds(): number | null;
|
|
116
|
+
/**
|
|
117
|
+
* USD cost for a single delta — the representative-rate ESTIMATE used only when the broker reports
|
|
118
|
+
* no real upstream cost for the turn (a BYO-provider turn, or a managed turn whose cost tap missed).
|
|
119
|
+
* Managed turns instead carry OpenRouter's exact `usage.cost` through to {@link addUsage}. Public so
|
|
120
|
+
* the loop can stamp the per-step `cost_usd` column on `run_steps` without re-deriving the rate table.
|
|
121
|
+
*/
|
|
122
|
+
costFor(delta: UsageDelta): number;
|
|
123
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// BudgetMeter — enforces token / USD / duration caps declared in the manifest.
|
|
2
|
+
//
|
|
3
|
+
// One meter per run. The agent loop calls `addUsage()` after each LLM turn and
|
|
4
|
+
// `assertWithinCaps()` between turns. When a cap is exceeded the meter throws
|
|
5
|
+
// `AppError(BUDGET_EXCEEDED)` and the loop tears down with a `turn_ended
|
|
6
|
+
// reason='error'` event.
|
|
7
|
+
//
|
|
8
|
+
// USD here tracks the bill closely: for a MANAGED turn the meter is fed OpenRouter's exact per-request
|
|
9
|
+
// `usage.cost` (already cache-discounted + model-correct), so the `max_usd` cap reflects real spend —
|
|
10
|
+
// not a hand-rolled token estimate. Only a BYO turn (no upstream price) or a managed turn whose cost
|
|
11
|
+
// the broker couldn't read falls back to a single flat representative rate
|
|
12
|
+
// (`model_rates.ts::BUDGET_GUARDRAIL_RATE`), since a per-`agent()`-model run has no one model. Actual
|
|
13
|
+
// billing remains per-request cost pass-through via Stripe meters (MASTER_SPEC §15.2). The meter takes
|
|
14
|
+
// the fallback rate as a constructor arg so tests inject a fixed rate.
|
|
15
|
+
import { AppError, ErrorCode } from "../support/index.js";
|
|
16
|
+
const ZERO_PRIOR = {
|
|
17
|
+
inputTokens: 0,
|
|
18
|
+
outputTokens: 0,
|
|
19
|
+
cacheReadTokens: 0,
|
|
20
|
+
cacheWriteTokens: 0,
|
|
21
|
+
totalUsd: 0,
|
|
22
|
+
activeMs: 0,
|
|
23
|
+
};
|
|
24
|
+
export class BudgetMeter {
|
|
25
|
+
budget;
|
|
26
|
+
rate;
|
|
27
|
+
startedAt;
|
|
28
|
+
deadlineStartedAt;
|
|
29
|
+
prior;
|
|
30
|
+
now;
|
|
31
|
+
inputTokens = 0;
|
|
32
|
+
outputTokens = 0;
|
|
33
|
+
cacheReadTokens = 0;
|
|
34
|
+
cacheWriteTokens = 0;
|
|
35
|
+
totalUsd = 0;
|
|
36
|
+
constructor(opts) {
|
|
37
|
+
if (opts.budget !== undefined)
|
|
38
|
+
this.budget = opts.budget;
|
|
39
|
+
this.rate = opts.rate;
|
|
40
|
+
this.startedAt = opts.startedAt;
|
|
41
|
+
this.deadlineStartedAt = opts.deadlineStartedAt ?? null;
|
|
42
|
+
this.prior = opts.priorUsage ?? ZERO_PRIOR;
|
|
43
|
+
this.now = opts.now ?? Date.now;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Add a usage delta to the accumulator + recompute USD. `realCostUsd`, when provided, is the EXACT
|
|
47
|
+
* upstream cost the broker observed for this turn (OpenRouter's per-request `usage.cost`) — used
|
|
48
|
+
* verbatim so the `max_usd` cap tracks ACTUAL spend (already cache-discounted, model-correct).
|
|
49
|
+
* Omitted for a BYO turn (no upstream price) or a managed turn whose cost the broker couldn't read,
|
|
50
|
+
* which fall back to the representative-rate {@link costFor} estimate. Token counts accumulate on
|
|
51
|
+
* both paths (they drive the `max_tokens` cap + the snapshot); only the USD basis differs.
|
|
52
|
+
*/
|
|
53
|
+
addUsage(delta, realCostUsd) {
|
|
54
|
+
this.inputTokens += delta.inputTokens ?? 0;
|
|
55
|
+
this.outputTokens += delta.outputTokens ?? 0;
|
|
56
|
+
this.cacheReadTokens += delta.cacheReadTokens ?? 0;
|
|
57
|
+
this.cacheWriteTokens += delta.cacheWriteTokens ?? 0;
|
|
58
|
+
this.totalUsd += realCostUsd ?? this.costFor(delta);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* THIS SESSION's accumulator snapshot (excludes prior-session usage). Read by per-session token
|
|
62
|
+
* metering — which reports token deltas to Stripe (deferred; not yet wired into the brokered
|
|
63
|
+
* loop) — and by audit/post-run cost rollups. Cap enforcement uses {@link cumulative} instead.
|
|
64
|
+
*/
|
|
65
|
+
snapshot() {
|
|
66
|
+
return {
|
|
67
|
+
inputTokens: this.inputTokens,
|
|
68
|
+
outputTokens: this.outputTokens,
|
|
69
|
+
cacheReadTokens: this.cacheReadTokens,
|
|
70
|
+
cacheWriteTokens: this.cacheWriteTokens,
|
|
71
|
+
totalTokens: this.inputTokens + this.outputTokens,
|
|
72
|
+
totalUsd: this.totalUsd,
|
|
73
|
+
elapsedMs: this.now() - this.startedAt,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* RUN-CUMULATIVE snapshot: this session's usage PLUS the prior sessions' usage seeded at
|
|
78
|
+
* construction. Cap enforcement ({@link assertWithinCaps}) and checkpoint persistence use
|
|
79
|
+
* this so a run that resumes after a sleep/wait/crash is bounded by its declared caps across
|
|
80
|
+
* the whole run — not once per session (review #2). `snapshot()` stays session-local because
|
|
81
|
+
* per-session token metering reports token deltas; seeding it would double-report to Stripe.
|
|
82
|
+
*/
|
|
83
|
+
cumulative() {
|
|
84
|
+
const s = this.snapshot();
|
|
85
|
+
return {
|
|
86
|
+
inputTokens: s.inputTokens + this.prior.inputTokens,
|
|
87
|
+
outputTokens: s.outputTokens + this.prior.outputTokens,
|
|
88
|
+
cacheReadTokens: s.cacheReadTokens + this.prior.cacheReadTokens,
|
|
89
|
+
cacheWriteTokens: s.cacheWriteTokens + this.prior.cacheWriteTokens,
|
|
90
|
+
totalTokens: s.inputTokens + this.prior.inputTokens + s.outputTokens + this.prior.outputTokens,
|
|
91
|
+
totalUsd: s.totalUsd + this.prior.totalUsd,
|
|
92
|
+
elapsedMs: s.elapsedMs + this.prior.activeMs,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Throws `AppError(BUDGET_EXCEEDED)` when any cap is breached. Call BETWEEN
|
|
97
|
+
* turns (after `addUsage` reflects the latest delta) — that way the meter
|
|
98
|
+
* tears the loop down before another LLM call is dispatched.
|
|
99
|
+
*/
|
|
100
|
+
assertWithinCaps() {
|
|
101
|
+
if (this.budget === undefined)
|
|
102
|
+
return;
|
|
103
|
+
// Cumulative across resume sessions — caps bound the whole run, not each session (review #2).
|
|
104
|
+
const snap = this.cumulative();
|
|
105
|
+
// `max_tokens` deliberately bounds CONVERSATION tokens (input + output) only; cache-read /
|
|
106
|
+
// cache-write tokens are tracked + fully billed via costFor() and are bounded by `max_usd`,
|
|
107
|
+
// not by this cap (review #19). totalTokens (snapshot/cumulative) reflects that choice.
|
|
108
|
+
if (this.budget.max_tokens !== undefined && snap.totalTokens > this.budget.max_tokens) {
|
|
109
|
+
throw new AppError(ErrorCode.BUDGET_EXCEEDED, `Token cap exceeded: ${snap.totalTokens.toString()} > ${this.budget.max_tokens.toString()}`, { kind: "tokens", used: snap.totalTokens, cap: this.budget.max_tokens });
|
|
110
|
+
}
|
|
111
|
+
if (this.budget.max_usd !== undefined && snap.totalUsd > this.budget.max_usd) {
|
|
112
|
+
throw new AppError(ErrorCode.BUDGET_EXCEEDED, `USD cap exceeded: $${snap.totalUsd.toFixed(4)} > $${this.budget.max_usd.toFixed(4)}`, { kind: "usd", used: snap.totalUsd, cap: this.budget.max_usd });
|
|
113
|
+
}
|
|
114
|
+
if (this.budget.max_duration_seconds !== undefined) {
|
|
115
|
+
const elapsedSeconds = Math.floor(snap.elapsedMs / 1000);
|
|
116
|
+
if (elapsedSeconds > this.budget.max_duration_seconds) {
|
|
117
|
+
throw new AppError(ErrorCode.BUDGET_EXCEEDED, `Duration cap exceeded: ${elapsedSeconds.toString()}s > ${this.budget.max_duration_seconds.toString()}s`, { kind: "duration", used: elapsedSeconds, cap: this.budget.max_duration_seconds });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// deadline_seconds: WALL-CLOCK from the run's original start, INCLUDING suspended idle (a run
|
|
121
|
+
// resumed past its deadline trips on its first cap check; a long-running one trips per turn).
|
|
122
|
+
// Distinct from max_duration_seconds (active compute) — a long sleep / human-input wait does not
|
|
123
|
+
// burn that, but DOES count here.
|
|
124
|
+
if (this.budget.deadline_seconds !== undefined && this.deadlineStartedAt !== null) {
|
|
125
|
+
const wallSeconds = Math.floor((this.now() - this.deadlineStartedAt) / 1000);
|
|
126
|
+
if (wallSeconds > this.budget.deadline_seconds) {
|
|
127
|
+
throw new AppError(ErrorCode.BUDGET_EXCEEDED, `Deadline exceeded: ${wallSeconds.toString()}s > ${this.budget.deadline_seconds.toString()}s (wall-clock)`, { kind: "deadline", used: wallSeconds, cap: this.budget.deadline_seconds });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Predicate variant for callers that prefer to handle the cap-hit path
|
|
133
|
+
* inline (e.g., the sleep tool rejects a sleep that would breach the
|
|
134
|
+
* duration cap). Returns the first breach reason or null.
|
|
135
|
+
*/
|
|
136
|
+
capBreachReason() {
|
|
137
|
+
try {
|
|
138
|
+
this.assertWithinCaps();
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err instanceof AppError &&
|
|
143
|
+
typeof err.detail === "object" &&
|
|
144
|
+
err.detail !== null &&
|
|
145
|
+
"kind" in err.detail) {
|
|
146
|
+
return err.detail.kind;
|
|
147
|
+
}
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The declared `max_duration_seconds` cap, or null when unset. Lets callers that
|
|
153
|
+
* reason about wall-clock ahead of time (e.g. the sleep tool rejecting a sleep that
|
|
154
|
+
* would overrun the run) read the cap without reaching into the meter's internals.
|
|
155
|
+
*/
|
|
156
|
+
durationCapSeconds() {
|
|
157
|
+
return this.budget?.max_duration_seconds ?? null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* USD cost for a single delta — the representative-rate ESTIMATE used only when the broker reports
|
|
161
|
+
* no real upstream cost for the turn (a BYO-provider turn, or a managed turn whose cost tap missed).
|
|
162
|
+
* Managed turns instead carry OpenRouter's exact `usage.cost` through to {@link addUsage}. Public so
|
|
163
|
+
* the loop can stamp the per-step `cost_usd` column on `run_steps` without re-deriving the rate table.
|
|
164
|
+
*/
|
|
165
|
+
costFor(delta) {
|
|
166
|
+
const inputCost = ((delta.inputTokens ?? 0) * this.rate.inputPerMillion) / 1_000_000;
|
|
167
|
+
const outputCost = ((delta.outputTokens ?? 0) * this.rate.outputPerMillion) / 1_000_000;
|
|
168
|
+
const cacheReadRate = this.rate.cacheReadPerMillion ?? this.rate.inputPerMillion / 10;
|
|
169
|
+
const cacheWriteRate = this.rate.cacheWritePerMillion ?? this.rate.inputPerMillion * 1.25;
|
|
170
|
+
const cacheReadCost = ((delta.cacheReadTokens ?? 0) * cacheReadRate) / 1_000_000;
|
|
171
|
+
const cacheWriteCost = ((delta.cacheWriteTokens ?? 0) * cacheWriteRate) / 1_000_000;
|
|
172
|
+
return inputCost + outputCost + cacheReadCost + cacheWriteCost;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export { runEventSchema, CHANNELS, DEFAULT_CHANNELS, channelOf, matchesChannels, makeCursor, TURN_CURSOR_STRIDE, type RunEvent, type RunEventKind, type Channel, type EventEnvelope, type TokenUsage, type ToolReturn, } from "@boardwalk-labs/workflow";
|
|
2
|
+
import type { RunEvent } from "@boardwalk-labs/workflow";
|
|
3
|
+
/** Error shape on `tool_call_error` and `turn_ended` (reason='error') — the SDK's event error. */
|
|
4
|
+
export interface AgentApiError {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
/** Distributive omit that preserves the discriminated union. */
|
|
9
|
+
type BodyOf<E> = E extends RunEvent ? Omit<E, "runId" | "turnId" | "seq" | "t"> : never;
|
|
10
|
+
/**
|
|
11
|
+
* A run event WITHOUT its envelope — what producers (the normalizer, the phase tracker, the log
|
|
12
|
+
* capture) build. The shared emitter stamps the envelope + computes the cursor.
|
|
13
|
+
*/
|
|
14
|
+
export type RunEventBody = BodyOf<RunEvent>;
|
|
15
|
+
/** The published/stored row: the run-global cursor + the full enveloped event (engine parity). */
|
|
16
|
+
export interface RunEventRow {
|
|
17
|
+
cursor: number;
|
|
18
|
+
event: RunEvent;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Minimal Redis publisher surface (publish only). We depend only on `publish` so tests inject a
|
|
22
|
+
* fake without pulling in ioredis; production wires the broker telemetry publisher.
|
|
23
|
+
*/
|
|
24
|
+
export interface RedisPublisher {
|
|
25
|
+
publish(channel: string, message: string): Promise<number>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The shared per-run emitter surface a turn stamps through (the worker's WorkerRunEventEmitter
|
|
29
|
+
* satisfies it). `emit` stamps the envelope + cursor on a body and rides the current stride block;
|
|
30
|
+
* `beginTurn` opens a new agent stride block, then emits its opening frame (the caller supplies
|
|
31
|
+
* `turn_started`).
|
|
32
|
+
*/
|
|
33
|
+
export interface TurnEventSink {
|
|
34
|
+
emit(body: RunEventBody, turnId?: string): RunEvent;
|
|
35
|
+
beginTurn(turnId: string, started: RunEventBody): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Identity of one agent() leaf — stamped on its `turn_started`/`turn_ended` frames so a stream
|
|
39
|
+
* consumer can tell concurrent agents apart. `agentId` is stable + run-unique (worker-assigned);
|
|
40
|
+
* `agentName` is the author's `AgentOptions.name`, present only when set. Mirrors the engine's
|
|
41
|
+
* `AgentIdentity` shape so the platform's wire matches `boardwalk dev` and the self-hosted server.
|
|
42
|
+
*/
|
|
43
|
+
export interface AgentIdentity {
|
|
44
|
+
agentId: string;
|
|
45
|
+
agentName?: string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Run-event wire format — ADOPTED from the published SDK (flip item, 2026-06-12).
|
|
2
|
+
//
|
|
3
|
+
// The contract lives in `@boardwalk-labs/workflow` (`runEventSchema`, CHANNELS, cursors): one
|
|
4
|
+
// typed, ordered stream per run, identical in every engine. This module re-exports the SDK
|
|
5
|
+
// contract and defines the BODY shape producers build (the envelope — runId / turnId / seq / t —
|
|
6
|
+
// is stamped centrally by the worker's shared RunEventEmitter, which owns the run-global
|
|
7
|
+
// `cursor = turn * TURN_CURSOR_STRIDE + seq` counter exactly like the OSS engine's supervisor).
|
|
8
|
+
export { runEventSchema, CHANNELS, DEFAULT_CHANNELS, channelOf, matchesChannels, makeCursor, TURN_CURSOR_STRIDE, } from "@boardwalk-labs/workflow";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ModelRate } from "./budget.js";
|
|
2
|
+
/**
|
|
3
|
+
* The flat representative rate (USD / million tokens) the BudgetMeter applies to the `max_usd` cap
|
|
4
|
+
* ONLY as the fallback for a turn with no real upstream cost (BYO / unavailable); a managed turn caps
|
|
5
|
+
* on its exact `usage.cost`. Sonnet-class list pricing — a model-agnostic backstop, not the billed price.
|
|
6
|
+
*/
|
|
7
|
+
export declare const BUDGET_GUARDRAIL_RATE: ModelRate;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The flat budget-guardrail rate — the FALLBACK basis for the in-run `max_usd` cap (BudgetMeter).
|
|
2
|
+
//
|
|
3
|
+
// A MANAGED turn now caps on OpenRouter's EXACT per-request `usage.cost`, forwarded from the broker to
|
|
4
|
+
// the worker (inference_proxy result frame → BudgetMeter.addUsage `realCostUsd`), so the cap tracks
|
|
5
|
+
// real spend — already cache-discounted + model-correct. This flat rate is used ONLY when a turn has
|
|
6
|
+
// no upstream cost: a BYO-provider turn (the org pays its own key), or a managed turn whose cost the
|
|
7
|
+
// broker couldn't read. It bounds runaway loops; it is NOT the bill. Actual billing is per-request
|
|
8
|
+
// COST PASS-THROUGH (OpenRouter's reported cost × margin) via Stripe meters (domain/billing +
|
|
9
|
+
// run_usage_service). MASTER_SPEC §15.2.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately NOT a per-model table with a silent fallback: a lookup that returns a Sonnet-class
|
|
12
|
+
// default for any unknown id only created the ILLUSION of precision. The real per-request cost (above)
|
|
13
|
+
// is the precise path; this rate is just the model-agnostic backstop. Un-metered managed models are
|
|
14
|
+
// rejected fail-closed at resolution (domain/inference/model_resolver), so the cap never has to price
|
|
15
|
+
// a model we don't support.
|
|
16
|
+
/**
|
|
17
|
+
* The flat representative rate (USD / million tokens) the BudgetMeter applies to the `max_usd` cap
|
|
18
|
+
* ONLY as the fallback for a turn with no real upstream cost (BYO / unavailable); a managed turn caps
|
|
19
|
+
* on its exact `usage.cost`. Sonnet-class list pricing — a model-agnostic backstop, not the billed price.
|
|
20
|
+
*/
|
|
21
|
+
export const BUDGET_GUARDRAIL_RATE = { inputPerMillion: 3, outputPerMillion: 15 };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Replacement token substituted for any known secret value. Generic on purpose — it must not
|
|
2
|
+
* reveal WHICH secret matched. */
|
|
3
|
+
export declare const REDACTION_PLACEHOLDER = "[REDACTED]";
|
|
4
|
+
/**
|
|
5
|
+
* Values shorter than this are NOT recorded. A 1–3 char "secret" would match common substrings and
|
|
6
|
+
* shred unrelated text; real credentials are long. Mirrors GitHub Actions' masking floor. A value
|
|
7
|
+
* this short should not be a secret in the first place — and the structural guarantee (secrets never
|
|
8
|
+
* flow to the LLM by construction) still holds for it; only the belt-and-suspenders scrub is skipped.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MIN_REDACTABLE_LENGTH = 4;
|
|
11
|
+
export interface SecretRedactorOptions {
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
minLength?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class SecretRedactor {
|
|
16
|
+
/** Recorded secret values, kept sorted longest-first so a longer secret is replaced before any
|
|
17
|
+
* shorter secret that is its substring. */
|
|
18
|
+
private recorded;
|
|
19
|
+
private readonly seen;
|
|
20
|
+
private readonly placeholder;
|
|
21
|
+
private readonly minLength;
|
|
22
|
+
constructor(opts?: SecretRedactorOptions);
|
|
23
|
+
/** Number of distinct values currently being redacted. */
|
|
24
|
+
get size(): number;
|
|
25
|
+
/** A snapshot of the recorded secret values (longest-first). Read by the worker to seed a fresh
|
|
26
|
+
* engine `Redactor` per leaf so the engine loop scrubs the SAME values out of model-bound content. */
|
|
27
|
+
get values(): readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Record a resolved secret value so future LLM-bound content has it scrubbed. No-op for values
|
|
30
|
+
* below the length floor or already known. Idempotent.
|
|
31
|
+
*/
|
|
32
|
+
record(value: string): void;
|
|
33
|
+
/** Replace every occurrence of every known secret value in `text` with the placeholder. */
|
|
34
|
+
redactText(text: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Deep-redact a JSON-shaped value: strings are scrubbed, arrays/objects recursed. Non-string
|
|
37
|
+
* primitives pass through (a secret is always a string). Returns the input unchanged when nothing
|
|
38
|
+
* is recorded, so the common no-secrets case is allocation-free.
|
|
39
|
+
*/
|
|
40
|
+
redactValue(value: unknown): unknown;
|
|
41
|
+
private redactAt;
|
|
42
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SecretRedactor — scrubs known secret values from everything bound for an LLM (MASTER_SPEC §12,
|
|
2
|
+
// docs/WORKFLOW_RUNTIME.md §6.2, plan #9).
|
|
3
|
+
//
|
|
4
|
+
// The architectural guarantee is structural: secrets live ONLY in the workflow PROGRAM (the trusted
|
|
5
|
+
// deterministic tool layer), which holds them via `secrets.get` / `ctx.secrets.resolve`. The
|
|
6
|
+
// `agent()` leaf never receives a secret by design — so prompt injection can't reach one. This
|
|
7
|
+
// redactor is DEFENSE-IN-DEPTH for the path where a program (or a tool result) inadvertently
|
|
8
|
+
// carries a known secret value into LLM context (prompt, system prompt, tool args/results,
|
|
9
|
+
// transcript): every value the run resolves is recorded here, and the leaf scrubs known values out
|
|
10
|
+
// of LLM-bound text before the model ever sees them.
|
|
11
|
+
//
|
|
12
|
+
// Pure logic by design (no I/O, no deps) so it can be tested exhaustively — the platform's secret
|
|
13
|
+
// boundary lives here.
|
|
14
|
+
/** Replacement token substituted for any known secret value. Generic on purpose — it must not
|
|
15
|
+
* reveal WHICH secret matched. */
|
|
16
|
+
export const REDACTION_PLACEHOLDER = "[REDACTED]";
|
|
17
|
+
/**
|
|
18
|
+
* Values shorter than this are NOT recorded. A 1–3 char "secret" would match common substrings and
|
|
19
|
+
* shred unrelated text; real credentials are long. Mirrors GitHub Actions' masking floor. A value
|
|
20
|
+
* this short should not be a secret in the first place — and the structural guarantee (secrets never
|
|
21
|
+
* flow to the LLM by construction) still holds for it; only the belt-and-suspenders scrub is skipped.
|
|
22
|
+
*/
|
|
23
|
+
export const MIN_REDACTABLE_LENGTH = 4;
|
|
24
|
+
/** Bound on recursion into nested tool results. JSON has no cycles, but a pathological structure
|
|
25
|
+
* shouldn't blow the stack; beyond this depth a value can't realistically still be a secret. */
|
|
26
|
+
const MAX_REDACT_DEPTH = 100;
|
|
27
|
+
export class SecretRedactor {
|
|
28
|
+
/** Recorded secret values, kept sorted longest-first so a longer secret is replaced before any
|
|
29
|
+
* shorter secret that is its substring. */
|
|
30
|
+
recorded = [];
|
|
31
|
+
seen = new Set();
|
|
32
|
+
placeholder;
|
|
33
|
+
minLength;
|
|
34
|
+
constructor(opts = {}) {
|
|
35
|
+
this.placeholder = opts.placeholder ?? REDACTION_PLACEHOLDER;
|
|
36
|
+
this.minLength = opts.minLength ?? MIN_REDACTABLE_LENGTH;
|
|
37
|
+
}
|
|
38
|
+
/** Number of distinct values currently being redacted. */
|
|
39
|
+
get size() {
|
|
40
|
+
return this.recorded.length;
|
|
41
|
+
}
|
|
42
|
+
/** A snapshot of the recorded secret values (longest-first). Read by the worker to seed a fresh
|
|
43
|
+
* engine `Redactor` per leaf so the engine loop scrubs the SAME values out of model-bound content. */
|
|
44
|
+
get values() {
|
|
45
|
+
return [...this.recorded];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Record a resolved secret value so future LLM-bound content has it scrubbed. No-op for values
|
|
49
|
+
* below the length floor or already known. Idempotent.
|
|
50
|
+
*/
|
|
51
|
+
record(value) {
|
|
52
|
+
if (value.length < this.minLength)
|
|
53
|
+
return;
|
|
54
|
+
if (this.seen.has(value))
|
|
55
|
+
return;
|
|
56
|
+
this.seen.add(value);
|
|
57
|
+
this.recorded.push(value);
|
|
58
|
+
// Longest-first: replacing the longer match first keeps a shorter substring secret from
|
|
59
|
+
// fragmenting the longer one (e.g. "abc" before "abcdef" would leave "[REDACTED]def").
|
|
60
|
+
this.recorded.sort((a, b) => b.length - a.length);
|
|
61
|
+
}
|
|
62
|
+
/** Replace every occurrence of every known secret value in `text` with the placeholder. */
|
|
63
|
+
redactText(text) {
|
|
64
|
+
if (this.recorded.length === 0)
|
|
65
|
+
return text;
|
|
66
|
+
let out = text;
|
|
67
|
+
for (const v of this.recorded) {
|
|
68
|
+
if (out.includes(v))
|
|
69
|
+
out = out.split(v).join(this.placeholder);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Deep-redact a JSON-shaped value: strings are scrubbed, arrays/objects recursed. Non-string
|
|
75
|
+
* primitives pass through (a secret is always a string). Returns the input unchanged when nothing
|
|
76
|
+
* is recorded, so the common no-secrets case is allocation-free.
|
|
77
|
+
*/
|
|
78
|
+
redactValue(value) {
|
|
79
|
+
if (this.recorded.length === 0)
|
|
80
|
+
return value;
|
|
81
|
+
return this.redactAt(value, 0);
|
|
82
|
+
}
|
|
83
|
+
redactAt(value, depth) {
|
|
84
|
+
if (typeof value === "string")
|
|
85
|
+
return this.redactText(value);
|
|
86
|
+
if (depth >= MAX_REDACT_DEPTH)
|
|
87
|
+
return value;
|
|
88
|
+
if (Array.isArray(value))
|
|
89
|
+
return value.map((v) => this.redactAt(v, depth + 1));
|
|
90
|
+
if (value !== null && typeof value === "object") {
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [k, v] of Object.entries(value))
|
|
93
|
+
out[k] = this.redactAt(v, depth + 1);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ArtifactCommitInput, ArtifactPresignInput, ArtifactPresignResult, ArtifactSignResult, ArtifactStore, ArtifactSummary, ArtifactWriteInput, ArtifactWriteResult } from "./tools/artifacts.js";
|
|
2
|
+
/** The broker surface the store needs (RunnerControlClient satisfies it). */
|
|
3
|
+
export interface ArtifactBrokerTransport {
|
|
4
|
+
writeArtifact(input: ArtifactWriteInput): Promise<ArtifactWriteResult>;
|
|
5
|
+
presignArtifact(input: ArtifactPresignInput): Promise<ArtifactPresignResult>;
|
|
6
|
+
uploadBytes(url: string, headers: Record<string, string>, body: Uint8Array): Promise<void>;
|
|
7
|
+
commitArtifact(input: ArtifactCommitInput): Promise<ArtifactWriteResult>;
|
|
8
|
+
listArtifacts(): Promise<ArtifactSummary[]>;
|
|
9
|
+
signArtifactUrl(artifactId: string, ttlSeconds: number): Promise<ArtifactSignResult>;
|
|
10
|
+
}
|
|
11
|
+
export declare class BrokerArtifactStore implements ArtifactStore {
|
|
12
|
+
private readonly broker;
|
|
13
|
+
constructor(broker: ArtifactBrokerTransport);
|
|
14
|
+
write(input: ArtifactWriteInput): Promise<ArtifactWriteResult>;
|
|
15
|
+
list(): Promise<ArtifactSummary[]>;
|
|
16
|
+
signedUrl(artifactId: string, ttlSeconds: number): Promise<ArtifactSignResult>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// BrokerArtifactStore — the ArtifactStore the `artifacts` tool uses under the Runner Credential
|
|
2
|
+
// Broker (docs/RUNNER_BROKER.md §4 — Artifacts). It forwards every op to the Runner Control API:
|
|
3
|
+
// the broker computes the S3 key, neutralizes the served content type (review #32), PUTs with its
|
|
4
|
+
// own creds, and records the catalog row — so the untrusted runner holds no S3 credential and can't
|
|
5
|
+
// bypass those server-side rules. A thin adapter over the run-bound RunnerControlClient.
|
|
6
|
+
//
|
|
7
|
+
// Writes route by size (docs/RUNNER_BROKER.md §7 step 4): a SMALL body is proxied inline through the
|
|
8
|
+
// broker (`POST .../artifacts`); a LARGE one takes the presigned-PUT path — the broker signs an S3
|
|
9
|
+
// PUT (still owning the key + content-type neutralization + catalog row) and the runner streams the
|
|
10
|
+
// bytes straight to S3, so they never pass through the control plane (no body cap).
|
|
11
|
+
import { decodeArtifactBody, shouldPresignArtifact } from "./wire/artifact_storage.js";
|
|
12
|
+
export class BrokerArtifactStore {
|
|
13
|
+
broker;
|
|
14
|
+
constructor(broker) {
|
|
15
|
+
this.broker = broker;
|
|
16
|
+
}
|
|
17
|
+
async write(input) {
|
|
18
|
+
const bytes = decodeArtifactBody(input.body, input.encoding);
|
|
19
|
+
if (!shouldPresignArtifact(bytes.length)) {
|
|
20
|
+
// Small: proxy the bytes inline (the broker neutralizes + PUTs + catalogs in one round trip).
|
|
21
|
+
return this.broker.writeArtifact(input);
|
|
22
|
+
}
|
|
23
|
+
// Large: presign → PUT bytes straight to S3 → commit. The broker owns the key, content-type
|
|
24
|
+
// neutralization, and the catalog row; only the byte transfer skips the control plane. The row is
|
|
25
|
+
// registered at commit, AFTER the PUT — so a failed upload (uploadBytes throws here) leaves none.
|
|
26
|
+
const presign = await this.broker.presignArtifact({
|
|
27
|
+
name: input.name,
|
|
28
|
+
contentType: input.contentType,
|
|
29
|
+
sizeBytes: bytes.length,
|
|
30
|
+
});
|
|
31
|
+
await this.broker.uploadBytes(presign.uploadUrl, presign.uploadHeaders, bytes);
|
|
32
|
+
return this.broker.commitArtifact({
|
|
33
|
+
s3Key: presign.s3Key,
|
|
34
|
+
name: input.name,
|
|
35
|
+
contentType: input.contentType,
|
|
36
|
+
sizeBytes: bytes.length,
|
|
37
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
list() {
|
|
41
|
+
return this.broker.listArtifacts();
|
|
42
|
+
}
|
|
43
|
+
signedUrl(artifactId, ttlSeconds) {
|
|
44
|
+
return this.broker.signArtifactUrl(artifactId, ttlSeconds);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CallOptions } from "@boardwalk-labs/workflow/runtime";
|
|
2
|
+
import type { ChildDispatcher, ChildResult, ScheduleOptions } from "./workflow_host.js";
|
|
3
|
+
import type { RunnerControlClient } from "./runner_control_client.js";
|
|
4
|
+
export interface BrokerChildDispatcherDeps {
|
|
5
|
+
client: RunnerControlClient;
|
|
6
|
+
/** Wait between polls. Injected so tests don't sleep on real time. Defaults to setTimeout. */
|
|
7
|
+
sleep?: (ms: number) => Promise<void>;
|
|
8
|
+
pollIntervalMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class BrokerChildDispatcher implements ChildDispatcher {
|
|
11
|
+
private readonly deps;
|
|
12
|
+
private readonly sleep;
|
|
13
|
+
private readonly pollIntervalMs;
|
|
14
|
+
constructor(deps: BrokerChildDispatcherDeps);
|
|
15
|
+
/** Create (or re-attach to) the child, then hold + poll to terminal and return its output. An
|
|
16
|
+
* abort (`signal`) stops the hold within one poll interval and throws RunAbortedError. */
|
|
17
|
+
call(slug: string, input: unknown, _opts: CallOptions | undefined, signal?: AbortSignal): Promise<unknown>;
|
|
18
|
+
/** Start (or idempotently re-attach to) the child and resolve its CURRENT state — no hold. The
|
|
19
|
+
* durable callWorkflow seam decides whether to suspend (non-terminal) or return (terminal). */
|
|
20
|
+
start(slug: string, input: unknown, _opts: CallOptions | undefined, signal?: AbortSignal): Promise<ChildResult>;
|
|
21
|
+
/** Poll a child's current state by id (the resume path), or null when it isn't this run's child. */
|
|
22
|
+
poll(childRunId: string): Promise<ChildResult | null>;
|
|
23
|
+
/** Fire-and-forget: create (or re-attach to) the child and return its id without holding. */
|
|
24
|
+
run(slug: string, input: unknown, _opts: CallOptions | undefined): Promise<string>;
|
|
25
|
+
/** Provision a durable schedule via the broker; returns the new schedule's id. A `Date` `at` is
|
|
26
|
+
* normalized to an ISO string (the broker spec carries string | number, not Date). */
|
|
27
|
+
schedule(slug: string, input: unknown, opts: ScheduleOptions): Promise<string>;
|
|
28
|
+
private pollToCompletion;
|
|
29
|
+
/** A completed child returns its output; a failed/cancelled child rejects the parent's await. */
|
|
30
|
+
private childOutput;
|
|
31
|
+
}
|