pi-subagents 0.57.0 → 0.59.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/CHANGELOG.md +104 -0
- package/docs/agents.md +18 -9
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +40 -1
- package/docs/models.md +27 -4
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +92 -6
- package/docs/workflows.md +115 -0
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +1 -1
- package/skills/pi-subagents/SKILL.md +3 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
- package/src/agents/agent-management.ts +28 -17
- package/src/agents/agent-serializer.ts +9 -2
- package/src/agents/agents.ts +103 -27
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +19 -18
- package/src/api/agents.ts +10 -5
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +32 -10
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +66 -27
- package/src/extension/public-execution.ts +15 -2
- package/src/extension/rpc.ts +35 -14
- package/src/extension/schemas.ts +37 -12
- package/src/extension/tool-description.ts +25 -6
- package/src/integrations/herdr-status.ts +5 -0
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +78 -46
- package/src/runs/background/async-job-tracker.ts +14 -12
- package/src/runs/background/async-resume.ts +29 -27
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +68 -7
- package/src/runs/background/chain-append.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +184 -9
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +29 -10
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +18 -8
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +389 -350
- package/src/runs/background/subagent-wait.ts +58 -10
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +22 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +134 -121
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +527 -257
- package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +463 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +5 -4
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +422 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
- package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
- package/src/runs/shared/model-exclusions.ts +12 -0
- package/src/runs/shared/model-fallback.ts +25 -6
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +15 -1
- package/src/runs/shared/pi-args.ts +81 -8
- package/src/runs/shared/single-output.ts +44 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +11 -2
- package/src/shared/launch-contract.ts +5 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +240 -30
- package/src/shared/utils.ts +41 -84
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/fleet-status.ts +125 -74
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +330 -33
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +230 -0
- package/src/workflows/scripted-workflow.ts +496 -35
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +79 -5
- package/src/workflows/workflow-settlement.ts +246 -0
- package/src/runs/shared/turn-budget.ts +0 -98
|
@@ -2,6 +2,10 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { Worker } from "node:worker_threads";
|
|
5
|
+
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/parallel-utils.ts";
|
|
6
|
+
import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
|
|
7
|
+
import type { HostStepNodeV1, SingleResult } from "../shared/types.ts";
|
|
8
|
+
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
5
9
|
|
|
6
10
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
7
11
|
const requireFromPackage = createRequire(import.meta.url);
|
|
@@ -252,11 +256,11 @@ function hostCall(method, args, observation) {
|
|
|
252
256
|
: promise;
|
|
253
257
|
}
|
|
254
258
|
|
|
255
|
-
function runHostCall(key, params, collectFailure, batch) {
|
|
259
|
+
function runHostCall(key, params, collectFailure, batch, generatedLaneKey) {
|
|
256
260
|
const callId = ++nextCallId;
|
|
257
261
|
const promise = new Promise((resolve, reject) => {
|
|
258
262
|
pending.set(callId, { resolve, reject });
|
|
259
|
-
parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}), ...(batch ? { batch } : {}) } });
|
|
263
|
+
parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}), ...(batch ? { batch } : {}), ...(generatedLaneKey ? { generatedLaneKey } : {}) } });
|
|
260
264
|
});
|
|
261
265
|
return { key, callId, promise };
|
|
262
266
|
}
|
|
@@ -269,6 +273,15 @@ function isArrayIndexProperty(prop) {
|
|
|
269
273
|
|
|
270
274
|
const runsAllResultTargets = new WeakMap();
|
|
271
275
|
|
|
276
|
+
const MAX_LANES = 32;
|
|
277
|
+
const MAX_LANE_STAGES = 16;
|
|
278
|
+
const MAX_LANE_STAGE_COUNT = 64;
|
|
279
|
+
const MAX_LANE_SPEC_BYTES = 64 * 1024;
|
|
280
|
+
const MAX_LANE_TASK_BYTES = 1024 * 1024;
|
|
281
|
+
const MAX_LANE_PATH_BYTES = 32 * 1024;
|
|
282
|
+
const MAX_LANE_BOARD_TEXT_BYTES = 256;
|
|
283
|
+
const LANE_PATH_FIELDS = new Set(["cwd", "output", "sessionDir"]);
|
|
284
|
+
|
|
272
285
|
function runsAllKeyAccessError(prop) {
|
|
273
286
|
return new Error("Cannot read runs.all result property '" + prop + "'. runs.all resolves to an ordered array, not a key map. Use results[0], array destructuring, or results.map((result) => result.output), not results." + prop + ".");
|
|
274
287
|
}
|
|
@@ -288,6 +301,224 @@ function wrapRunsAllResults(results, keys) {
|
|
|
288
301
|
return proxy;
|
|
289
302
|
}
|
|
290
303
|
|
|
304
|
+
function laneByteLength(value) {
|
|
305
|
+
return new TextEncoder().encode(value).byteLength;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function laneBoardText(value) {
|
|
309
|
+
if (typeof value !== "string" || !value) return undefined;
|
|
310
|
+
if (laneByteLength(value) <= MAX_LANE_BOARD_TEXT_BYTES) return value;
|
|
311
|
+
const bytes = new TextEncoder().encode(value).subarray(0, MAX_LANE_BOARD_TEXT_BYTES - 3);
|
|
312
|
+
return new TextDecoder().decode(bytes) + "...";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function laneBoardPath(value) {
|
|
316
|
+
if (typeof value !== "string" || !value || laneByteLength(value) > MAX_LANE_BOARD_TEXT_BYTES) return undefined;
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function validateLaneStageBounds(params, label) {
|
|
321
|
+
if (typeof params.task === "string" && laneByteLength(params.task) > MAX_LANE_TASK_BYTES) throw new Error(label + " task exceeds 1 MiB when UTF-8 encoded.");
|
|
322
|
+
for (const field of LANE_PATH_FIELDS) if (typeof params[field] === "string" && laneByteLength(params[field]) > MAX_LANE_PATH_BYTES) throw new Error(label + " " + field + " exceeds 32 KiB when UTF-8 encoded.");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validateLaneKey(value, owner) {
|
|
326
|
+
if (typeof value !== "string" || !runKeyPattern.test(value)) throw new Error(owner + " key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.");
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function laneStageParams(stage, previous) {
|
|
331
|
+
const resume = stage.resume;
|
|
332
|
+
const params = { ...stage.params };
|
|
333
|
+
if (resume !== "previous") return params;
|
|
334
|
+
if (!previous || typeof previous.runId !== "string" || !previous.runId.trim()) return undefined;
|
|
335
|
+
return { ...params, resume: previous.runId.trim() };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function laneStageVerdict(result) {
|
|
339
|
+
const structured = result && typeof result === "object" && !Array.isArray(result) ? result.structuredOutput : undefined;
|
|
340
|
+
const verdict = structured && typeof structured === "object" && !Array.isArray(structured) ? structured.verdict : undefined;
|
|
341
|
+
return typeof verdict === "string" && verdict.trim() ? laneBoardText(verdict.trim()) : undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function laneStageIsBlocked(result) {
|
|
345
|
+
const structured = result && typeof result === "object" && !Array.isArray(result) ? result.structuredOutput : undefined;
|
|
346
|
+
return structured && typeof structured === "object" && !Array.isArray(structured) && structured.verdict === "blocked";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function laneStageRecord(stageKey, child, forcedState) {
|
|
350
|
+
const result = child && typeof child === "object" && !Array.isArray(child) ? child : undefined;
|
|
351
|
+
const ok = result?.ok === true;
|
|
352
|
+
const verdict = laneStageVerdict(result);
|
|
353
|
+
const blocked = laneStageIsBlocked(result);
|
|
354
|
+
const state = forcedState ?? (result?.stopped ? "stopped" : result?.detached ? "detached" : ok ? blocked ? "blocked" : "completed" : "failed");
|
|
355
|
+
const error = state !== "completed"
|
|
356
|
+
? state === "blocked" && blocked
|
|
357
|
+
? "Stage returned a blocked verdict."
|
|
358
|
+
: typeof result?.error === "string"
|
|
359
|
+
? result.error
|
|
360
|
+
: typeof result?.output === "string"
|
|
361
|
+
? result.output
|
|
362
|
+
: "Stage did not complete successfully."
|
|
363
|
+
: undefined;
|
|
364
|
+
return {
|
|
365
|
+
key: stageKey,
|
|
366
|
+
...(typeof result?.runId === "string" && result.runId.trim() ? { runId: laneBoardText(result.runId.trim()) } : {}),
|
|
367
|
+
...(result ? { ok } : {}),
|
|
368
|
+
state,
|
|
369
|
+
...(typeof result?.outputReference === "string" ? (laneBoardPath(result.outputReference) ? { outputReference: laneBoardPath(result.outputReference) } : {}) : {}),
|
|
370
|
+
...(verdict ? { verdict } : {}),
|
|
371
|
+
...(error ? { error: laneBoardText(error) } : {}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function laneFailure(stageKey, error) {
|
|
376
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
377
|
+
return { key: stageKey, ok: false, output: text, error: text, artifactPaths: [] };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function runCollected(key, params, observe, generatedLaneKey) {
|
|
381
|
+
validateRunCall(key, params, "runs.lanes stage", runFingerprints);
|
|
382
|
+
const launched = runHostCall(key, params, true, undefined, generatedLaneKey);
|
|
383
|
+
observe([{ key, operation: "run", callId: launched.callId }]);
|
|
384
|
+
return launched.promise.then(decorateWorkflowChildResult);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validateLaneSpecs(laneSpecs) {
|
|
388
|
+
if (!Array.isArray(laneSpecs)) throw new Error("runs.lanes(lanes) requires an array.");
|
|
389
|
+
if (laneSpecs.length === 0) throw new Error("runs.lanes(lanes) requires at least one lane.");
|
|
390
|
+
if (laneSpecs.length > MAX_LANES) throw new Error("runs.lanes supports at most " + MAX_LANES + " lanes.");
|
|
391
|
+
assertJsonValue(laneSpecs, "runs.lanes lanes");
|
|
392
|
+
if (laneByteLength(stableRunJson(laneSpecs)) > MAX_LANE_SPEC_BYTES) throw new Error("runs.lanes canonical JSON exceeds 64 KiB.");
|
|
393
|
+
const generatedKeys = new Set();
|
|
394
|
+
const validationFingerprints = new Map();
|
|
395
|
+
let stageCount = 0;
|
|
396
|
+
const normalized = [];
|
|
397
|
+
for (let laneIndex = 0; laneIndex < laneSpecs.length; laneIndex++) {
|
|
398
|
+
const lane = laneSpecs[laneIndex];
|
|
399
|
+
const laneLabel = "runs.lanes lane " + laneIndex;
|
|
400
|
+
if (!lane || typeof lane !== "object" || Array.isArray(lane)) throw new Error(laneLabel + " must be an object.");
|
|
401
|
+
const laneFields = Object.keys(lane);
|
|
402
|
+
if (laneFields.some((field) => field !== "key" && field !== "stages")) throw new Error(laneLabel + " contains unsupported fields.");
|
|
403
|
+
const laneKey = validateLaneKey(lane.key, laneLabel);
|
|
404
|
+
if (!Array.isArray(lane.stages) || lane.stages.length === 0) throw new Error(laneLabel + " stages must contain at least one stage.");
|
|
405
|
+
if (lane.stages.length > MAX_LANE_STAGES) throw new Error(laneLabel + " supports at most " + MAX_LANE_STAGES + " stages.");
|
|
406
|
+
const stageKeys = new Set();
|
|
407
|
+
const stages = [];
|
|
408
|
+
for (let stageIndex = 0; stageIndex < lane.stages.length; stageIndex++) {
|
|
409
|
+
stageCount++;
|
|
410
|
+
if (stageCount > MAX_LANE_STAGE_COUNT) throw new Error("runs.lanes supports at most " + MAX_LANE_STAGE_COUNT + " total stages.");
|
|
411
|
+
const stage = lane.stages[stageIndex];
|
|
412
|
+
const stageLabel = laneLabel + " stage " + stageIndex;
|
|
413
|
+
if (!stage || typeof stage !== "object" || Array.isArray(stage)) throw new Error(stageLabel + " must be an object.");
|
|
414
|
+
const stageKey = validateLaneKey(stage.key, stageLabel);
|
|
415
|
+
if (stageKeys.has(stageKey)) throw new Error(laneLabel + " contains duplicate stage key '" + stageKey + "'.");
|
|
416
|
+
stageKeys.add(stageKey);
|
|
417
|
+
const generatedKey = laneKey + "." + stageKey;
|
|
418
|
+
validateLaneKey(generatedKey, stageLabel + " generated");
|
|
419
|
+
if (generatedKeys.has(generatedKey)) throw new Error("runs.lanes generated child key '" + generatedKey + "' is duplicated.");
|
|
420
|
+
generatedKeys.add(generatedKey);
|
|
421
|
+
const resume = stage.resume;
|
|
422
|
+
if (resume !== undefined && resume !== "previous") {
|
|
423
|
+
if (stageIndex === 0 && typeof resume === "string") {
|
|
424
|
+
throw new Error(stageLabel + " cannot resume a retained run id in runs.lanes; use runs.run(key, { resume: id }) outside lanes, or start the lane with an agent stage and use resume: \"previous\" later.");
|
|
425
|
+
}
|
|
426
|
+
throw new Error(stageLabel + " resume must be 'previous'.");
|
|
427
|
+
}
|
|
428
|
+
if (stageIndex === 0 && resume === "previous") throw new Error(stageLabel + " cannot resume previous without a predecessor stage.");
|
|
429
|
+
const { key: _stageKey, resume: _resume, ...params } = stage;
|
|
430
|
+
const validationParams = resume === "previous" ? { ...params, resume: "retained-run-placeholder" } : params;
|
|
431
|
+
validateLaneStageBounds(validationParams, stageLabel);
|
|
432
|
+
validateRunCall(generatedKey, validationParams, stageLabel, validationFingerprints);
|
|
433
|
+
const existingFingerprint = runFingerprints.get(generatedKey);
|
|
434
|
+
if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(params))) {
|
|
435
|
+
throw new Error("runs.lanes generated child key '" + generatedKey + "' is already used with incompatible launch params.");
|
|
436
|
+
}
|
|
437
|
+
stages.push({ key: stageKey, generatedKey, resume, params });
|
|
438
|
+
}
|
|
439
|
+
normalized.push({ key: laneKey, stages });
|
|
440
|
+
}
|
|
441
|
+
return normalized;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function runLane(lane, firstResult, observe) {
|
|
445
|
+
const records = [];
|
|
446
|
+
const appendSkipped = (start) => {
|
|
447
|
+
for (let index = start; index < lane.stages.length; index++) records.push({ key: lane.stages[index].key, state: "skipped" });
|
|
448
|
+
};
|
|
449
|
+
const finish = (state, failedStage) => ({ key: lane.key, state, ...(failedStage ? { failedStage } : {}), stages: records });
|
|
450
|
+
const visit = (index, previous) => {
|
|
451
|
+
if (index >= lane.stages.length) return Promise.resolve(finish("complete"));
|
|
452
|
+
const stage = lane.stages[index];
|
|
453
|
+
if (index === 0) {
|
|
454
|
+
const record = laneStageRecord(stage.key, previous);
|
|
455
|
+
records.push(record);
|
|
456
|
+
if (record.state !== "completed") {
|
|
457
|
+
appendSkipped(index + 1);
|
|
458
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
459
|
+
}
|
|
460
|
+
return visit(index + 1, previous);
|
|
461
|
+
}
|
|
462
|
+
if (stage.resume === "previous" && (!previous || typeof previous.runId !== "string" || !previous.runId.trim())) {
|
|
463
|
+
records.push({ key: stage.key, state: "blocked", error: "Previous stage did not return a retained run id." });
|
|
464
|
+
appendSkipped(index + 1);
|
|
465
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
466
|
+
}
|
|
467
|
+
if (!previous || previous.ok !== true || laneStageIsBlocked(previous)) {
|
|
468
|
+
records.push({ key: stage.key, state: "blocked", error: "Previous stage did not complete successfully." });
|
|
469
|
+
appendSkipped(index + 1);
|
|
470
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
471
|
+
}
|
|
472
|
+
const params = laneStageParams(stage, previous);
|
|
473
|
+
if (!params) {
|
|
474
|
+
records.push({ key: stage.key, state: "blocked", error: "Previous stage did not return a retained run id." });
|
|
475
|
+
appendSkipped(index + 1);
|
|
476
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
477
|
+
}
|
|
478
|
+
let launched;
|
|
479
|
+
try {
|
|
480
|
+
launched = runCollected(stage.generatedKey, params, observe, lane.key);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const failed = laneFailure(stage.generatedKey, error);
|
|
483
|
+
records.push(laneStageRecord(stage.key, failed));
|
|
484
|
+
appendSkipped(index + 1);
|
|
485
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
486
|
+
}
|
|
487
|
+
return launched.then((result) => {
|
|
488
|
+
const record = laneStageRecord(stage.key, result);
|
|
489
|
+
records.push(record);
|
|
490
|
+
if (record.state === "completed") return visit(index + 1, result);
|
|
491
|
+
appendSkipped(index + 1);
|
|
492
|
+
return finish("blocked", stage.key);
|
|
493
|
+
}, (error) => {
|
|
494
|
+
const failed = laneFailure(stage.generatedKey, error);
|
|
495
|
+
records.push(laneStageRecord(stage.key, failed));
|
|
496
|
+
appendSkipped(index + 1);
|
|
497
|
+
return finish("blocked", stage.key);
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
return visit(0, firstResult);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function runLanes(laneSpecs) {
|
|
504
|
+
const lanes = validateLaneSpecs(laneSpecs);
|
|
505
|
+
const firstItems = lanes.map((lane) => {
|
|
506
|
+
const first = lane.stages[0];
|
|
507
|
+
return { key: first.generatedKey, ...first.params };
|
|
508
|
+
});
|
|
509
|
+
// Share the runs.all batch launcher while allowing each lane to advance independently.
|
|
510
|
+
const firstBatch = launchRunsAll(firstItems, lanes.map((lane) => lane.key));
|
|
511
|
+
const firstResults = firstBatch.launched.map(({ promise }) => promise.then(decorateWorkflowChildResult));
|
|
512
|
+
let trackedAggregate;
|
|
513
|
+
const observe = (observations) => trackRunObservation(observations, trackedAggregate);
|
|
514
|
+
const laneAggregate = Promise.all(lanes.map((lane, index) => firstResults[index].then(
|
|
515
|
+
(result) => runLane(lane, result, observe),
|
|
516
|
+
(error) => runLane(lane, laneFailure(lane.stages[0].generatedKey, error), observe),
|
|
517
|
+
)));
|
|
518
|
+
trackedAggregate = trackRunObservation(firstBatch.launched.map(({ key, callId }) => ({ key, operation: "run", callId })), laneAggregate);
|
|
519
|
+
return trackedAggregate;
|
|
520
|
+
}
|
|
521
|
+
|
|
291
522
|
function formatRef(result) {
|
|
292
523
|
if (!result || typeof result !== "object") throw new Error("runs.ref(result) requires a run result object.");
|
|
293
524
|
const parts = ["run " + (result.key || "unknown")];
|
|
@@ -332,8 +563,28 @@ function validateExtensionBindings(value, label) {
|
|
|
332
563
|
if (new TextEncoder().encode(stableRunJson(value)).byteLength > 16384) throw new Error(label + " extensionBindings canonical JSON exceeds 16384 bytes.");
|
|
333
564
|
}
|
|
334
565
|
|
|
566
|
+
function validateLaneMetadata(value, label, workflowKey) {
|
|
567
|
+
if (value === undefined) return;
|
|
568
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be a plain JSON object.");
|
|
569
|
+
const fields = Object.keys(value);
|
|
570
|
+
const allowed = ["version", "key", "mode", "sourceRef", "claims", "outputPaths"];
|
|
571
|
+
const unknown = fields.filter((field) => !allowed.includes(field));
|
|
572
|
+
if (unknown.length > 0) throw new Error(label + " has unsupported fields: " + unknown.join(", ") + ".");
|
|
573
|
+
if (value.version !== 1) throw new Error(label + ".version must be 1.");
|
|
574
|
+
if (typeof value.key !== "string" || !value.key.trim() || !runKeyPattern.test(value.key.trim())) throw new Error(label + ".key is invalid.");
|
|
575
|
+
if (workflowKey !== undefined && value.key.trim() !== workflowKey) throw new Error(label + ".key must match workflow key '" + workflowKey + "'.");
|
|
576
|
+
if (value.mode !== undefined && !["mutation", "review", "scout", "gate"].includes(value.mode)) throw new Error(label + ".mode is invalid.");
|
|
577
|
+
const bounded = (entry, maxBytes) => typeof entry === "string" && entry.trim() && new TextEncoder().encode(entry.trim()).byteLength <= maxBytes && !/[\r\n\u0000]/.test(entry);
|
|
578
|
+
if (value.sourceRef !== undefined && !bounded(value.sourceRef, 128)) throw new Error(label + ".sourceRef is invalid.");
|
|
579
|
+
for (const [name, maxItems, maxLength] of [["claims", 20, 160], ["outputPaths", 10, 256]]) {
|
|
580
|
+
if (value[name] === undefined) continue;
|
|
581
|
+
if (!Array.isArray(value[name]) || value[name].length > maxItems || value[name].some((entry) => !bounded(entry, maxLength))) throw new Error(label + "." + name + " is invalid.");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
335
585
|
function validateRunCall(key, params, label, fingerprints) {
|
|
336
586
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
|
|
587
|
+
if (hostKeys.has(key)) throw new Error("Workflow key '" + key + "' is already used by runs.host.");
|
|
337
588
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error(label + " requires a params object.");
|
|
338
589
|
if (Object.prototype.hasOwnProperty.call(params, "action") || Object.prototype.hasOwnProperty.call(params, "workflowScript") || Object.prototype.hasOwnProperty.call(params, "tasks") || Object.prototype.hasOwnProperty.call(params, "chain") || Object.prototype.hasOwnProperty.call(params, "parallel") || Object.prototype.hasOwnProperty.call(params, "concurrency") || Object.prototype.hasOwnProperty.call(params, "chainDir")) {
|
|
339
590
|
const hint = label === "runs.run" ? "; use runs.all(...) and JavaScript control flow for orchestration." : ".";
|
|
@@ -341,6 +592,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
341
592
|
}
|
|
342
593
|
if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
|
|
343
594
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
|
|
595
|
+
validateLaneMetadata(params.lane, label + " lane", key);
|
|
344
596
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
|
|
345
597
|
if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
|
|
346
598
|
if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
|
|
@@ -365,6 +617,44 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
365
617
|
fingerprints.set(key, fingerprint);
|
|
366
618
|
}
|
|
367
619
|
|
|
620
|
+
const hostKeys = new Set();
|
|
621
|
+
|
|
622
|
+
function validateHostCommand(key, params) {
|
|
623
|
+
validateLaneKey(key, "runs.host");
|
|
624
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("runs.host('" + key + "') params must be an object.");
|
|
625
|
+
const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
|
|
626
|
+
const unknown = Object.keys(params).filter((field) => !allowed.has(field));
|
|
627
|
+
if (unknown.length) throw new Error("runs.host('" + key + "') params have unsupported fields: " + unknown.join(", ") + ".");
|
|
628
|
+
if (params.kind !== "command") throw new Error("runs.host('" + key + "') kind must be 'command'.");
|
|
629
|
+
if (typeof params.command !== "string" || !params.command.trim() || params.command.includes("\u0000") || new TextEncoder().encode(params.command.trim()).byteLength > 16384) throw new Error("runs.host('" + key + "') command must be a non-empty string of at most 16384 bytes without NUL.");
|
|
630
|
+
if (!Number.isInteger(params.timeoutMs) || params.timeoutMs < 1 || params.timeoutMs > 86400000) throw new Error("runs.host('" + key + "') timeoutMs must be an integer from 1 to 86400000.");
|
|
631
|
+
if (params.output !== undefined && (typeof params.output !== "string" || !params.output.trim() || /^[/\\]|(?:^|[/\\])\.\.(?:[/\\]|$)/.test(params.output) || /[\u0000-\u001f\u007f]/.test(params.output) || new TextEncoder().encode(params.output.trim()).byteLength > 240)) throw new Error("runs.host('" + key + "') output must be a bounded relative path without traversal or control characters.");
|
|
632
|
+
if (params.role !== undefined && params.role !== "ci" && params.role !== "gate") throw new Error("runs.host('" + key + "') role must be 'ci' or 'gate'.");
|
|
633
|
+
if (params.provider !== undefined && (typeof params.provider !== "string" || !params.provider.trim() || /[\r\n\u0000]/.test(params.provider) || new TextEncoder().encode(params.provider.trim()).byteLength > 64)) throw new Error("runs.host('" + key + "') provider must be a non-empty single-line string of at most 64 bytes.");
|
|
634
|
+
assertJsonValue(params, "runs.host('" + key + "') params");
|
|
635
|
+
if (hostKeys.has(key) || runFingerprints.has(key)) throw new Error("Workflow key '" + key + "' is already in use.");
|
|
636
|
+
if (hostKeys.size >= 32) throw new Error("workflowScript supports at most 32 runs.host calls.");
|
|
637
|
+
hostKeys.add(key);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function launchRunsAll(items, generatedLaneKeys) {
|
|
641
|
+
if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
|
|
642
|
+
const fingerprints = new Map(runFingerprints);
|
|
643
|
+
const calls = [];
|
|
644
|
+
for (let index = 0; index < items.length; index++) {
|
|
645
|
+
if (!Object.prototype.hasOwnProperty.call(items, index)) throw new Error("runs.all items must not contain sparse entries.");
|
|
646
|
+
const item = items[index];
|
|
647
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("runs.all item " + index + " must be an object.");
|
|
648
|
+
const { key, ...params } = item;
|
|
649
|
+
validateRunCall(key, params, "runs.all item " + index, fingerprints);
|
|
650
|
+
calls.push({ key, params });
|
|
651
|
+
}
|
|
652
|
+
runFingerprints = fingerprints;
|
|
653
|
+
const batch = { id: "batch-" + (++nextCallId), calls };
|
|
654
|
+
const launched = calls.map(({ key, params }, index) => runHostCall(key, params, true, batch, generatedLaneKeys?.[index]));
|
|
655
|
+
return { calls, launched };
|
|
656
|
+
}
|
|
657
|
+
|
|
368
658
|
const runs = Object.freeze({
|
|
369
659
|
run(key, params) {
|
|
370
660
|
validateRunCall(key, params, "runs.run", runFingerprints);
|
|
@@ -372,22 +662,16 @@ const runs = Object.freeze({
|
|
|
372
662
|
return trackRunObservation([{ key, operation: "run", callId: launched.callId }], launched.promise.then(decorateWorkflowChildResult));
|
|
373
663
|
},
|
|
374
664
|
all(items) {
|
|
375
|
-
|
|
376
|
-
const fingerprints = new Map(runFingerprints);
|
|
377
|
-
const calls = [];
|
|
378
|
-
for (let index = 0; index < items.length; index++) {
|
|
379
|
-
if (!Object.prototype.hasOwnProperty.call(items, index)) throw new Error("runs.all items must not contain sparse entries.");
|
|
380
|
-
const item = items[index];
|
|
381
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("runs.all item " + index + " must be an object.");
|
|
382
|
-
const { key, ...params } = item;
|
|
383
|
-
validateRunCall(key, params, "runs.all item " + index, fingerprints);
|
|
384
|
-
calls.push({ key, params });
|
|
385
|
-
}
|
|
386
|
-
runFingerprints = fingerprints;
|
|
387
|
-
const batch = { id: "batch-" + (++nextCallId), calls };
|
|
388
|
-
const launched = calls.map(({ key, params }) => runHostCall(key, params, true, batch));
|
|
665
|
+
const { calls, launched } = launchRunsAll(items);
|
|
389
666
|
return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })), Promise.all(launched.map(({ promise }) => promise)).then((results) => wrapRunsAllResults(results.map(decorateWorkflowChildResult), calls.map(({ key }) => key))));
|
|
390
667
|
},
|
|
668
|
+
lanes(laneSpecs) {
|
|
669
|
+
return runLanes(laneSpecs);
|
|
670
|
+
},
|
|
671
|
+
host(key, params) {
|
|
672
|
+
validateHostCommand(key, params);
|
|
673
|
+
return hostCall("host", { key, params }, { key, operation: "host" });
|
|
674
|
+
},
|
|
391
675
|
steer(key, message, options = {}) {
|
|
392
676
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error("runs.steer has an invalid key.");
|
|
393
677
|
if (typeof message !== "string" || !message.trim()) throw new Error("runs.steer message must be a non-empty string.");
|
|
@@ -654,6 +938,8 @@ parentPort.on("message", async (message) => {
|
|
|
654
938
|
export interface WorkflowScriptChildResult {
|
|
655
939
|
key: string;
|
|
656
940
|
ok: boolean;
|
|
941
|
+
lane?: import("../shared/types.ts").WorkflowLaneMetadata;
|
|
942
|
+
terminalOutcome?: import("../shared/types.ts").WorkflowTerminalOutcome;
|
|
657
943
|
stopped?: boolean;
|
|
658
944
|
/** Canonical child agent name when launch resolution produced one. */
|
|
659
945
|
agent?: string;
|
|
@@ -666,15 +952,16 @@ export interface WorkflowScriptChildResult {
|
|
|
666
952
|
requestedContext?: "fresh" | "fork";
|
|
667
953
|
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
668
954
|
outputReference?: string;
|
|
955
|
+
outputPathMapping?: { requestedPath: string; savedPath: string };
|
|
669
956
|
externalAdapter?: import("../shared/types.ts").ExternalCliReceiptMetadata;
|
|
670
957
|
resumability?: { state: "resumable" } | { state: "not-resumable"; reason: string };
|
|
671
958
|
continuation?: { runIds: string[] };
|
|
672
959
|
artifactPaths: string[];
|
|
673
|
-
results?:
|
|
960
|
+
results?: SingleResult[];
|
|
674
961
|
}
|
|
675
962
|
|
|
676
963
|
export interface WorkflowScriptTraceEntry {
|
|
677
|
-
operation: "run" | "status" | "steer";
|
|
964
|
+
operation: "run" | "status" | "steer" | "host";
|
|
678
965
|
key: string;
|
|
679
966
|
state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused" | "queued" | "delivered" | "missed";
|
|
680
967
|
/** Canonical child agent name when resolved launch or result data is available. */
|
|
@@ -684,6 +971,10 @@ export interface WorkflowScriptTraceEntry {
|
|
|
684
971
|
phase?: string;
|
|
685
972
|
label?: string;
|
|
686
973
|
error?: string;
|
|
974
|
+
/** Internal provenance for a generated runs.lanes child key. */
|
|
975
|
+
generatedLaneKey?: string;
|
|
976
|
+
lane?: import("../shared/types.ts").WorkflowLaneMetadata;
|
|
977
|
+
warning?: string;
|
|
687
978
|
}
|
|
688
979
|
|
|
689
980
|
export interface WorkflowSteerOptions {
|
|
@@ -722,9 +1013,9 @@ export interface WorkflowScriptResult {
|
|
|
722
1013
|
|
|
723
1014
|
export class WorkflowScriptError extends Error {
|
|
724
1015
|
readonly partial: Omit<WorkflowScriptResult, "value">;
|
|
725
|
-
readonly errorKind?: "detached-child";
|
|
1016
|
+
readonly errorKind?: "detached-child" | "timeout";
|
|
726
1017
|
|
|
727
|
-
constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child") {
|
|
1018
|
+
constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child" | "timeout") {
|
|
728
1019
|
super(message);
|
|
729
1020
|
this.name = "WorkflowScriptError";
|
|
730
1021
|
this.partial = partial;
|
|
@@ -738,11 +1029,15 @@ export interface RunWorkflowScriptOptions {
|
|
|
738
1029
|
oneUsePermit?: { claim: (key: string) => string | undefined };
|
|
739
1030
|
timeoutMs?: number;
|
|
740
1031
|
signal?: AbortSignal;
|
|
1032
|
+
/** Maximum children executing concurrently within this workflow. Defaults to 20. */
|
|
1033
|
+
globalConcurrencyLimit?: number;
|
|
741
1034
|
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
|
|
742
1035
|
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
743
1036
|
resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
744
1037
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
745
1038
|
steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
|
|
1039
|
+
host?: (key: string, params: WorkflowHostCommandParams, signal: AbortSignal) => Promise<WorkflowHostCommandResult>;
|
|
1040
|
+
onHostStep?: (hostStep: HostStepNodeV1) => void;
|
|
746
1041
|
state?: {
|
|
747
1042
|
get: (key: string) => unknown | Promise<unknown>;
|
|
748
1043
|
set: (key: string, value: unknown) => void | Promise<void>;
|
|
@@ -914,12 +1209,47 @@ function literalString(node: unknown): string | undefined {
|
|
|
914
1209
|
return undefined;
|
|
915
1210
|
}
|
|
916
1211
|
|
|
917
|
-
function directRunsCall(node: unknown, method: "run" | "all"): node is AstNode {
|
|
1212
|
+
function directRunsCall(node: unknown, method: "run" | "all" | "host"): node is AstNode {
|
|
918
1213
|
if (!astNode(node) || node.type !== "CallExpression" || !astNode(node.callee) || node.callee.type !== "MemberExpression") return false;
|
|
919
1214
|
const property = node.callee.computed === true ? literalString(node.callee.property) : astNode(node.callee.property) && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
|
|
920
1215
|
return property === method && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "runs";
|
|
921
1216
|
}
|
|
922
1217
|
|
|
1218
|
+
function validateStaticHostCall(call: AstNode): WorkflowScriptValidationError[] {
|
|
1219
|
+
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
1220
|
+
const keyNode = astNode(args[0]) ? args[0] : undefined;
|
|
1221
|
+
const params = astNode(args[1]) ? args[1] : undefined;
|
|
1222
|
+
const errors: WorkflowScriptValidationError[] = [];
|
|
1223
|
+
const key = literalString(keyNode);
|
|
1224
|
+
if (keyNode && key !== undefined && !KEY_PATTERN.test(key)) errors.push({ message: "runs.host key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
|
|
1225
|
+
if (!params || params.type !== "ObjectExpression" || !Array.isArray(params.properties) || params.properties.some((property) => !astNode(property) || property.type !== "Property")) return errors;
|
|
1226
|
+
const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
|
|
1227
|
+
for (const property of params.properties) {
|
|
1228
|
+
if (!astNode(property)) continue;
|
|
1229
|
+
const name = staticPropertyKey(property);
|
|
1230
|
+
if (name !== undefined && !allowed.has(name)) errors.push({ message: `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
|
|
1231
|
+
}
|
|
1232
|
+
const kindNode = directObjectPropertyValue(params, "kind");
|
|
1233
|
+
const commandNode = directObjectPropertyValue(params, "command");
|
|
1234
|
+
const timeoutNode = directObjectPropertyValue(params, "timeoutMs");
|
|
1235
|
+
const kind = literalString(kindNode);
|
|
1236
|
+
const command = literalString(commandNode);
|
|
1237
|
+
const timeout = astNode(timeoutNode) && timeoutNode.type === "Literal" ? timeoutNode.value : undefined;
|
|
1238
|
+
if (!kindNode) errors.push({ message: "runs.host params require kind: 'command'.", ...nodeLocation(params) });
|
|
1239
|
+
else if (kind !== undefined && kind !== "command") errors.push({ message: "runs.host params kind must be 'command'.", ...nodeLocation(kindNode) });
|
|
1240
|
+
if (!commandNode) errors.push({ message: "runs.host params require command.", ...nodeLocation(params) });
|
|
1241
|
+
else if (command !== undefined && !command.trim()) errors.push({ message: "runs.host params command must be non-empty.", ...nodeLocation(commandNode) });
|
|
1242
|
+
if (!timeoutNode) errors.push({ message: "runs.host params require timeoutMs.", ...nodeLocation(params) });
|
|
1243
|
+
else if (typeof timeout === "number" && (!Number.isInteger(timeout) || timeout < 1)) errors.push({ message: "runs.host params timeoutMs must be a positive integer.", ...nodeLocation(timeoutNode) });
|
|
1244
|
+
const outputNode = directObjectPropertyValue(params, "output");
|
|
1245
|
+
const output = literalString(outputNode);
|
|
1246
|
+
if (outputNode && output !== undefined && (!output.trim() || /^[/\\]|(?:^|[/\\])\.\.(?:[/\\]|$)/.test(output))) errors.push({ message: "runs.host params output must be a non-empty relative path without traversal.", ...nodeLocation(outputNode) });
|
|
1247
|
+
const roleNode = directObjectPropertyValue(params, "role");
|
|
1248
|
+
const role = literalString(roleNode);
|
|
1249
|
+
if (roleNode && role !== undefined && role !== "ci" && role !== "gate") errors.push({ message: "runs.host params role must be 'ci' or 'gate'.", ...nodeLocation(roleNode) });
|
|
1250
|
+
return errors;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
923
1253
|
function nodeLocation(node: AstNode): Pick<WorkflowScriptValidationError, "line" | "column"> {
|
|
924
1254
|
return node.loc ? { line: Math.max(1, node.loc.start.line - 1), column: node.loc.start.column + 1 } : {};
|
|
925
1255
|
}
|
|
@@ -1040,6 +1370,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1040
1370
|
}
|
|
1041
1371
|
}
|
|
1042
1372
|
}
|
|
1373
|
+
if (directRunsCall(node, "host")) errors.push(...validateStaticHostCall(node));
|
|
1043
1374
|
const boundaryValue = node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "Identifier" && node.callee.name === "emit" && Array.isArray(node.arguments) && astNode(node.arguments[0])
|
|
1044
1375
|
? node.arguments[0]
|
|
1045
1376
|
: node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "MemberExpression" && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "state" && astNode(node.callee.property) && node.callee.property.type === "Identifier" && node.callee.property.name === "set" && Array.isArray(node.arguments) && astNode(node.arguments[1])
|
|
@@ -1109,9 +1440,48 @@ function resolveWorkflowParserEntry(): string {
|
|
|
1109
1440
|
}
|
|
1110
1441
|
}
|
|
1111
1442
|
|
|
1443
|
+
const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "lane", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "worktree"] as const;
|
|
1444
|
+
|
|
1445
|
+
function isZeroUsage(usage: unknown): boolean {
|
|
1446
|
+
if (!isRecord(usage)) return false;
|
|
1447
|
+
const cost = usage.cost;
|
|
1448
|
+
return (usage.input ?? 0) === 0
|
|
1449
|
+
&& (usage.output ?? 0) === 0
|
|
1450
|
+
&& (usage.cacheRead ?? 0) === 0
|
|
1451
|
+
&& (usage.cacheWrite ?? 0) === 0
|
|
1452
|
+
&& (!isRecord(cost) || (cost.total ?? 0) === 0);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function setupAbortResumeParams(params: Record<string, unknown>, result: WorkflowScriptChildResult, signal: AbortSignal): Record<string, unknown> | undefined {
|
|
1456
|
+
if (signal.aborted || result.ok || result.stopped || result.interrupted || !result.runId) return undefined;
|
|
1457
|
+
const childResult = Array.isArray(result.results) && result.results.length === 1 && isRecord(result.results[0]) ? result.results[0] : undefined;
|
|
1458
|
+
const error = typeof childResult?.error === "string" ? childResult.error : result.error;
|
|
1459
|
+
if (error !== "This operation was aborted" || !isZeroUsage(childResult?.usage)) return undefined;
|
|
1460
|
+
const messages = Array.isArray(childResult?.messages) ? childResult.messages : [];
|
|
1461
|
+
const message = messages.findLast((entry) => isRecord(entry) && entry.role === "assistant");
|
|
1462
|
+
if (message !== undefined) {
|
|
1463
|
+
if (!isRecord(message)) return undefined;
|
|
1464
|
+
if (message.stopReason !== "error" || message.errorMessage !== error) return undefined;
|
|
1465
|
+
if (!Array.isArray(message.content) || message.content.length > 0 || !isZeroUsage(message.usage)) return undefined;
|
|
1466
|
+
if (Object.hasOwn(message, "diagnostics") || Object.hasOwn(message, "responseId")) return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
const task = typeof params.task === "string" && params.task.trim() ? params.task.trim() : "Continue after the setup abort.";
|
|
1469
|
+
const resumeParams: Record<string, unknown> = { resume: result.runId, task };
|
|
1470
|
+
for (const key of AUTO_RESUME_PARAM_KEYS) {
|
|
1471
|
+
if (Object.hasOwn(params, key)) resumeParams[key] = params[key];
|
|
1472
|
+
}
|
|
1473
|
+
resumeParams.resume = result.runId;
|
|
1474
|
+
resumeParams.task = task;
|
|
1475
|
+
return resumeParams;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1112
1478
|
export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
|
|
1113
1479
|
if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
|
|
1114
1480
|
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
|
|
1481
|
+
if (options.globalConcurrencyLimit !== undefined && (!Number.isInteger(options.globalConcurrencyLimit) || options.globalConcurrencyLimit < 1)) {
|
|
1482
|
+
throw new Error("workflow script global concurrency limit must be a positive integer.");
|
|
1483
|
+
}
|
|
1484
|
+
const launchSemaphore = new Semaphore(options.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
1115
1485
|
|
|
1116
1486
|
let acornPath: string;
|
|
1117
1487
|
try {
|
|
@@ -1125,13 +1495,15 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1125
1495
|
const trace: WorkflowScriptTraceEntry[] = [];
|
|
1126
1496
|
const children = new Map<string, WorkflowScriptChildResult>();
|
|
1127
1497
|
const childOrder: string[] = [];
|
|
1128
|
-
const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
|
|
1498
|
+
const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean; generatedLaneKey?: string }>();
|
|
1129
1499
|
const steers = new Map<number, { key: string; promise: Promise<WorkflowSteerResult>; observed: boolean }>();
|
|
1500
|
+
const hostCalls = new Map<number, { key: string; promise: Promise<WorkflowHostCommandResult>; observed: boolean }>();
|
|
1130
1501
|
const stoppedLaunches = new Set<string>();
|
|
1131
1502
|
const childStopControllers = new Map<string, AbortController>();
|
|
1132
1503
|
const batchAdmissions = new Map<string, Promise<void>>();
|
|
1133
1504
|
const observedRunCalls = new Set<number>();
|
|
1134
1505
|
const observedSteerCalls = new Set<number>();
|
|
1506
|
+
const observedHostCalls = new Set<number>();
|
|
1135
1507
|
const childController = new AbortController();
|
|
1136
1508
|
let settled = false;
|
|
1137
1509
|
let finishing = false;
|
|
@@ -1152,7 +1524,18 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1152
1524
|
console.error("Workflow onTrace callback failed:", error);
|
|
1153
1525
|
}
|
|
1154
1526
|
};
|
|
1527
|
+
const hostStepChanged = (hostStep: HostStepNodeV1) => {
|
|
1528
|
+
try {
|
|
1529
|
+
options.onHostStep?.(hostStep);
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
console.error("Workflow onHostStep callback failed:", error);
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1155
1534
|
const stoppedChildResult = (key: string, message: string): WorkflowScriptChildResult => ({ key, ok: false, stopped: true, output: message, error: message, artifactPaths: [] });
|
|
1535
|
+
const responseBoundaryFailure = (key: string, error: unknown): WorkflowScriptChildResult => {
|
|
1536
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1537
|
+
return { key, ok: false, output: text, error: text, artifactPaths: [] };
|
|
1538
|
+
};
|
|
1156
1539
|
const stopChild = (key: string, message = `Workflow child '${key}' stopped by user.`): boolean => {
|
|
1157
1540
|
if (!launches.has(key) || children.has(key)) return false;
|
|
1158
1541
|
stoppedLaunches.add(key);
|
|
@@ -1166,6 +1549,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1166
1549
|
...(started?.agent ? { agent: started.agent } : {}),
|
|
1167
1550
|
...(started?.phase ? { phase: started.phase } : {}),
|
|
1168
1551
|
...(started?.label ? { label: started.label } : {}),
|
|
1552
|
+
...(started?.generatedLaneKey ? { generatedLaneKey: started.generatedLaneKey } : {}),
|
|
1169
1553
|
error: message,
|
|
1170
1554
|
});
|
|
1171
1555
|
traceChanged();
|
|
@@ -1178,7 +1562,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1178
1562
|
if (settled || finishing) return;
|
|
1179
1563
|
finishing = true;
|
|
1180
1564
|
childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed."));
|
|
1181
|
-
void Promise.allSettled([...steers.values()].map(({ promise }) => promise)).then(() => {
|
|
1565
|
+
void Promise.allSettled([...steers.values(), ...hostCalls.values()].map(({ promise }) => promise)).then(() => {
|
|
1182
1566
|
if (settled) return;
|
|
1183
1567
|
settled = true;
|
|
1184
1568
|
options.registerStopChild?.(undefined);
|
|
@@ -1191,10 +1575,12 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1191
1575
|
: "value" in outcome
|
|
1192
1576
|
? (() => {
|
|
1193
1577
|
const unobservedSteers = [...steers.values()].filter((steer) => !steer.observed).map((steer) => steer.key);
|
|
1194
|
-
|
|
1578
|
+
if (unobservedSteers.length > 0) return new Error(`workflowScript completed with unawaited runs.steer call(s): ${unobservedSteers.map((key) => `'${key}'`).join(", ")}. Await or return each call.`);
|
|
1579
|
+
const unobservedHosts = [...hostCalls.values()].filter((host) => !host.observed).map((host) => host.key);
|
|
1580
|
+
return unobservedHosts.length > 0 ? new Error(`workflowScript completed with unawaited runs.host call(s): ${unobservedHosts.map((key) => `'${key}'`).join(", ")}. Await or return each call.`) : undefined;
|
|
1195
1581
|
})()
|
|
1196
1582
|
: undefined;
|
|
1197
|
-
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child"
|
|
1583
|
+
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" || outcome.error.workflowErrorKind === "timeout" ? outcome.error.workflowErrorKind : undefined));
|
|
1198
1584
|
else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
|
|
1199
1585
|
else resolve({ value: outcome.value, ...partial() });
|
|
1200
1586
|
});
|
|
@@ -1217,6 +1603,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1217
1603
|
...(started?.agent ? { agent: started.agent } : {}),
|
|
1218
1604
|
...(started?.phase ? { phase: started.phase } : {}),
|
|
1219
1605
|
...(started?.label ? { label: started.label } : {}),
|
|
1606
|
+
...(started?.generatedLaneKey ? { generatedLaneKey: started.generatedLaneKey } : {}),
|
|
1220
1607
|
error: error.message,
|
|
1221
1608
|
});
|
|
1222
1609
|
}
|
|
@@ -1225,7 +1612,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1225
1612
|
};
|
|
1226
1613
|
const timer = options.timeoutMs === undefined
|
|
1227
1614
|
? undefined
|
|
1228
|
-
: setTimeout(() =>
|
|
1615
|
+
: setTimeout(() => {
|
|
1616
|
+
const error = new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) as Error & { workflowErrorKind: "timeout" };
|
|
1617
|
+
error.workflowErrorKind = "timeout";
|
|
1618
|
+
finish({ error });
|
|
1619
|
+
}, options.timeoutMs);
|
|
1229
1620
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1230
1621
|
if (options.signal?.aborted) return onAbort();
|
|
1231
1622
|
|
|
@@ -1281,12 +1672,16 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1281
1672
|
const steer = steers.get(message.callId);
|
|
1282
1673
|
if (steer) steer.observed = true;
|
|
1283
1674
|
else observedSteerCalls.add(message.callId);
|
|
1675
|
+
} else if (message.operation === "host") {
|
|
1676
|
+
const host = hostCalls.get(message.callId);
|
|
1677
|
+
if (host) host.observed = true;
|
|
1678
|
+
else observedHostCalls.add(message.callId);
|
|
1284
1679
|
}
|
|
1285
1680
|
return;
|
|
1286
1681
|
}
|
|
1287
1682
|
if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
|
|
1288
1683
|
|
|
1289
|
-
const respond = (promise: Promise<unknown>, responsePath?: string) => {
|
|
1684
|
+
const respond = (promise: Promise<unknown>, responsePath?: string, onBoundaryError?: (error: unknown) => void) => {
|
|
1290
1685
|
void promise.then(
|
|
1291
1686
|
(value) => {
|
|
1292
1687
|
if (settled) return;
|
|
@@ -1299,6 +1694,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1299
1694
|
assertWorkflowJsonValue(normalized, responsePath);
|
|
1300
1695
|
worker.postMessage({ type: "response", callId: message.callId, ok: true, value: normalized });
|
|
1301
1696
|
} catch (error) {
|
|
1697
|
+
onBoundaryError?.(error);
|
|
1302
1698
|
worker.postMessage({ type: "response", callId: message.callId, ok: false, error: `${responsePath} must contain only JSON data before it can be returned from workflowScript. Return a plain projection such as { runId, ok, output }. ${error instanceof Error ? error.message : String(error)}` });
|
|
1303
1699
|
}
|
|
1304
1700
|
},
|
|
@@ -1373,6 +1769,52 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1373
1769
|
respond(promise);
|
|
1374
1770
|
return;
|
|
1375
1771
|
}
|
|
1772
|
+
if (message.method === "host") {
|
|
1773
|
+
let key: string;
|
|
1774
|
+
let params: WorkflowHostCommandParams;
|
|
1775
|
+
try {
|
|
1776
|
+
key = validateKey(message.args.key, "runs.host");
|
|
1777
|
+
params = normalizeWorkflowHostCommandParams(message.args.params, `runs.host('${key}') params`);
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
return respond(Promise.reject(error));
|
|
1780
|
+
}
|
|
1781
|
+
if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
|
|
1782
|
+
if (hostCalls.size >= HOST_STEP_MAX_COUNT) return respond(Promise.reject(new Error(`workflowScript supports at most ${HOST_STEP_MAX_COUNT} runs.host calls.`)));
|
|
1783
|
+
const startedAt = Date.now();
|
|
1784
|
+
const startedStep: HostStepNodeV1 = {
|
|
1785
|
+
version: 1,
|
|
1786
|
+
kind: "host-step",
|
|
1787
|
+
monitorKind: "command",
|
|
1788
|
+
id: key,
|
|
1789
|
+
label: key,
|
|
1790
|
+
...(params.role ? { role: params.role } : {}),
|
|
1791
|
+
...(params.provider ? { provider: params.provider } : {}),
|
|
1792
|
+
state: "running",
|
|
1793
|
+
updatedAt: startedAt,
|
|
1794
|
+
deadlineAt: startedAt + params.timeoutMs,
|
|
1795
|
+
};
|
|
1796
|
+
hostStepChanged(startedStep);
|
|
1797
|
+
trace.push({ operation: "host", key, state: "started" });
|
|
1798
|
+
traceChanged();
|
|
1799
|
+
const promise = Promise.resolve().then(() => options.host!(key, params, childController.signal)).then((result) => {
|
|
1800
|
+
const state = result.state === "stopped" ? "cancelled" : result.ok ? "done" : "error";
|
|
1801
|
+
const detail = [result.error, result.stderr.trim() || result.stdout.trim()].filter(Boolean).join(" ").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
1802
|
+
hostStepChanged({ ...startedStep, state, ...(state === "done" ? { verdict: "pass" as const } : {}), ...(!result.ok ? { reasonCode: result.state === "timed-out" ? "timed_out" : result.state === "stopped" ? "aborted" : "command_failed" } : {}), ...(detail ? { detail } : {}), reportPath: params.output ?? result.outputPath.split(/[\\/]/).at(-1), exitCode: result.exitCode, updatedAt: Date.now() });
|
|
1803
|
+
trace.push({ operation: "host", key, state: result.ok ? "completed" : result.state === "stopped" ? "stopped" : "failed", durationMs: result.durationMs, ...(!result.ok ? { error: result.error ?? "Host command failed." } : {}) });
|
|
1804
|
+
traceChanged();
|
|
1805
|
+
if (!result.ok) throw new Error(`Host command '${key}' failed: ${detail || result.error || `exit code ${result.exitCode ?? "unknown"}`}`);
|
|
1806
|
+
return result;
|
|
1807
|
+
}, (error: unknown) => {
|
|
1808
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1809
|
+
hostStepChanged({ ...startedStep, state: "error", reasonCode: "execution_failed", detail: text.replace(/\s+/g, " ").slice(0, 200), updatedAt: Date.now() });
|
|
1810
|
+
trace.push({ operation: "host", key, state: "failed", durationMs: Date.now() - startedAt, error: text });
|
|
1811
|
+
traceChanged();
|
|
1812
|
+
throw error;
|
|
1813
|
+
});
|
|
1814
|
+
hostCalls.set(message.callId, { key, promise, observed: observedHostCalls.delete(message.callId) });
|
|
1815
|
+
respond(promise, `runs.host('${key}') result`);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1376
1818
|
if (message.method !== "run") return respond(Promise.reject(new Error(`Unknown runs API method '${message.method}'.`)));
|
|
1377
1819
|
|
|
1378
1820
|
let key: string;
|
|
@@ -1383,6 +1825,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1383
1825
|
}
|
|
1384
1826
|
const params = message.args.params;
|
|
1385
1827
|
if (!isRecord(params)) return respond(Promise.reject(new Error(`runs.run('${key}', params) requires a params object.`)));
|
|
1828
|
+
const generatedLaneKey = typeof message.args.generatedLaneKey === "string" && KEY_PATTERN.test(message.args.generatedLaneKey) && key.startsWith(`${message.args.generatedLaneKey}.`)
|
|
1829
|
+
? message.args.generatedLaneKey
|
|
1830
|
+
: undefined;
|
|
1386
1831
|
const collectFailure = message.args.collectFailure === true;
|
|
1387
1832
|
const callObserved = observedRunCalls.delete(message.callId);
|
|
1388
1833
|
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
@@ -1400,9 +1845,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1400
1845
|
if (existing) {
|
|
1401
1846
|
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
1402
1847
|
if (callObserved) existing.observed = true;
|
|
1403
|
-
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
|
|
1848
|
+
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params), ...(existing.generatedLaneKey ? { generatedLaneKey: existing.generatedLaneKey } : {}) });
|
|
1404
1849
|
traceChanged();
|
|
1405
|
-
return respond(deliver(existing.promise), `runs.run('${key}') result
|
|
1850
|
+
return respond(deliver(existing.promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
|
|
1406
1851
|
}
|
|
1407
1852
|
const permitError = options.oneUsePermit?.claim(key);
|
|
1408
1853
|
if (permitError) return respond(Promise.reject(new Error(permitError)));
|
|
@@ -1487,7 +1932,23 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1487
1932
|
if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
|
|
1488
1933
|
}
|
|
1489
1934
|
const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
|
|
1490
|
-
|
|
1935
|
+
await launchSemaphore.acquire();
|
|
1936
|
+
try {
|
|
1937
|
+
if (settled || finishing || stoppedLaunches.has(key) || childSignal.aborted) {
|
|
1938
|
+
const reason = childSignal.reason;
|
|
1939
|
+
const text = children.get(key)?.error ?? (reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.");
|
|
1940
|
+
return stoppedChildResult(key, text);
|
|
1941
|
+
}
|
|
1942
|
+
const result = await options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1943
|
+
const autoResumeParams = setupAbortResumeParams(params, result, childSignal);
|
|
1944
|
+
if (!autoResumeParams) return result;
|
|
1945
|
+
resolvedResumeLineage = [...new Set([...(resolvedResumeLineage ?? []), result.runId!])];
|
|
1946
|
+
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(autoResumeParams), ...(generatedLaneKey ? { generatedLaneKey } : {}), phase: "auto-resume", runId: result.runId });
|
|
1947
|
+
traceChanged();
|
|
1948
|
+
return options.launch(key, autoResumeParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1949
|
+
} finally {
|
|
1950
|
+
launchSemaphore.release();
|
|
1951
|
+
}
|
|
1491
1952
|
}).then((result) => {
|
|
1492
1953
|
let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
1493
1954
|
if (resolvedResumeLineage?.length && normalized.runId) {
|
|
@@ -1497,7 +1958,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1497
1958
|
if (stoppedLaunches.has(key)) return children.get(key) ?? normalized;
|
|
1498
1959
|
children.set(key, normalized);
|
|
1499
1960
|
const state = normalized.ok ? "completed" : normalized.stopped ? "stopped" : normalized.detached ? "detached" : "failed";
|
|
1500
|
-
trace.push({ operation: "run", key, state, durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
|
|
1961
|
+
trace.push({ operation: "run", key, state, durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
|
|
1501
1962
|
traceChanged();
|
|
1502
1963
|
return normalized;
|
|
1503
1964
|
}, (error: unknown) => {
|
|
@@ -1506,15 +1967,15 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1506
1967
|
childStopControllers.delete(key);
|
|
1507
1968
|
if (stoppedLaunches.has(key)) return children.get(key) ?? { ...failure, stopped: true };
|
|
1508
1969
|
children.set(key, failure);
|
|
1509
|
-
trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), error: text });
|
|
1970
|
+
trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), error: text });
|
|
1510
1971
|
traceChanged();
|
|
1511
1972
|
return failure;
|
|
1512
1973
|
});
|
|
1513
|
-
launches.set(key, { fingerprint, promise, observed: callObserved });
|
|
1974
|
+
launches.set(key, { fingerprint, promise, observed: callObserved, ...(generatedLaneKey ? { generatedLaneKey } : {}) });
|
|
1514
1975
|
childOrder.push(key);
|
|
1515
|
-
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
|
|
1976
|
+
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}) });
|
|
1516
1977
|
traceChanged();
|
|
1517
|
-
respond(deliver(promise), `runs.run('${key}') result
|
|
1978
|
+
respond(deliver(promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
|
|
1518
1979
|
});
|
|
1519
1980
|
|
|
1520
1981
|
worker.postMessage({ type: "start", script: options.script, stateEnabled: options.state !== undefined });
|