@boardwalk-labs/runner 0.2.15 → 0.2.16
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/README.md
CHANGED
|
@@ -31,7 +31,9 @@ boardwalk-runner start --url https://api.boardwalk.sh --pool default
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
`start` polls for runs targeting `runs_on: { kind: "self-hosted" }` and executes one at a
|
|
34
|
-
time; run more daemons (or machines) for concurrency.
|
|
34
|
+
time; run more daemons (or machines) for concurrency. Each run executes in a container by default
|
|
35
|
+
(Docker or Podman required); pass `--host` to run it directly on the machine instead. `Ctrl-C`
|
|
36
|
+
drains: the current run
|
|
35
37
|
finishes, nothing new is claimed. Useful flags: `--once` (execute one run, then exit),
|
|
36
38
|
`--verbose` (debug-level daemon logs), `--debug` (also debug-logs inside each run process),
|
|
37
39
|
`--work-dir`, `--identity-dir`. Behind a corporate proxy, launch with `NODE_USE_ENV_PROXY=1`
|
|
@@ -58,7 +60,7 @@ runs every non-browser workflow exactly as before.
|
|
|
58
60
|
|
|
59
61
|
## Security model
|
|
60
62
|
|
|
61
|
-
This part of the contract is settled
|
|
63
|
+
This part of the contract is settled:
|
|
62
64
|
|
|
63
65
|
- The runner **never receives broad credentials**: a single-purpose registration token to join,
|
|
64
66
|
a standing runner token that can only poll/claim/heartbeat, and a short-lived **run-scoped**
|
|
@@ -87,7 +89,7 @@ pnpm lint && pnpm typecheck && pnpm build
|
|
|
87
89
|
- [`sdk`](https://github.com/boardwalk-labs/sdk) — `@boardwalk-labs/workflow`, the TypeScript API a workflow program imports
|
|
88
90
|
- [`cli`](https://github.com/boardwalk-labs/cli) — `boardwalk`: scaffold, validate, run locally, deploy
|
|
89
91
|
- [`examples`](https://github.com/boardwalk-labs/examples) — copyable workflow templates (`boardwalk init --template`)
|
|
90
|
-
- [`plugins`](https://github.com/boardwalk-labs/plugins) — skills
|
|
92
|
+
- [`plugins`](https://github.com/boardwalk-labs/plugins) — coding-agent skills (Claude Code, Codex, Cursor, OpenClaw, OpenCode) + a control-plane MCP server
|
|
91
93
|
- [`runner-images`](https://github.com/boardwalk-labs/runner-images) — reproducible base images hosted runners execute in
|
|
92
94
|
|
|
93
95
|
Hosted platform and docs: [boardwalk.sh](https://boardwalk.sh).
|
|
@@ -83,6 +83,11 @@ export class EngineLeafExecutor {
|
|
|
83
83
|
// io (children get their own via `forChild`), and the loop is strictly streamModel→reportUsage per
|
|
84
84
|
// turn, so a single slot never races. Null ⇒ no upstream cost for the next turn (BYO / unavailable).
|
|
85
85
|
let pendingCostMicros = null;
|
|
86
|
+
// The turn currently streaming, tracked so `streamModel` can attribute a `turn_reset` to it when
|
|
87
|
+
// the broker restarts a dropped model call mid-stream. Set on `startTurn` and every `emit` (the
|
|
88
|
+
// engine stamps both with the turn's id); within a leaf, turns are strictly sequential, so this is
|
|
89
|
+
// never stale for the turn actually streaming.
|
|
90
|
+
let currentTurnId = undefined;
|
|
86
91
|
return {
|
|
87
92
|
identity,
|
|
88
93
|
redactor,
|
|
@@ -113,16 +118,24 @@ export class EngineLeafExecutor {
|
|
|
113
118
|
throwIfAborted(signal);
|
|
114
119
|
return this.streamModel(req, providerIo, signal, redactor, (costMicros) => {
|
|
115
120
|
pendingCostMicros = (pendingCostMicros ?? 0) + costMicros;
|
|
121
|
+
},
|
|
122
|
+
// The broker restarted a dropped model call after content had already streamed: void the
|
|
123
|
+
// turn's emitted text/reasoning so a viewer re-renders from the restart, not the concatenation.
|
|
124
|
+
() => {
|
|
125
|
+
if (currentTurnId !== undefined)
|
|
126
|
+
sink.emit({ kind: "turn_reset" }, currentTurnId);
|
|
116
127
|
});
|
|
117
128
|
},
|
|
118
129
|
// turn_started rides a NEW stride block (the supervisor opens it); subsequent leaf events ride
|
|
119
130
|
// the same block. The shared emitter owns the run-global cursor.
|
|
120
131
|
startTurn: (turnId) => {
|
|
132
|
+
currentTurnId = turnId;
|
|
121
133
|
sink.beginTurn(turnId, { kind: "turn_started", ...identity });
|
|
122
134
|
},
|
|
123
135
|
// Engine LeafEventBody → the platform's v1 RunEventBody. The platform already adopted the SDK
|
|
124
136
|
// v1 wire format, and the engine emits the same kinds, so this is a near-identity mapping.
|
|
125
137
|
emit: (turnId, body) => {
|
|
138
|
+
currentTurnId = turnId;
|
|
126
139
|
sink.emit(toRunEventBody(body, identity), turnId);
|
|
127
140
|
},
|
|
128
141
|
// Usage flows to the budget authority after EVERY model call. Feed the run-level meter and, if
|
|
@@ -202,7 +215,7 @@ export class EngineLeafExecutor {
|
|
|
202
215
|
/** POST one model turn to the broker's `/inference` and adapt its NDJSON stream into the engine's
|
|
203
216
|
* ModelTurnResult: each `delta` frame drives providerIo.onDelta; the terminal `result` frame is
|
|
204
217
|
* the turn. An `error` frame throws (the broker already classified it). An abort mid-stream throws. */
|
|
205
|
-
async streamModel(req, providerIo, signal, redactor, onCost) {
|
|
218
|
+
async streamModel(req, providerIo, signal, redactor, onCost, onReset) {
|
|
206
219
|
// Runner-direct BYO (D7): the org's own endpoint + key, called with the same engine adapters
|
|
207
220
|
// the broker uses. No platform cost (BYO is never metered) — onCost stays untouched.
|
|
208
221
|
// A direct turn needs an explicit model (an omitted model means the managed auto lane,
|
|
@@ -240,6 +253,12 @@ export class EngineLeafExecutor {
|
|
|
240
253
|
else if (frame.kind === "reasoning") {
|
|
241
254
|
providerIo.onReasoningDelta?.(frame.text);
|
|
242
255
|
}
|
|
256
|
+
else if (frame.kind === "reset") {
|
|
257
|
+
// The broker recovered a transient mid-stream drop and is restarting the turn: everything
|
|
258
|
+
// relayed above is void. Signal the viewer to discard the turn's streamed text/reasoning; the
|
|
259
|
+
// authoritative turn still comes from the single terminal `result` frame below.
|
|
260
|
+
onReset?.();
|
|
261
|
+
}
|
|
243
262
|
else if (frame.kind === "result") {
|
|
244
263
|
// contextTokens (when the broker knows the served model's window) lets the engine's loop
|
|
245
264
|
// size compaction against the real window instead of a conservative default.
|
|
@@ -32,13 +32,17 @@ export interface ProxyError {
|
|
|
32
32
|
}
|
|
33
33
|
/** A parsed response frame. `ping` is a no-payload heartbeat the broker emits during a long model
|
|
34
34
|
* turn to keep the connection producing bytes (so idle/body timeouts don't sever it); the worker
|
|
35
|
-
* ignores it.
|
|
35
|
+
* ignores it. `reset` voids every delta/reasoning relayed so far for the in-progress turn — the
|
|
36
|
+
* broker restarts a turn after a transient mid-stream drop; the worker signals the viewer to discard
|
|
37
|
+
* the turn's streamed output (the authoritative turn still arrives in the single `result`). */
|
|
36
38
|
export type InferenceFrame = {
|
|
37
39
|
kind: "delta";
|
|
38
40
|
text: string;
|
|
39
41
|
} | {
|
|
40
42
|
kind: "reasoning";
|
|
41
43
|
text: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "reset";
|
|
42
46
|
} | {
|
|
43
47
|
kind: "result";
|
|
44
48
|
turn: ChatTurn;
|
|
@@ -70,6 +74,10 @@ export declare function serializeReasoningFrame(text: string): string;
|
|
|
70
74
|
export declare function serializeResultFrame(turn: ChatTurn, modelRef: string, costMicros?: number, contextTokens?: number): string;
|
|
71
75
|
/** Serialize a terminal error as a single NDJSON line. */
|
|
72
76
|
export declare function serializeErrorFrame(error: ProxyError): string;
|
|
77
|
+
/** Serialize a `reset` frame (no payload): the broker restarted a turn after a transient mid-stream
|
|
78
|
+
* drop, so every delta/reasoning relayed so far is void. The broker is the producer; this mirror
|
|
79
|
+
* exists so runner tests can construct the frame. */
|
|
80
|
+
export declare function serializeResetFrame(): string;
|
|
73
81
|
/** Serialize a heartbeat (no payload) as a single NDJSON line. The broker emits these on an interval
|
|
74
82
|
* during a long model turn so the worker↔broker connection keeps producing bytes — neither side's
|
|
75
83
|
* idle/body timeout fires while the model is generating but not yet streaming text. */
|
|
@@ -100,6 +100,12 @@ export function serializeResultFrame(turn, modelRef, costMicros = 0, contextToke
|
|
|
100
100
|
export function serializeErrorFrame(error) {
|
|
101
101
|
return `${JSON.stringify({ t: "error", error })}\n`;
|
|
102
102
|
}
|
|
103
|
+
/** Serialize a `reset` frame (no payload): the broker restarted a turn after a transient mid-stream
|
|
104
|
+
* drop, so every delta/reasoning relayed so far is void. The broker is the producer; this mirror
|
|
105
|
+
* exists so runner tests can construct the frame. */
|
|
106
|
+
export function serializeResetFrame() {
|
|
107
|
+
return `${JSON.stringify({ t: "reset" })}\n`;
|
|
108
|
+
}
|
|
103
109
|
/** Serialize a heartbeat (no payload) as a single NDJSON line. The broker emits these on an interval
|
|
104
110
|
* during a long model turn so the worker↔broker connection keeps producing bytes — neither side's
|
|
105
111
|
* idle/body timeout fires while the model is generating but not yet streaming text. */
|
|
@@ -133,6 +139,8 @@ export function parseInferenceFrame(line) {
|
|
|
133
139
|
};
|
|
134
140
|
case "error":
|
|
135
141
|
return { kind: "error", error: toProxyError(rec.error) };
|
|
142
|
+
case "reset":
|
|
143
|
+
return { kind: "reset" };
|
|
136
144
|
case "ping":
|
|
137
145
|
return { kind: "ping" };
|
|
138
146
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boardwalk-labs/runner",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Boardwalk self-hosted runner: execute cloud-scheduled runs on your own machines. Currently: the canonical runner contract types.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@boardwalk-labs/engine": "0.2.12",
|
|
57
|
-
"@boardwalk-labs/workflow": "0.2.
|
|
57
|
+
"@boardwalk-labs/workflow": "0.2.6",
|
|
58
58
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
59
|
"node-gyp-build": "^4.8.4",
|
|
60
60
|
"tar": "^7.5.16",
|