@relayflows/sdk 2.0.11 → 2.0.13
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/adapters/base.d.ts +6 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +1 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/authored-admission.d.ts +5 -0
- package/dist/authored-admission.d.ts.map +1 -0
- package/dist/authored-admission.js +14 -0
- package/dist/authored-admission.js.map +1 -0
- package/dist/authored-budget.d.ts +1 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +18 -9
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +10 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +15 -7
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +20 -0
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +86 -11
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.d.ts +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +5 -3
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-mcp.d.ts +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +7 -4
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.d.ts +2 -0
- package/dist/authored-node-entry.d.ts.map +1 -0
- package/dist/authored-node-entry.js +111 -0
- package/dist/authored-node-entry.js.map +1 -0
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -0
- package/dist/authored-node-runner.js +246 -0
- package/dist/authored-node-runner.js.map +1 -0
- package/dist/authored-plugin-effect.d.ts +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +7 -4
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-root.d.ts +38 -0
- package/dist/authored-root.d.ts.map +1 -0
- package/dist/authored-root.js +329 -0
- package/dist/authored-root.js.map +1 -0
- package/dist/authored-runtime-capability.d.ts +6 -0
- package/dist/authored-runtime-capability.d.ts.map +1 -0
- package/dist/authored-runtime-capability.js +40 -0
- package/dist/authored-runtime-capability.js.map +1 -0
- package/dist/authored-source-authority.d.ts +4 -0
- package/dist/authored-source-authority.d.ts.map +1 -0
- package/dist/authored-source-authority.js +61 -0
- package/dist/authored-source-authority.js.map +1 -0
- package/dist/authored-worker-step.d.ts +2 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +11 -6
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/budget-preflight.d.ts +28 -3
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +34 -24
- package/dist/budget-preflight.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +8 -0
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check.js +3 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/deploy.d.ts.map +1 -1
- package/dist/cli/deploy.js +5 -0
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +46 -14
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +90 -12
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts +40 -0
- package/dist/cli/step-failure.d.ts.map +1 -0
- package/dist/cli/step-failure.js +150 -0
- package/dist/cli/step-failure.js.map +1 -0
- package/dist/cli-adapter.d.ts +16 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +17 -0
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -8
- package/dist/cli.js.map +1 -1
- package/dist/cloud-run.d.ts +11 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +65 -19
- package/dist/cloud-run.js.map +1 -1
- package/dist/daemon-connection.d.ts +7 -0
- package/dist/daemon-connection.d.ts.map +1 -1
- package/dist/daemon-connection.js +7 -0
- package/dist/daemon-connection.js.map +1 -1
- package/dist/failure-kinds.d.ts +30 -4
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +8 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +3 -7
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +6 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +4 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +2 -2
- package/dist/local-agent.js.map +1 -1
- package/dist/model-pricing.d.ts +7 -6
- package/dist/model-pricing.d.ts.map +1 -1
- package/dist/model-pricing.js +8 -6
- package/dist/model-pricing.js.map +1 -1
- package/dist/preflight.d.ts +9 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +36 -18
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +24 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +27 -11
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +6 -5
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-spend.d.ts +13 -8
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +27 -4
- package/dist/worker-spend.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +4 -2
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/claude.ts +1 -0
- package/src/authored-admission.ts +17 -0
- package/src/authored-budget.ts +38 -11
- package/src/authored-flow-error.ts +0 -1
- package/src/authored-flow-executor.ts +32 -7
- package/src/authored-flow-loader.ts +96 -13
- package/src/authored-helper-effect.ts +5 -3
- package/src/authored-mcp.ts +6 -4
- package/src/authored-node-entry.ts +90 -0
- package/src/authored-node-runner.ts +219 -0
- package/src/authored-plugin-effect.ts +6 -3
- package/src/authored-root.ts +396 -0
- package/src/authored-runtime-capability.ts +40 -0
- package/src/authored-source-authority.ts +62 -0
- package/src/authored-worker-step.ts +15 -4
- package/src/budget-preflight.ts +52 -25
- package/src/cli/build.ts +7 -0
- package/src/cli/check.ts +3 -1
- package/src/cli/deploy.ts +4 -0
- package/src/cli/direct-run.ts +52 -16
- package/src/cli/run.ts +101 -12
- package/src/cli/step-failure.ts +160 -0
- package/src/cli-adapter.ts +27 -0
- package/src/cli.ts +9 -9
- package/src/cloud-run.ts +74 -21
- package/src/daemon-connection.ts +8 -0
- package/src/failure-kinds.ts +28 -2
- package/src/journal-client.ts +8 -3
- package/src/llm-worker.ts +4 -2
- package/src/local-agent.ts +7 -2
- package/src/model-pricing.ts +8 -6
- package/src/preflight.ts +45 -18
- package/src/protocol.ts +17 -2
- package/src/spec.ts +28 -5
- package/src/worker-cli.ts +7 -5
- package/src/worker-spend.ts +32 -5
- package/src/worker.ts +4 -2
- package/dist/cli/deterministic-failure.d.ts +0 -5
- package/dist/cli/deterministic-failure.d.ts.map +0 -1
- package/dist/cli/deterministic-failure.js +0 -66
- package/dist/cli/deterministic-failure.js.map +0 -1
- package/src/cli/deterministic-failure.ts +0 -69
package/src/preflight.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { TriggerSource } from '@relayflows/surface';
|
|
|
8
8
|
import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js';
|
|
9
9
|
import { compileSpec, CompileError } from './compile.js';
|
|
10
10
|
import { helperCall } from './yaml-helpers.js';
|
|
11
|
+
import { resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
|
|
11
12
|
import { isNamedGate, NAMED_GATE_FAILURE_KINDS, type NamedGateFailureKind } from './named-gates.js';
|
|
12
13
|
import { compileScopes, type ScopeInput, type MountRegistry } from './scope-compiler.js';
|
|
13
14
|
import { readMountRegistry } from './mount-registry.js';
|
|
@@ -22,8 +23,10 @@ export interface CliResolution {
|
|
|
22
23
|
stepId: string;
|
|
23
24
|
cli: string;
|
|
24
25
|
source: CliResolutionSource;
|
|
25
|
-
/**
|
|
26
|
+
/** Effective model probed with the CLI. */
|
|
26
27
|
model?: string;
|
|
28
|
+
/** Exact source selected by step > named agent > adapter default priority. */
|
|
29
|
+
modelSource?: CliModelSource;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface CliProbeResult {
|
|
@@ -114,6 +117,11 @@ export type PreflightDiagnostic = PreflightRefusal | PreflightWarning;
|
|
|
114
117
|
export interface PreflightResult {
|
|
115
118
|
plugins?: readonly LoadedPlugin[];
|
|
116
119
|
mcpTools?: Readonly<Record<string, readonly string[]>>;
|
|
120
|
+
/**
|
|
121
|
+
* True when no diagnostic is a refusal. `ok: true` may still carry warnings
|
|
122
|
+
* (e.g. `budget_unmetered`); callers that report to a person must surface
|
|
123
|
+
* `diagnostics`, not just branch on `ok`.
|
|
124
|
+
*/
|
|
117
125
|
ok: boolean;
|
|
118
126
|
gates: StepGateInspection[];
|
|
119
127
|
resolutions: CliResolution[];
|
|
@@ -208,18 +216,17 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
|
|
|
208
216
|
};
|
|
209
217
|
}
|
|
210
218
|
const diagnostics: PreflightDiagnostic[] = [];
|
|
219
|
+
const cliResolutionDiagnostics: PreflightDiagnostic[] = [];
|
|
211
220
|
const resolutions: CliResolution[] = [];
|
|
212
221
|
const resolutionByStep = new Map<string, CliResolution>();
|
|
213
222
|
const cliProbeResults = new Map<string, CliProbeOutcome>();
|
|
214
223
|
|
|
215
|
-
diagnostics.push(...unknownModelDiagnostics(compiled, options));
|
|
216
224
|
diagnostics.push(...scopeDiagnostics(compiled, options));
|
|
217
225
|
for (const server of new Set(options.mcpServers ?? [])) {
|
|
218
226
|
if (options.mcp !== undefined && Object.hasOwn(options.mcp, server)) continue;
|
|
219
227
|
diagnostics.push({ severity: 'refusal', kind: 'mcp_undeclared_server', server,
|
|
220
228
|
message: `MCP server "${server}" is not declared in the nearest flows.json mcp map.` });
|
|
221
229
|
}
|
|
222
|
-
diagnostics.push(...budgetDiagnostics(compiled));
|
|
223
230
|
// Resolve the complete flow before touching any environment fact. A later
|
|
224
231
|
// statically unresolved CLI makes the whole submission impossible, so no
|
|
225
232
|
// earlier command, provider/model, or trigger probe may run first.
|
|
@@ -228,7 +235,7 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
|
|
|
228
235
|
if (step.type === 'agent' && helperCall(step) !== undefined) continue;
|
|
229
236
|
const resolution = resolveCli(step, compiled, options.projectCli);
|
|
230
237
|
if (resolution === undefined) {
|
|
231
|
-
|
|
238
|
+
cliResolutionDiagnostics.push({
|
|
232
239
|
severity: 'refusal',
|
|
233
240
|
kind: 'cli_unresolved',
|
|
234
241
|
stepId: step.id,
|
|
@@ -239,7 +246,19 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
|
|
|
239
246
|
resolutionByStep.set(step.id, resolution);
|
|
240
247
|
}
|
|
241
248
|
}
|
|
242
|
-
|
|
249
|
+
diagnostics.push(...cliResolutionDiagnostics);
|
|
250
|
+
if (cliResolutionDiagnostics.length > 0) {
|
|
251
|
+
return { ok: false, gates: compiled.steps.map(inspectStepGate), resolutions, diagnostics };
|
|
252
|
+
}
|
|
253
|
+
diagnostics.push(...unknownModelDiagnostics(compiled, options, resolutionByStep));
|
|
254
|
+
diagnostics.push(...budgetDiagnostics(
|
|
255
|
+
compiled,
|
|
256
|
+
new Map(resolutions.map(resolution => [resolution.stepId, {
|
|
257
|
+
cli: resolution.cli,
|
|
258
|
+
...(resolution.model === undefined ? {} : { model: resolution.model }),
|
|
259
|
+
}])),
|
|
260
|
+
));
|
|
261
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === 'refusal')) {
|
|
243
262
|
return { ok: false, gates: compiled.steps.map(inspectStepGate), resolutions, diagnostics };
|
|
244
263
|
}
|
|
245
264
|
|
|
@@ -311,6 +330,7 @@ function scopeDiagnostics(flow: FlowSpec, options: PreflightOptions): PreflightR
|
|
|
311
330
|
function unknownModelDiagnostics(
|
|
312
331
|
flow: FlowSpec,
|
|
313
332
|
options: PreflightOptions,
|
|
333
|
+
resolutionByStep: ReadonlyMap<string, CliResolution> = new Map(),
|
|
314
334
|
): PreflightRefusal[] {
|
|
315
335
|
const diagnostics: PreflightRefusal[] = [];
|
|
316
336
|
// model_unknown is a governance check: it exists to enforce a project's
|
|
@@ -340,16 +360,21 @@ function unknownModelDiagnostics(
|
|
|
340
360
|
}
|
|
341
361
|
|
|
342
362
|
for (const step of flow.steps) {
|
|
343
|
-
if (step.type === 'deterministic'
|
|
344
|
-
|
|
345
|
-
|
|
363
|
+
if (step.type === 'deterministic') continue;
|
|
364
|
+
const resolution = resolutionByStep.get(step.id);
|
|
365
|
+
// Selected named declarations were checked once above, including unused
|
|
366
|
+
// declarations. Other sources are step declarations or adapter defaults.
|
|
367
|
+
if (resolution?.modelSource === 'named') continue;
|
|
368
|
+
const model = resolution?.model ?? step.model;
|
|
369
|
+
if (model === undefined || isKnownModel(model, options.models)) continue;
|
|
370
|
+
if (!enforceRegistry) continue;
|
|
346
371
|
diagnostics.push({
|
|
347
372
|
severity: 'refusal',
|
|
348
373
|
kind: 'model_unknown',
|
|
349
374
|
stepId: step.id,
|
|
350
375
|
...(resolution === undefined ? {} : { cli: resolution.cli }),
|
|
351
|
-
model
|
|
352
|
-
message: unknownModelMessage(step.id,
|
|
376
|
+
model,
|
|
377
|
+
message: unknownModelMessage(step.id, model, resolution?.cli, options.modelRegistryPath),
|
|
353
378
|
});
|
|
354
379
|
}
|
|
355
380
|
|
|
@@ -402,14 +427,16 @@ function resolveCli(
|
|
|
402
427
|
const named = step.type === 'agent' && step.agent !== undefined
|
|
403
428
|
? flow.agents?.[step.agent]
|
|
404
429
|
: undefined;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
if (
|
|
430
|
+
const resolved = (cli: string, source: CliResolutionSource): CliResolution => {
|
|
431
|
+
const effectiveModel = resolveCliModelSelection(cli, { step: step.model, named: named?.model });
|
|
432
|
+
return { stepId: step.id, cli, source,
|
|
433
|
+
...(effectiveModel.model === undefined ? {} : { model: effectiveModel.model }),
|
|
434
|
+
...(effectiveModel.source === undefined ? {} : { modelSource: effectiveModel.source }) };
|
|
435
|
+
};
|
|
436
|
+
if (step.cli !== undefined) return resolved(step.cli, 'step');
|
|
437
|
+
if (named !== undefined) return resolved(named.cli, 'named');
|
|
438
|
+
if (flow.cli !== undefined) return resolved(flow.cli, 'flow');
|
|
439
|
+
if (projectCli !== undefined) return resolved(projectCli, 'project');
|
|
413
440
|
return undefined;
|
|
414
441
|
}
|
|
415
442
|
|
package/src/protocol.ts
CHANGED
|
@@ -72,11 +72,26 @@ export interface HelloResult {
|
|
|
72
72
|
export interface StepSpend {
|
|
73
73
|
tokens_input: number;
|
|
74
74
|
tokens_output: number;
|
|
75
|
+
/** Metered dollars; a lower bound when `dollars_unmetered` is set. */
|
|
75
76
|
dollars: number;
|
|
77
|
+
/** Present (true) only when the step spent tokens of unknown dollar cost. */
|
|
78
|
+
dollars_unmetered?: true;
|
|
76
79
|
wallclock_ms: number;
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Worker-reported `step.complete` usage. A priced step reports exact
|
|
84
|
+
* `dollars`; a step whose model has no frozen price reports its tokens with
|
|
85
|
+
* `dollars_unmetered: true` and no dollar amount, so unknown cost is never
|
|
86
|
+
* journaled as a measured $0. See `workerSpend` for the full contract.
|
|
87
|
+
*/
|
|
88
|
+
export type StepUsage =
|
|
89
|
+
| { tokens_in: number; tokens_out: number; dollars: string; dollars_unmetered?: never }
|
|
90
|
+
| { tokens_in: number; tokens_out: number; dollars_unmetered: true; dollars?: never };
|
|
91
|
+
|
|
79
92
|
export interface RunStartParams {
|
|
93
|
+
/** Caller-owned retry identity. Reuse with a different spec is refused. */
|
|
94
|
+
admission_key?: string;
|
|
80
95
|
reuse_from_run_id?: string;
|
|
81
96
|
/**
|
|
82
97
|
* The kernel spec dialect — the ONE boundary shape `RunSpec::parse`
|
|
@@ -128,7 +143,7 @@ export interface RunGetResult {
|
|
|
128
143
|
run_id: string;
|
|
129
144
|
status: RunStatus;
|
|
130
145
|
steps: Record<string, StepSnapshot>;
|
|
131
|
-
budget: { tokens_in: number; tokens_out: number; dollars: string };
|
|
146
|
+
budget: { tokens_in: number; tokens_out: number; dollars: string; dollars_unmetered?: true };
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
export interface RunWatchParams {
|
|
@@ -305,7 +320,7 @@ export interface StepCompleteParams {
|
|
|
305
320
|
idempotency_key: string;
|
|
306
321
|
completionReason: CompletionReason;
|
|
307
322
|
output?: unknown;
|
|
308
|
-
usage?:
|
|
323
|
+
usage?: StepUsage;
|
|
309
324
|
started_pins?: Pins;
|
|
310
325
|
end_pins?: Pins;
|
|
311
326
|
effects?: EffectRef[];
|
package/src/spec.ts
CHANGED
|
@@ -238,10 +238,11 @@ export interface AgentStepSpec extends BaseStepSpec {
|
|
|
238
238
|
/** Inert preflight declaration; overrides the flow/project CLI default. */
|
|
239
239
|
cli?: string;
|
|
240
240
|
/**
|
|
241
|
-
* Model the declared CLI must use.
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
241
|
+
* Model the declared CLI must use. A step declaration wins over its selected
|
|
242
|
+
* named agent and any registered adapter default. Raw Claude/Codex adapters
|
|
243
|
+
* receive the effective model as a real flag; an identified Relayflows
|
|
244
|
+
* wrapper receives an explicitly declared model in its same-process request.
|
|
245
|
+
* The effective choice is journaled instead of being ambient host state.
|
|
245
246
|
*/
|
|
246
247
|
model?: string;
|
|
247
248
|
surfaces?: AgentSurfaces;
|
|
@@ -451,9 +452,31 @@ export interface KernelAgentStep extends KernelStepCommon {
|
|
|
451
452
|
|
|
452
453
|
export type KernelStepSpec = KernelDeterministicStep | KernelLlmStep | KernelAgentStep;
|
|
453
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Accounting carried into a run that continues an earlier one. The authored
|
|
457
|
+
* executor lowers each step to its own kernel run and sends the previous run's
|
|
458
|
+
* totals here. It mirrors the kernel's `PriorSpend` field for field, so the
|
|
459
|
+
* handoff cannot quietly narrow what a carried budget can express.
|
|
460
|
+
*/
|
|
461
|
+
export interface KernelPriorSpend {
|
|
462
|
+
tokens_in: number;
|
|
463
|
+
tokens_out: number;
|
|
464
|
+
/** Metered dollars only; a lower bound when `dollars_unmetered` is set. */
|
|
465
|
+
dollars: string;
|
|
466
|
+
wallclock_ms: number;
|
|
467
|
+
day?: number;
|
|
468
|
+
/**
|
|
469
|
+
* At least one carried charge spent tokens of unknown dollar cost, so
|
|
470
|
+
* `dollars` is a lower bound rather than a measured total. Omitted when
|
|
471
|
+
* false, so a fully metered flow's payload keeps its exact bytes and a
|
|
472
|
+
* kernel that predates the key never receives it.
|
|
473
|
+
*/
|
|
474
|
+
dollars_unmetered?: boolean;
|
|
475
|
+
}
|
|
476
|
+
|
|
454
477
|
export interface KernelBudgetSpec {
|
|
455
478
|
pricing?: 'frozen';
|
|
456
|
-
prior_spend?:
|
|
479
|
+
prior_spend?: KernelPriorSpend;
|
|
457
480
|
max_tokens?: number;
|
|
458
481
|
max_wallclock_ms?: number;
|
|
459
482
|
window?: 'day';
|
package/src/worker-cli.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
agentExecution,
|
|
7
7
|
llmExecution,
|
|
8
8
|
cliAdapterKind,
|
|
9
|
+
resolveCliModel,
|
|
9
10
|
type CliInvocation,
|
|
10
11
|
type CliAdapterKind,
|
|
11
12
|
} from './cli-adapter.js';
|
|
@@ -68,9 +69,10 @@ export async function runAgentCli(
|
|
|
68
69
|
throw new Error('Lease-bound agent execution requires macOS or Linux process-group cancellation; Windows is unsupported.');
|
|
69
70
|
}
|
|
70
71
|
const kind = cliAdapterKind(cli);
|
|
72
|
+
const effectiveModel = resolveCliModel(cli, model);
|
|
71
73
|
|
|
72
74
|
if (mode === 'agent' && transport === 'relay') {
|
|
73
|
-
return runViaAgentRelay(kind, instruction, wakeContext,
|
|
75
|
+
return runViaAgentRelay(kind, instruction, wakeContext, effectiveModel, relayContext, cwd, signal);
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
if (kind === 'relayflows-wrapper-v1') {
|
|
@@ -78,17 +80,17 @@ export async function runAgentCli(
|
|
|
78
80
|
cli,
|
|
79
81
|
instruction,
|
|
80
82
|
wakeContext,
|
|
81
|
-
|
|
83
|
+
effectiveModel,
|
|
82
84
|
wrapperEnvironment(process.env),
|
|
83
85
|
wrapperLimits,
|
|
84
86
|
signal,
|
|
85
|
-
)),
|
|
87
|
+
)), effectiveModel);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
89
91
|
delete env[WAKE_CONTEXT_ENV];
|
|
90
92
|
delete env[MODEL_ENV];
|
|
91
|
-
const invocation = mode === 'llm' ? llmExecution(kind, instruction,
|
|
93
|
+
const invocation = mode === 'llm' ? llmExecution(kind, instruction, effectiveModel) : agentExecution(kind, instruction, effectiveModel);
|
|
92
94
|
|
|
93
95
|
if (wakeContext !== undefined) {
|
|
94
96
|
try {
|
|
@@ -106,7 +108,7 @@ export async function runAgentCli(
|
|
|
106
108
|
// Structured provider output carries the authoritative token counts.
|
|
107
109
|
const args = [...invocation.args];
|
|
108
110
|
args.splice(args.length - 1, 0, ...(kind === 'claude' ? ['--output-format', 'json'] : ['--json']));
|
|
109
|
-
return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd), kind),
|
|
111
|
+
return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd), kind), effectiveModel);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/** Wait under the same worker lease for an authoritative task receipt. */
|
package/src/worker-spend.ts
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
import { pricedUsage } from './model-pricing.js';
|
|
2
|
+
import type { StepUsage } from './protocol.js';
|
|
2
3
|
import type { WorkerCliResult } from './worker-cli.js';
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Usage for a step whose dollar cost is unknown (unpriced or undeclared model).
|
|
7
|
+
*
|
|
8
|
+
* It carries the reported tokens and `dollars_unmetered: true`, and never a
|
|
9
|
+
* `dollars` amount: unknown cost is not journaled as a measured $0. The kernel
|
|
10
|
+
* records the flag on the step's `budget`/`spend` and on the run total, counts
|
|
11
|
+
* the tokens toward token ceilings, and compares only metered dollars against
|
|
12
|
+
* `maxDollars`. A CLI that reported no tokens still marks the step unmetered,
|
|
13
|
+
* with zero tokens — the same token accounting any unreported step gets.
|
|
14
|
+
*/
|
|
15
|
+
function unmeteredUsage(input: number | undefined, output: number | undefined): StepUsage {
|
|
16
|
+
return { tokens_in: input ?? 0, tokens_out: output ?? 0, dollars_unmetered: true };
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
/**
|
|
5
20
|
* Attach token/dollar usage to a worker's CLI result. Invalid token counts
|
|
6
21
|
* (non-integer or negative) are the one remaining failure mode — those are
|
|
7
22
|
* journaled as `worker_error` with the usage projected from clamped counts.
|
|
8
23
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
24
|
+
* Contract across preflight → worker → kernel (keep all three aligned):
|
|
25
|
+
* - preflight (`budgetDiagnostics`) warns `budget_unmetered` for a
|
|
26
|
+
* dollar-budgeted step with no frozen price and lets it run;
|
|
27
|
+
* - this function returns priced usage (`dollars`) for a priced model, and
|
|
28
|
+
* unmetered usage (`dollars_unmetered: true`, no `dollars`) otherwise —
|
|
29
|
+
* never `undefined` for a successful decode, so token ceilings always see the
|
|
30
|
+
* step and the journal always says whether its dollars are known;
|
|
31
|
+
* - the kernel (`machine/budget.rs`) enforces tokens on every charge and
|
|
32
|
+
* dollars on metered charges only, and rejects unmetered usage that also
|
|
33
|
+
* claims non-zero dollars.
|
|
34
|
+
* `tests/budget-unmetered-live.test.ts` pins the chain end to end.
|
|
12
35
|
*/
|
|
13
|
-
export function workerSpend(result: WorkerCliResult, model?: string) {
|
|
14
|
-
try {
|
|
36
|
+
export function workerSpend(result: WorkerCliResult, model?: string): { result: WorkerCliResult; usage: StepUsage | undefined } {
|
|
37
|
+
try {
|
|
38
|
+
const usage = pricedUsage(model, result.tokens_input, result.tokens_output)
|
|
39
|
+
?? unmeteredUsage(result.tokens_input, result.tokens_output);
|
|
40
|
+
return { result, usage };
|
|
41
|
+
}
|
|
15
42
|
catch (error) {
|
|
16
43
|
return {
|
|
17
44
|
result: { ...result, exit_code: null, stderr_tail: error instanceof Error ? error.message : 'Invalid model usage' },
|
package/src/worker.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { JournalClient } from './journal-client.js';
|
|
|
5
5
|
import type { Pins, StepDispatchEvent } from './protocol.js';
|
|
6
6
|
import type { KernelAgentStep } from './spec.js';
|
|
7
7
|
import { runAgentCli } from './worker-cli.js';
|
|
8
|
+
import { resolveCliModel } from './cli-adapter.js';
|
|
8
9
|
import { withWorkerLease } from './worker-lease.js';
|
|
9
10
|
import { workerInstruction } from './worker-input.js';
|
|
10
11
|
import { helperCall } from './yaml-helpers.js';
|
|
@@ -105,9 +106,10 @@ export class AgentWorker extends EventEmitter {
|
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
107
108
|
let humanIntervention = false;
|
|
109
|
+
const effectiveModel = typeof spec.cli === 'string' ? resolveCliModel(spec.cli, spec.model) : spec.model;
|
|
108
110
|
const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal =>
|
|
109
111
|
typeof spec.cli === 'string' && typeof spec.instruction === 'string'
|
|
110
|
-
? runAgentCli(spec.cli, workerInstruction(spec.instruction, dispatch), dispatch.wake_context,
|
|
112
|
+
? runAgentCli(spec.cli, workerInstruction(spec.instruction, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'agent', this.options.dataDir === undefined ? undefined : {
|
|
111
113
|
dataDir: this.options.dataDir, runId: dispatch.run_id, stepId: dispatch.step_id,
|
|
112
114
|
onReady: this.options.onPtyReady, onDrive: () => { humanIntervention = true; },
|
|
113
115
|
}, typeof spec.cwd === 'string' ? spec.cwd : undefined,
|
|
@@ -115,7 +117,7 @@ export class AgentWorker extends EventEmitter {
|
|
|
115
117
|
{ runId: dispatch.run_id, stepId: dispatch.step_id, idempotencyKey: dispatch.idempotency_key,
|
|
116
118
|
dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema })
|
|
117
119
|
: Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'agent step has no declared CLI' }));
|
|
118
|
-
const { result, usage } = workerSpend(completed,
|
|
120
|
+
const { result, usage } = workerSpend(completed, effectiveModel);
|
|
119
121
|
const completionReason = result.exit_code === 0 ? 'success' : 'worker_error';
|
|
120
122
|
|
|
121
123
|
// Output shape: if the CLI's stdout parses as JSON, promote THAT
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { StepFailedDetails } from '../failure-kinds.js';
|
|
2
|
-
import type { JournalClient } from '../journal-client.js';
|
|
3
|
-
/** Read completion evidence through the existing protocol, including later pages. */
|
|
4
|
-
export declare function deterministicFailureDetails(client: JournalClient, runId: string): Promise<StepFailedDetails | undefined>;
|
|
5
|
-
//# sourceMappingURL=deterministic-failure.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deterministic-failure.d.ts","sourceRoot":"","sources":["../../src/cli/deterministic-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D,qFAAqF;AACrF,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAqCxC"}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
const STDERR_TAIL_BYTES = 1_024;
|
|
2
|
-
/** Read completion evidence through the existing protocol, including later pages. */
|
|
3
|
-
export async function deterministicFailureDetails(client, runId) {
|
|
4
|
-
const snapshot = await client.runGet(runId);
|
|
5
|
-
if (!Object.values(snapshot.steps).some(step => step.type === 'deterministic'))
|
|
6
|
-
return undefined;
|
|
7
|
-
let fromSeq = 1;
|
|
8
|
-
const failures = new Map();
|
|
9
|
-
while (true) {
|
|
10
|
-
const { entries } = await client.journalRead(runId, fromSeq, 100);
|
|
11
|
-
if (entries.length === 0)
|
|
12
|
-
break;
|
|
13
|
-
for (const raw of entries) {
|
|
14
|
-
const entry = record(raw);
|
|
15
|
-
const seq = entry?.['seq'];
|
|
16
|
-
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < fromSeq) {
|
|
17
|
-
throw new Error('invalid journal sequence in command failure inspection');
|
|
18
|
-
}
|
|
19
|
-
fromSeq = seq + 1;
|
|
20
|
-
const stepId = entry?.['step_id'];
|
|
21
|
-
if (entry?.['entry_type'] !== 'step.completed' || typeof stepId !== 'string'
|
|
22
|
-
|| snapshot.steps[stepId]?.type !== 'deterministic')
|
|
23
|
-
continue;
|
|
24
|
-
// A later completion supersedes an earlier failed attempt.
|
|
25
|
-
failures.delete(stepId);
|
|
26
|
-
const payload = record(entry['payload']);
|
|
27
|
-
// Post-#292 the kernel preserves the captured `{exit_code, stdout_tail,
|
|
28
|
-
// stderr_tail}` in `output` on failed completions; prefer it. Fall back
|
|
29
|
-
// to `trajectory_tail` for journal records emitted before that fix.
|
|
30
|
-
const output = record(payload?.['output']) ?? record(payload?.['trajectory_tail']);
|
|
31
|
-
const exitCode = output?.['exit_code'];
|
|
32
|
-
if (payload?.['disposition'] !== 'step_done' || payload['completionReason'] === 'success'
|
|
33
|
-
|| typeof exitCode !== 'number' || !Number.isSafeInteger(exitCode) || exitCode === 0)
|
|
34
|
-
continue;
|
|
35
|
-
failures.set(stepId, {
|
|
36
|
-
stepId,
|
|
37
|
-
exitCode,
|
|
38
|
-
stderrTail: stderrTail(output?.['stderr_tail']),
|
|
39
|
-
hint: `flows replay ${shellQuote(runId)} --at ${shellQuote(stepId)}`,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return [...failures.values()].at(-1);
|
|
44
|
-
}
|
|
45
|
-
function record(value) {
|
|
46
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
47
|
-
? value : undefined;
|
|
48
|
-
}
|
|
49
|
-
function stderrTail(value) {
|
|
50
|
-
if (typeof value !== 'string')
|
|
51
|
-
return '';
|
|
52
|
-
const bytes = Buffer.from(value, 'utf8');
|
|
53
|
-
let start = Math.max(0, bytes.length - STDERR_TAIL_BYTES);
|
|
54
|
-
// Drop a partial leading code point, avoiding replacement-byte expansion.
|
|
55
|
-
while (start < bytes.length && (bytes[start] & 0xc0) === 0x80)
|
|
56
|
-
start += 1;
|
|
57
|
-
// Preserve tabs/newlines; replace binary controls (including ESC and CR),
|
|
58
|
-
// C1 controls and Unicode formatting controls without growing the excerpt.
|
|
59
|
-
return bytes.subarray(start).toString('utf8')
|
|
60
|
-
.replace(/[\p{Cc}\p{Cf}]/gu, character => character === '\n' || character === '\t' ? character : '?');
|
|
61
|
-
}
|
|
62
|
-
function shellQuote(value) {
|
|
63
|
-
return /^[A-Za-z0-9_-]+$/.test(value) && !value.startsWith('-')
|
|
64
|
-
? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
65
|
-
}
|
|
66
|
-
//# sourceMappingURL=deterministic-failure.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deterministic-failure.js","sourceRoot":"","sources":["../../src/cli/deterministic-failure.ts"],"names":[],"mappings":"AAGA,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,MAAqB,EACrB,KAAa;IAEb,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC;QAAE,OAAO,SAAS,CAAC;IACjG,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6B,CAAC;IACtD,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QAClE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM;QAChC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,MAAM,GAAG,GAAG,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;YAC3B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,OAAO,EAAE,CAAC;gBAC3E,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;YAC5E,CAAC;YACD,OAAO,GAAG,GAAG,GAAG,CAAC,CAAC;YAClB,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,SAAS,CAAC,CAAC;YAClC,IAAI,KAAK,EAAE,CAAC,YAAY,CAAC,KAAK,gBAAgB,IAAI,OAAO,MAAM,KAAK,QAAQ;mBACvE,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,KAAK,eAAe;gBAAE,SAAS;YAChE,2DAA2D;YAC3D,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACxB,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YACzC,wEAAwE;YACxE,wEAAwE;YACxE,oEAAoE;YACpE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC;YACnF,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,WAAW,CAAC,CAAC;YACvC,IAAI,OAAO,EAAE,CAAC,aAAa,CAAC,KAAK,WAAW,IAAI,OAAO,CAAC,kBAAkB,CAAC,KAAK,SAAS;mBACpF,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,QAAQ,KAAK,CAAC;gBAAE,SAAS;YACjG,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE;gBACnB,MAAM;gBACN,QAAQ;gBACR,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,aAAa,CAAC,CAAC;gBAC/C,IAAI,EAAE,gBAAgB,UAAU,CAAC,KAAK,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,EAAE;aACrE,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACzE,CAAC,CAAC,KAAgC,CAAC,CAAC,CAAC,SAAS,CAAC;AACnD,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACzC,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,iBAAiB,CAAC,CAAC;IAC1D,0EAA0E;IAC1E,OAAO,KAAK,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAE,GAAG,IAAI,CAAC,KAAK,IAAI;QAAE,KAAK,IAAI,CAAC,CAAC;IAC3E,0EAA0E;IAC1E,2EAA2E;IAC3E,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;SAC1C,OAAO,CAAC,kBAAkB,EAAE,SAAS,CAAC,EAAE,CAAC,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAC1G,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAC7D,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AAClD,CAAC"}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { StepFailedDetails } from '../failure-kinds.js';
|
|
2
|
-
import type { JournalClient } from '../journal-client.js';
|
|
3
|
-
|
|
4
|
-
const STDERR_TAIL_BYTES = 1_024;
|
|
5
|
-
|
|
6
|
-
/** Read completion evidence through the existing protocol, including later pages. */
|
|
7
|
-
export async function deterministicFailureDetails(
|
|
8
|
-
client: JournalClient,
|
|
9
|
-
runId: string,
|
|
10
|
-
): Promise<StepFailedDetails | undefined> {
|
|
11
|
-
const snapshot = await client.runGet(runId);
|
|
12
|
-
if (!Object.values(snapshot.steps).some(step => step.type === 'deterministic')) return undefined;
|
|
13
|
-
let fromSeq = 1;
|
|
14
|
-
const failures = new Map<string, StepFailedDetails>();
|
|
15
|
-
while (true) {
|
|
16
|
-
const { entries } = await client.journalRead(runId, fromSeq, 100);
|
|
17
|
-
if (entries.length === 0) break;
|
|
18
|
-
for (const raw of entries) {
|
|
19
|
-
const entry = record(raw);
|
|
20
|
-
const seq = entry?.['seq'];
|
|
21
|
-
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < fromSeq) {
|
|
22
|
-
throw new Error('invalid journal sequence in command failure inspection');
|
|
23
|
-
}
|
|
24
|
-
fromSeq = seq + 1;
|
|
25
|
-
const stepId = entry?.['step_id'];
|
|
26
|
-
if (entry?.['entry_type'] !== 'step.completed' || typeof stepId !== 'string'
|
|
27
|
-
|| snapshot.steps[stepId]?.type !== 'deterministic') continue;
|
|
28
|
-
// A later completion supersedes an earlier failed attempt.
|
|
29
|
-
failures.delete(stepId);
|
|
30
|
-
const payload = record(entry['payload']);
|
|
31
|
-
// Post-#292 the kernel preserves the captured `{exit_code, stdout_tail,
|
|
32
|
-
// stderr_tail}` in `output` on failed completions; prefer it. Fall back
|
|
33
|
-
// to `trajectory_tail` for journal records emitted before that fix.
|
|
34
|
-
const output = record(payload?.['output']) ?? record(payload?.['trajectory_tail']);
|
|
35
|
-
const exitCode = output?.['exit_code'];
|
|
36
|
-
if (payload?.['disposition'] !== 'step_done' || payload['completionReason'] === 'success'
|
|
37
|
-
|| typeof exitCode !== 'number' || !Number.isSafeInteger(exitCode) || exitCode === 0) continue;
|
|
38
|
-
failures.set(stepId, {
|
|
39
|
-
stepId,
|
|
40
|
-
exitCode,
|
|
41
|
-
stderrTail: stderrTail(output?.['stderr_tail']),
|
|
42
|
-
hint: `flows replay ${shellQuote(runId)} --at ${shellQuote(stepId)}`,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return [...failures.values()].at(-1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function record(value: unknown): Record<string, unknown> | undefined {
|
|
50
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
51
|
-
? value as Record<string, unknown> : undefined;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function stderrTail(value: unknown): string {
|
|
55
|
-
if (typeof value !== 'string') return '';
|
|
56
|
-
const bytes = Buffer.from(value, 'utf8');
|
|
57
|
-
let start = Math.max(0, bytes.length - STDERR_TAIL_BYTES);
|
|
58
|
-
// Drop a partial leading code point, avoiding replacement-byte expansion.
|
|
59
|
-
while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
|
|
60
|
-
// Preserve tabs/newlines; replace binary controls (including ESC and CR),
|
|
61
|
-
// C1 controls and Unicode formatting controls without growing the excerpt.
|
|
62
|
-
return bytes.subarray(start).toString('utf8')
|
|
63
|
-
.replace(/[\p{Cc}\p{Cf}]/gu, character => character === '\n' || character === '\t' ? character : '?');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function shellQuote(value: string): string {
|
|
67
|
-
return /^[A-Za-z0-9_-]+$/.test(value) && !value.startsWith('-')
|
|
68
|
-
? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
69
|
-
}
|