@signalridge/pi-workflows 1.7.5 → 1.9.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 +26 -0
- package/README.md +157 -10
- package/package.json +3 -2
- package/skills/workflow-authoring/SKILL.md +1 -1
- package/skills/workflow-authoring/references/capabilities.md +1 -1
- package/skills/workflow-authoring/references/capability-details.md +1 -1
- package/skills/workflow-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +1 -1
- package/src/arming.ts +264 -3
- package/src/engine.ts +1212 -379
- package/src/errors.ts +26 -0
- package/src/index.ts +578 -225
- package/src/inspection.ts +544 -0
- package/src/journal.ts +38 -11
- package/src/orchestrator.ts +22 -4
- package/src/runtime.ts +427 -168
- package/src/safe-text.ts +4 -35
- package/src/ui-bus.ts +243 -0
- package/src/ui-command-center.ts +1032 -0
- package/src/ui-inspection.ts +206 -0
- package/src/ui-presentation.ts +196 -0
- package/src/ui.ts +43 -241
- package/src/vm-bootstrap.ts +218 -0
- package/src/vm-helper.ts +502 -0
package/src/runtime.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A workflow is a raw JavaScript module. The runtime parses it under an acorn
|
|
5
5
|
* gate (deterministic literal `export const meta` first, no nondeterministic
|
|
6
|
-
* builtins), then executes the body
|
|
6
|
+
* builtins), then executes the body/callbacks in an owned Node helper's vm realm
|
|
7
|
+
* with validated IPC proxies to the parent-owned bounded set
|
|
7
8
|
* of orchestration globals (`agent`, `parallel`, `pipeline`, `workflow`,
|
|
8
9
|
* quality/control helpers, `phase`, `log`, `args`, `cwd`, restricted
|
|
9
10
|
* `process`, `budget`). The vm is a determinism guardrail, not a security
|
|
@@ -20,7 +21,6 @@
|
|
|
20
21
|
|
|
21
22
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
22
23
|
import { createHash } from "node:crypto";
|
|
23
|
-
import vm from "node:vm";
|
|
24
24
|
import type { ManagedRoutingPolicy } from "@signalridge/pi-subagents-protocol";
|
|
25
25
|
import { agentTierPolicyIdentity, isManagedAgentTier } from "@signalridge/pi-subagents-protocol";
|
|
26
26
|
import { parse } from "acorn";
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type WorkflowStrength,
|
|
41
41
|
type WorkflowStrengthTable,
|
|
42
42
|
} from "./strengths.js";
|
|
43
|
+
import { executeOwnedVm } from "./vm-helper.js";
|
|
43
44
|
|
|
44
45
|
// ── Meta contract ────────────────────────────────────────────────────────────
|
|
45
46
|
|
|
@@ -122,6 +123,11 @@ export interface WorkflowAgentRunner {
|
|
|
122
123
|
|
|
123
124
|
export interface AgentRunOptions {
|
|
124
125
|
label?: string;
|
|
126
|
+
/** Observation-only scope; never part of call hashing or dispatch policy. */
|
|
127
|
+
graphId?: string;
|
|
128
|
+
taskKey?: string;
|
|
129
|
+
taskId?: string;
|
|
130
|
+
phase?: string;
|
|
125
131
|
sessionName?: string;
|
|
126
132
|
/** Plain JSON Schema the result must satisfy; the runtime parses+validates client-side. */
|
|
127
133
|
schema?: unknown;
|
|
@@ -211,6 +217,10 @@ export interface CheckpointOptions {
|
|
|
211
217
|
|
|
212
218
|
export interface WorkflowRuntimeEvent {
|
|
213
219
|
type: "phase" | "workflow" | "quality" | "control-attempt" | "task";
|
|
220
|
+
/** Resolver-proven source classification for nested workflow observations. */
|
|
221
|
+
sourceKind?: "saved" | "inline";
|
|
222
|
+
/** Observation-only frame-scoped hash of the exact attributed phase title. */
|
|
223
|
+
phaseKey?: string;
|
|
214
224
|
[key: string]: unknown;
|
|
215
225
|
}
|
|
216
226
|
|
|
@@ -290,6 +300,8 @@ export interface WorkflowRunOptions {
|
|
|
290
300
|
agentTimeoutMs?: number | null;
|
|
291
301
|
runId?: string;
|
|
292
302
|
resumeJournal?: Map<string, JournalEntry>;
|
|
303
|
+
/** Accepted parent-frame nested generation watermarks, independent of replay eligibility. */
|
|
304
|
+
resumeWorkflowGenerations?: ReadonlyMap<number, number>;
|
|
293
305
|
resumeFromRunId?: string;
|
|
294
306
|
toolset?: string;
|
|
295
307
|
excludeTools?: string[];
|
|
@@ -303,14 +315,23 @@ export interface WorkflowRunOptions {
|
|
|
303
315
|
/** Resolve a nested `workflow(name)` reference to the script it names. */
|
|
304
316
|
loadSavedWorkflow?: (name: string) => string | undefined;
|
|
305
317
|
confirm?: (promptText: string, options: CheckpointOptions, signal?: AbortSignal) => Promise<unknown>;
|
|
306
|
-
onLog?: (message: string) => void;
|
|
318
|
+
onLog?: (message: string, scope?: { frameId: string }) => void;
|
|
307
319
|
onPhase?: (title: string) => void;
|
|
308
320
|
onRuntimeEvent?: (event: WorkflowRuntimeEvent) => void;
|
|
309
321
|
onAgentStart?: (event: {
|
|
310
322
|
id: string;
|
|
311
323
|
label: string;
|
|
312
324
|
phase?: string;
|
|
325
|
+
/** Observation-only exact title identity, scoped to this call's frame. */
|
|
326
|
+
phaseKey?: string;
|
|
313
327
|
prompt: string;
|
|
328
|
+
frameId?: string;
|
|
329
|
+
callIndex?: number;
|
|
330
|
+
agentType?: string;
|
|
331
|
+
replayed?: boolean;
|
|
332
|
+
graphId?: string;
|
|
333
|
+
taskKey?: string;
|
|
334
|
+
taskId?: string;
|
|
314
335
|
model?: string;
|
|
315
336
|
tier?: string;
|
|
316
337
|
}) => void;
|
|
@@ -318,6 +339,7 @@ export interface WorkflowRunOptions {
|
|
|
318
339
|
id: string;
|
|
319
340
|
label: string;
|
|
320
341
|
phase?: string;
|
|
342
|
+
phaseKey?: string;
|
|
321
343
|
tier?: string;
|
|
322
344
|
result: unknown;
|
|
323
345
|
tokens?: number;
|
|
@@ -391,6 +413,45 @@ export const MAX_AGENT_RETRIES = 3;
|
|
|
391
413
|
*/
|
|
392
414
|
export const DEFAULT_AGENT_TIMEOUT_MS: number | null = 1_800_000;
|
|
393
415
|
export const MAX_SCRIPT_SYNC_MS = 1_000;
|
|
416
|
+
/** Identities are retained verbatim, never clipped like display text. */
|
|
417
|
+
export const MAX_RUNTIME_IDENTITY_LENGTH = 2_048;
|
|
418
|
+
const MAX_ROOT_FRAME_LENGTH = 1_024;
|
|
419
|
+
const MAX_EXECUTION_NONCE_LENGTH = 512;
|
|
420
|
+
const MAX_WORKFLOW_GENERATION = 1_000_000;
|
|
421
|
+
|
|
422
|
+
function validateRuntimeIdentity(value: string, label: string, limit: number): void {
|
|
423
|
+
if (typeof value !== "string" || value.length === 0 || value.length > limit) {
|
|
424
|
+
throw new WorkflowError(
|
|
425
|
+
`${label} must be a non-empty identity of at most ${limit} characters`,
|
|
426
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
427
|
+
{ recoverable: false },
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function copyWorkflowGenerations(values?: ReadonlyMap<number, number>): Map<number, number> {
|
|
433
|
+
const generations = new Map<number, number>();
|
|
434
|
+
for (const [index, generation] of values ?? []) {
|
|
435
|
+
if (
|
|
436
|
+
!Number.isInteger(index) ||
|
|
437
|
+
index < 0 ||
|
|
438
|
+
index >= MAX_CALLS_PER_WORKFLOW ||
|
|
439
|
+
!Number.isInteger(generation) ||
|
|
440
|
+
generation < 1 ||
|
|
441
|
+
generation > MAX_WORKFLOW_GENERATION
|
|
442
|
+
) {
|
|
443
|
+
throw new WorkflowError(
|
|
444
|
+
"Invalid nested workflow generation watermark",
|
|
445
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
446
|
+
{
|
|
447
|
+
recoverable: false,
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
generations.set(index, generation);
|
|
452
|
+
}
|
|
453
|
+
return generations;
|
|
454
|
+
}
|
|
394
455
|
|
|
395
456
|
// ── Determinism guardrail ────────────────────────────────────────────────────
|
|
396
457
|
|
|
@@ -895,7 +956,8 @@ export function normalizeMaxAgents(value: unknown): number {
|
|
|
895
956
|
export function normalizeAgentTimeout(value: unknown): number | null {
|
|
896
957
|
if (value === null) return null;
|
|
897
958
|
if (value === undefined) return DEFAULT_AGENT_TIMEOUT_MS;
|
|
898
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 1
|
|
959
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 1 || value > 2_147_483_647)
|
|
960
|
+
return DEFAULT_AGENT_TIMEOUT_MS;
|
|
899
961
|
return Math.floor(value);
|
|
900
962
|
}
|
|
901
963
|
|
|
@@ -1028,7 +1090,10 @@ function validateValue(value: unknown, schema: SchemaNode, path: string): string
|
|
|
1028
1090
|
if (type !== undefined) {
|
|
1029
1091
|
const types = Array.isArray(type) ? type : [type];
|
|
1030
1092
|
const typeOf = (v: unknown): string => (v === null ? "null" : Array.isArray(v) ? "array" : typeof v);
|
|
1031
|
-
|
|
1093
|
+
const matches = types.some((expected) =>
|
|
1094
|
+
expected === "integer" ? typeof value === "number" && Number.isInteger(value) : expected === typeOf(value),
|
|
1095
|
+
);
|
|
1096
|
+
if (!matches) {
|
|
1032
1097
|
errors.push(`${path}: expected type ${types.join("/")}, got ${typeOf(value)}`);
|
|
1033
1098
|
return errors;
|
|
1034
1099
|
}
|
|
@@ -1228,7 +1293,13 @@ export interface WorkflowRuntimeBindings {
|
|
|
1228
1293
|
/** Fan-out cancellation scope: parallel()/pipeline() batches mark themselves. */
|
|
1229
1294
|
const fanoutScope = new AsyncLocalStorage<{ cancelled: boolean }>();
|
|
1230
1295
|
/** Task-local phase attribution for named orchestration callbacks. */
|
|
1231
|
-
const orchestrationScope = new AsyncLocalStorage<{
|
|
1296
|
+
const orchestrationScope = new AsyncLocalStorage<{
|
|
1297
|
+
frameId: string;
|
|
1298
|
+
graphId: string;
|
|
1299
|
+
taskKey: string;
|
|
1300
|
+
taskId: string;
|
|
1301
|
+
phase?: string;
|
|
1302
|
+
}>();
|
|
1232
1303
|
|
|
1233
1304
|
// ── The run ──────────────────────────────────────────────────────────────────
|
|
1234
1305
|
|
|
@@ -1237,10 +1308,24 @@ export async function runWorkflow<T = unknown>(
|
|
|
1237
1308
|
options: WorkflowRunOptions = {},
|
|
1238
1309
|
): Promise<WorkflowRunResult<T>> {
|
|
1239
1310
|
const started = Date.now();
|
|
1311
|
+
// Each frame owns cancellation, even when a parent catches a nested failure.
|
|
1312
|
+
const frameController = new AbortController();
|
|
1313
|
+
options = {
|
|
1314
|
+
...options,
|
|
1315
|
+
signal: options.signal ? AbortSignal.any([options.signal, frameController.signal]) : frameController.signal,
|
|
1316
|
+
};
|
|
1240
1317
|
const { meta, body } = parseWorkflowScript(script);
|
|
1241
1318
|
const maxAgents = normalizeMaxAgents(options.maxAgents ?? MAX_AGENTS_PER_RUN);
|
|
1242
1319
|
const agentTimeoutMs = normalizeAgentTimeout(options.agentTimeoutMs);
|
|
1243
1320
|
const runId = options.runId ?? `run-${started.toString(36)}`;
|
|
1321
|
+
// Reserve room for the one supported child level, nonce and graph/call suffixes.
|
|
1322
|
+
validateRuntimeIdentity(
|
|
1323
|
+
runId,
|
|
1324
|
+
"runId",
|
|
1325
|
+
options.workflowDepth ? MAX_RUNTIME_IDENTITY_LENGTH - 64 : MAX_ROOT_FRAME_LENGTH,
|
|
1326
|
+
);
|
|
1327
|
+
if (options.executionNonce !== undefined)
|
|
1328
|
+
validateRuntimeIdentity(options.executionNonce, "executionNonce", MAX_EXECUTION_NONCE_LENGTH);
|
|
1244
1329
|
const baseCwd = options.cwd ?? process.cwd();
|
|
1245
1330
|
// The host's catalogue, known before the first call. A script cannot name a
|
|
1246
1331
|
// tier, so this is not a typo check: it is what the strength table is resolved
|
|
@@ -1260,7 +1345,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1260
1345
|
currentPhase: meta.phases?.[0]?.title,
|
|
1261
1346
|
phaseBudgets: new Map(),
|
|
1262
1347
|
callSeq: 0,
|
|
1263
|
-
workflowGenerations:
|
|
1348
|
+
workflowGenerations: copyWorkflowGenerations(options.resumeWorkflowGenerations),
|
|
1264
1349
|
firstMiss: Number.POSITIVE_INFINITY,
|
|
1265
1350
|
};
|
|
1266
1351
|
|
|
@@ -1294,7 +1379,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1294
1379
|
const log = (message: unknown) => {
|
|
1295
1380
|
const text = String(message);
|
|
1296
1381
|
state.logs.push(text);
|
|
1297
|
-
options.onLog?.(text);
|
|
1382
|
+
options.onLog?.(text, { frameId: runId });
|
|
1298
1383
|
};
|
|
1299
1384
|
if (concurrencyClampNotice !== undefined) log(concurrencyClampNotice);
|
|
1300
1385
|
|
|
@@ -1384,14 +1469,39 @@ export async function runWorkflow<T = unknown>(
|
|
|
1384
1469
|
);
|
|
1385
1470
|
};
|
|
1386
1471
|
|
|
1472
|
+
let graphSequence = 0;
|
|
1473
|
+
let helperSequence = 0;
|
|
1387
1474
|
const emitRuntimeEvent = (event: WorkflowRuntimeEvent): void => {
|
|
1388
1475
|
try {
|
|
1389
|
-
|
|
1476
|
+
const scope = orchestrationScope.getStore();
|
|
1477
|
+
options.onRuntimeEvent?.(
|
|
1478
|
+
structuredClone({
|
|
1479
|
+
// Origin task scope can belong to the parent; emission frame cannot.
|
|
1480
|
+
...(scope ? { graphId: scope.graphId, taskKey: scope.taskKey, taskId: scope.taskId } : {}),
|
|
1481
|
+
...event,
|
|
1482
|
+
// A child's forwarded events must never look like its parent's phase.
|
|
1483
|
+
frameId: runId,
|
|
1484
|
+
runId,
|
|
1485
|
+
}),
|
|
1486
|
+
);
|
|
1390
1487
|
} catch {
|
|
1391
1488
|
// Runtime observers are diagnostic surfaces and must not change execution.
|
|
1392
1489
|
}
|
|
1393
1490
|
};
|
|
1394
1491
|
|
|
1492
|
+
// Observation identity is independent of bounded display text and call hashing.
|
|
1493
|
+
const phaseObservation = (title: string | undefined) => ({
|
|
1494
|
+
phase: title,
|
|
1495
|
+
phaseKey:
|
|
1496
|
+
title === undefined
|
|
1497
|
+
? undefined
|
|
1498
|
+
: createHash("sha256")
|
|
1499
|
+
.update(JSON.stringify([runId, title]))
|
|
1500
|
+
.digest("hex"),
|
|
1501
|
+
});
|
|
1502
|
+
const taskObservation = (scope: ReturnType<typeof orchestrationScope.getStore>) =>
|
|
1503
|
+
scope ? { graphId: scope.graphId, taskKey: scope.taskKey, taskId: scope.taskId } : {};
|
|
1504
|
+
|
|
1395
1505
|
const phase = (title: string, phaseOptions?: { budget?: number }) => {
|
|
1396
1506
|
state.currentPhase = title;
|
|
1397
1507
|
if (!state.phases.includes(title)) state.phases.push(title);
|
|
@@ -1404,6 +1514,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1404
1514
|
emitRuntimeEvent({
|
|
1405
1515
|
type: "phase",
|
|
1406
1516
|
title,
|
|
1517
|
+
phaseKey: phaseObservation(title).phaseKey,
|
|
1407
1518
|
budget: typeof phaseOptions?.budget === "number" && phaseOptions.budget > 0 ? phaseOptions.budget : null,
|
|
1408
1519
|
});
|
|
1409
1520
|
};
|
|
@@ -1489,6 +1600,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
1489
1600
|
// synchronously, before suspending on the limiter below.
|
|
1490
1601
|
const batch = fanoutScope.getStore();
|
|
1491
1602
|
|
|
1603
|
+
// Preserve inherited phase attribution (and therefore existing call hashes)
|
|
1604
|
+
// across nested frames; a task key names its origin graph, not the call's frame.
|
|
1492
1605
|
const taskScope = orchestrationScope.getStore();
|
|
1493
1606
|
const assignedPhase = agentOptions.phase ?? taskScope?.phase ?? state.currentPhase;
|
|
1494
1607
|
|
|
@@ -1538,6 +1651,13 @@ export async function runWorkflow<T = unknown>(
|
|
|
1538
1651
|
const deltaKey = `${runId}:${callIndex}`;
|
|
1539
1652
|
|
|
1540
1653
|
const label = requestedLabel || defaultAgentLabel(assignedPhase, shared.agentCount + 1);
|
|
1654
|
+
const observation = {
|
|
1655
|
+
frameId: runId,
|
|
1656
|
+
callIndex,
|
|
1657
|
+
agentType: agentOptions.agentType ?? "general-purpose",
|
|
1658
|
+
...phaseObservation(assignedPhase),
|
|
1659
|
+
...taskObservation(taskScope),
|
|
1660
|
+
};
|
|
1541
1661
|
|
|
1542
1662
|
// Longest-unchanged-prefix resume: replay a cached result only while the
|
|
1543
1663
|
// prefix is still intact. Once any call misses, it AND everything after it
|
|
@@ -1556,6 +1676,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
1556
1676
|
const replayTier = cached.tier ?? effectiveTier;
|
|
1557
1677
|
options.onAgentStart?.({
|
|
1558
1678
|
id: deltaKey,
|
|
1679
|
+
...observation,
|
|
1680
|
+
replayed: true,
|
|
1559
1681
|
label,
|
|
1560
1682
|
phase: assignedPhase,
|
|
1561
1683
|
prompt: promptText,
|
|
@@ -1564,7 +1686,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1564
1686
|
options.onAgentEnd?.({
|
|
1565
1687
|
id: deltaKey,
|
|
1566
1688
|
label,
|
|
1567
|
-
|
|
1689
|
+
...phaseObservation(assignedPhase),
|
|
1568
1690
|
tier: replayTier,
|
|
1569
1691
|
result: cached.result,
|
|
1570
1692
|
tokens: 0,
|
|
@@ -1616,12 +1738,15 @@ export async function runWorkflow<T = unknown>(
|
|
|
1616
1738
|
throwIfAborted();
|
|
1617
1739
|
if (batch?.cancelled) throw agentLimitError();
|
|
1618
1740
|
|
|
1619
|
-
const timeout =
|
|
1741
|
+
const timeout =
|
|
1742
|
+
agentOptions.timeoutMs !== undefined ? normalizeAgentTimeout(agentOptions.timeoutMs) : agentTimeoutMs;
|
|
1620
1743
|
const retryAttempts = normalizeAgentRetries(agentOptions.retries ?? options.agentRetries ?? 0);
|
|
1621
1744
|
const maxAttempts = retryAttempts + 1;
|
|
1622
1745
|
|
|
1623
1746
|
options.onAgentStart?.({
|
|
1624
1747
|
id: deltaKey,
|
|
1748
|
+
...observation,
|
|
1749
|
+
replayed: false,
|
|
1625
1750
|
label,
|
|
1626
1751
|
phase: assignedPhase,
|
|
1627
1752
|
prompt: promptText,
|
|
@@ -1637,7 +1762,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1637
1762
|
// failed attempt does not double-count the next one.
|
|
1638
1763
|
let usage: AgentUsage | undefined;
|
|
1639
1764
|
const recordTokens = (result: unknown): number => {
|
|
1640
|
-
const tokens = usage
|
|
1765
|
+
const tokens = usage === undefined ? estimateTokens(result) + estimateTokens(promptText) : usage.total;
|
|
1641
1766
|
if (usage) {
|
|
1642
1767
|
shared.tokenUsage.input += usage.input;
|
|
1643
1768
|
shared.tokenUsage.output += usage.output;
|
|
@@ -1699,6 +1824,10 @@ export async function runWorkflow<T = unknown>(
|
|
|
1699
1824
|
let restartTimeout: (() => void) | undefined;
|
|
1700
1825
|
const runPromise = agentRunner.run(promptText + schemaInstruction + repairFeedback, {
|
|
1701
1826
|
label,
|
|
1827
|
+
phase: assignedPhase,
|
|
1828
|
+
...(taskScope
|
|
1829
|
+
? { graphId: taskScope.graphId, taskKey: taskScope.taskKey, taskId: taskScope.taskId }
|
|
1830
|
+
: {}),
|
|
1702
1831
|
sessionName: agentOptions.thread
|
|
1703
1832
|
? `workflow:${runId} thread:${agentOptions.thread}`
|
|
1704
1833
|
: `workflow:${runId} ${label}`,
|
|
@@ -1709,7 +1838,10 @@ export async function runWorkflow<T = unknown>(
|
|
|
1709
1838
|
agentType: agentOptions.agentType,
|
|
1710
1839
|
isolation: resolvedIsolation,
|
|
1711
1840
|
toolset: options.toolset ?? agentOptions.toolset,
|
|
1712
|
-
excludeTools:
|
|
1841
|
+
excludeTools:
|
|
1842
|
+
options.excludeTools === undefined && agentOptions.excludeTools === undefined
|
|
1843
|
+
? undefined
|
|
1844
|
+
: [...new Set([...(options.excludeTools ?? []), ...(agentOptions.excludeTools ?? [])])],
|
|
1713
1845
|
runId,
|
|
1714
1846
|
callIndex,
|
|
1715
1847
|
callHash,
|
|
@@ -1800,7 +1932,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1800
1932
|
options.onAgentEnd?.({
|
|
1801
1933
|
id: deltaKey,
|
|
1802
1934
|
label,
|
|
1803
|
-
|
|
1935
|
+
...phaseObservation(assignedPhase),
|
|
1804
1936
|
result,
|
|
1805
1937
|
tier: resolvedTier,
|
|
1806
1938
|
tokens,
|
|
@@ -1842,7 +1974,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1842
1974
|
options.onAgentEnd?.({
|
|
1843
1975
|
id: deltaKey,
|
|
1844
1976
|
label,
|
|
1845
|
-
|
|
1977
|
+
...phaseObservation(assignedPhase),
|
|
1846
1978
|
result: null,
|
|
1847
1979
|
tier: resolvedTier,
|
|
1848
1980
|
tokens,
|
|
@@ -1973,11 +2105,20 @@ export async function runWorkflow<T = unknown>(
|
|
|
1973
2105
|
const orchestrate = async (rawTasks: unknown, rawOptions?: unknown): Promise<OrchestrationResult> => {
|
|
1974
2106
|
throwIfAborted();
|
|
1975
2107
|
const normalized = normalizeOrchestration(rawTasks, rawOptions);
|
|
2108
|
+
const graphId = `${runId}:graph-${++graphSequence}`;
|
|
2109
|
+
const taskKeys = new Map(normalized.tasks.map((task, index) => [task.id, `${graphId}:task-${index}`]));
|
|
1976
2110
|
const tasks = normalized.tasks.map((task) => ({
|
|
1977
2111
|
...task,
|
|
1978
2112
|
run: (context: OrchestrationTaskContext) =>
|
|
1979
|
-
orchestrationScope.run(
|
|
1980
|
-
|
|
2113
|
+
orchestrationScope.run(
|
|
2114
|
+
{
|
|
2115
|
+
frameId: runId,
|
|
2116
|
+
graphId,
|
|
2117
|
+
taskKey: taskKeys.get(task.id) ?? "",
|
|
2118
|
+
taskId: task.id,
|
|
2119
|
+
...(task.phase === undefined ? {} : { phase: task.phase }),
|
|
2120
|
+
},
|
|
2121
|
+
() => task.run(context),
|
|
1981
2122
|
),
|
|
1982
2123
|
}));
|
|
1983
2124
|
return executeOrchestration(tasks, normalized.options, {
|
|
@@ -1988,7 +2129,15 @@ export async function runWorkflow<T = unknown>(
|
|
|
1988
2129
|
return !workflowError.recoverable || workflowError.code === WorkflowErrorCode.PROVIDER_USAGE_LIMIT;
|
|
1989
2130
|
},
|
|
1990
2131
|
formatError: (error: unknown) => wrapError(error).message,
|
|
1991
|
-
onEvent:
|
|
2132
|
+
onEvent: (event) => {
|
|
2133
|
+
const task = normalized.tasks.find((candidate) => candidate.id === event.taskId);
|
|
2134
|
+
emitRuntimeEvent({
|
|
2135
|
+
...event,
|
|
2136
|
+
graphId,
|
|
2137
|
+
taskKey: taskKeys.get(event.taskId),
|
|
2138
|
+
...(task?.description === undefined ? {} : { description: task.description }),
|
|
2139
|
+
});
|
|
2140
|
+
},
|
|
1992
2141
|
});
|
|
1993
2142
|
};
|
|
1994
2143
|
|
|
@@ -2005,8 +2154,18 @@ export async function runWorkflow<T = unknown>(
|
|
|
2005
2154
|
});
|
|
2006
2155
|
}
|
|
2007
2156
|
const callIndex = nextCallIndex();
|
|
2008
|
-
const
|
|
2157
|
+
const taskScope = orchestrationScope.getStore();
|
|
2158
|
+
const workflowPhase = taskScope?.phase ?? state.currentPhase;
|
|
2009
2159
|
const workflowName = String(nameOrScript);
|
|
2160
|
+
const savedScript = options.loadSavedWorkflow?.(workflowName);
|
|
2161
|
+
const childScript = savedScript ?? workflowName;
|
|
2162
|
+
const observation = {
|
|
2163
|
+
...phaseObservation(workflowPhase),
|
|
2164
|
+
...taskObservation(taskScope),
|
|
2165
|
+
// Classify the resolver result, never source syntax or display text.
|
|
2166
|
+
sourceKind: savedScript === undefined ? ("inline" as const) : ("saved" as const),
|
|
2167
|
+
name: savedScript === undefined ? "nested workflow" : workflowName,
|
|
2168
|
+
};
|
|
2010
2169
|
const argsPresent = childArgs !== undefined;
|
|
2011
2170
|
const callHash = hashIdentity({
|
|
2012
2171
|
kind: "workflow",
|
|
@@ -2026,17 +2185,41 @@ export async function runWorkflow<T = unknown>(
|
|
|
2026
2185
|
});
|
|
2027
2186
|
const cached = options.resumeJournal?.get(`${runId}:${callIndex}`);
|
|
2028
2187
|
if (cached != null && cached.hash === callHash && callIndex < state.firstMiss) {
|
|
2029
|
-
state.workflowGenerations.set(
|
|
2188
|
+
state.workflowGenerations.set(
|
|
2189
|
+
callIndex,
|
|
2190
|
+
Math.max(state.workflowGenerations.get(callIndex) ?? 0, cached.generation ?? 1),
|
|
2191
|
+
);
|
|
2030
2192
|
if (cached.storeDelta) store.applyDelta(cached.storeDelta);
|
|
2031
2193
|
const replayedAgentCount = cached.agentCount ?? 0;
|
|
2032
2194
|
shared.agentCount += replayedAgentCount;
|
|
2033
2195
|
frameAgentCount += replayedAgentCount;
|
|
2196
|
+
emitRuntimeEvent({
|
|
2197
|
+
type: "workflow",
|
|
2198
|
+
stage: "replay",
|
|
2199
|
+
...observation,
|
|
2200
|
+
callIndex,
|
|
2201
|
+
replayedAgentCount,
|
|
2202
|
+
});
|
|
2034
2203
|
return cached.result;
|
|
2035
2204
|
}
|
|
2036
2205
|
if (cached == null || cached.hash !== callHash) state.firstMiss = Math.min(state.firstMiss, callIndex);
|
|
2037
|
-
const generation = (state.workflowGenerations.get(callIndex) ?? cached?.generation ?? 0) + 1;
|
|
2206
|
+
const generation = Math.max(state.workflowGenerations.get(callIndex) ?? 0, cached?.generation ?? 0) + 1;
|
|
2207
|
+
if (generation > MAX_WORKFLOW_GENERATION) {
|
|
2208
|
+
throw new WorkflowError("Nested workflow generation limit exceeded", WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, {
|
|
2209
|
+
recoverable: false,
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2038
2212
|
state.workflowGenerations.set(callIndex, generation);
|
|
2039
|
-
|
|
2213
|
+
const childFrameId = `${runId}-nested-call-${callIndex}-g${generation}${options.executionNonce ? `-${options.executionNonce}` : ""}`;
|
|
2214
|
+
emitRuntimeEvent({
|
|
2215
|
+
type: "workflow",
|
|
2216
|
+
stage: "start",
|
|
2217
|
+
...observation,
|
|
2218
|
+
args: childArgs,
|
|
2219
|
+
callIndex,
|
|
2220
|
+
childFrameId,
|
|
2221
|
+
});
|
|
2222
|
+
let outcome: "completed" | "failed" | "cancelled" = "failed";
|
|
2040
2223
|
try {
|
|
2041
2224
|
const childDeltas = new Map<number, Record<string, unknown>>();
|
|
2042
2225
|
const child = await runWorkflow(childScript, {
|
|
@@ -2049,6 +2232,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
2049
2232
|
// call results here would duplicate nested calls and make the synthetic
|
|
2050
2233
|
// parent journal entry ambiguous.
|
|
2051
2234
|
resumeJournal: undefined,
|
|
2235
|
+
// These watermarks belong to the parent lexical namespace only.
|
|
2236
|
+
resumeWorkflowGenerations: undefined,
|
|
2052
2237
|
resumeFromRunId: undefined,
|
|
2053
2238
|
onWorkflowJournal: undefined,
|
|
2054
2239
|
onAgentJournal: (entry) => {
|
|
@@ -2060,7 +2245,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
2060
2245
|
// The call index + generation keeps changed nested calls distinct across
|
|
2061
2246
|
// resume. The engine nonce additionally fences an interrupted child whose
|
|
2062
2247
|
// result was never durable before a process/branch restart.
|
|
2063
|
-
runId:
|
|
2248
|
+
runId: childFrameId,
|
|
2064
2249
|
});
|
|
2065
2250
|
const normalizedChildResult = child.result === undefined ? null : child.result;
|
|
2066
2251
|
const storeDelta: Record<string, unknown> = {};
|
|
@@ -2079,14 +2264,51 @@ export async function runWorkflow<T = unknown>(
|
|
|
2079
2264
|
agentCount: child.agentCount,
|
|
2080
2265
|
...(Object.keys(storeDelta).length > 0 ? { storeDelta } : {}),
|
|
2081
2266
|
});
|
|
2267
|
+
outcome = "completed";
|
|
2082
2268
|
return normalizedChildResult;
|
|
2083
2269
|
} finally {
|
|
2084
|
-
|
|
2270
|
+
if (isAborted()) outcome = "cancelled";
|
|
2271
|
+
emitRuntimeEvent({
|
|
2272
|
+
type: "workflow",
|
|
2273
|
+
stage: "end",
|
|
2274
|
+
...observation,
|
|
2275
|
+
args: childArgs,
|
|
2276
|
+
callIndex,
|
|
2277
|
+
childFrameId,
|
|
2278
|
+
outcome,
|
|
2279
|
+
});
|
|
2085
2280
|
}
|
|
2086
2281
|
};
|
|
2087
2282
|
|
|
2088
2283
|
// ── Quality-pattern stdlib ──────────────────────────────────────────────────
|
|
2089
2284
|
|
|
2285
|
+
const observeQuality = async <T>(helper: string, run: () => Promise<T>): Promise<T> => {
|
|
2286
|
+
const helperId = `${runId}:helper-${++helperSequence}`;
|
|
2287
|
+
emitRuntimeEvent({ type: "quality", stage: "start", helper, helperId });
|
|
2288
|
+
let outcome: "completed" | "failed" | "cancelled" = "failed";
|
|
2289
|
+
let accepted: boolean | undefined;
|
|
2290
|
+
try {
|
|
2291
|
+
const value = await run();
|
|
2292
|
+
outcome = "completed";
|
|
2293
|
+
if (value && typeof value === "object") {
|
|
2294
|
+
const verdict = value as Record<string, unknown>;
|
|
2295
|
+
if (typeof verdict.real === "boolean") accepted = verdict.real;
|
|
2296
|
+
if (typeof verdict.complete === "boolean") accepted = verdict.complete;
|
|
2297
|
+
}
|
|
2298
|
+
return value;
|
|
2299
|
+
} finally {
|
|
2300
|
+
if (isAborted()) outcome = "cancelled";
|
|
2301
|
+
emitRuntimeEvent({
|
|
2302
|
+
type: "quality",
|
|
2303
|
+
stage: "end",
|
|
2304
|
+
helper,
|
|
2305
|
+
helperId,
|
|
2306
|
+
outcome,
|
|
2307
|
+
...(accepted === undefined ? {} : { accepted }),
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2090
2312
|
const VERIFY_SCHEMA = {
|
|
2091
2313
|
type: "object",
|
|
2092
2314
|
properties: { real: { type: "boolean" }, reason: { type: "string" } },
|
|
@@ -2101,33 +2323,34 @@ export async function runWorkflow<T = unknown>(
|
|
|
2101
2323
|
total: number;
|
|
2102
2324
|
votes: Array<{ real: boolean; reason?: string }>;
|
|
2103
2325
|
}> => {
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2326
|
+
return observeQuality("verify", async () => {
|
|
2327
|
+
const reviewers = Math.max(1, opts.reviewers ?? 2);
|
|
2328
|
+
const threshold = opts.threshold ?? 0.5;
|
|
2329
|
+
const lenses = opts.lens ? (Array.isArray(opts.lens) ? opts.lens : [opts.lens]) : [];
|
|
2330
|
+
const claim = typeof item === "string" ? item : JSON.stringify(item);
|
|
2331
|
+
const votes = (
|
|
2332
|
+
await parallel(
|
|
2333
|
+
Array.from(
|
|
2334
|
+
{ length: reviewers },
|
|
2335
|
+
(_v, i) => () =>
|
|
2336
|
+
agent(
|
|
2337
|
+
`Adversarially review whether the following is REAL/correct. Try to refute it; default to real=false if unsure.${lenses.length ? ` Focus lens: ${lenses[i % lenses.length]}.` : ""}\n\n${claim}`,
|
|
2338
|
+
// Defaults low: numerous, narrow, and aggregated by the vote
|
|
2339
|
+
// below, which is what carries the gate rather than any single
|
|
2340
|
+
// reviewer's depth. A caller who wants a deeper gate says so.
|
|
2341
|
+
{ label: `verify ${i + 1}`, strength: opts.strength ?? "low", schema: VERIFY_SCHEMA },
|
|
2342
|
+
),
|
|
2343
|
+
),
|
|
2344
|
+
)
|
|
2345
|
+
).filter(Boolean) as Array<{ real?: boolean; reason?: string }>;
|
|
2346
|
+
const realCount = votes.filter((v) => v?.real).length;
|
|
2347
|
+
return {
|
|
2348
|
+
real: votes.length > 0 && realCount / votes.length >= threshold,
|
|
2349
|
+
realCount,
|
|
2350
|
+
total: votes.length,
|
|
2351
|
+
votes: votes.map((v) => ({ real: Boolean(v?.real), reason: v?.reason })),
|
|
2352
|
+
};
|
|
2353
|
+
});
|
|
2131
2354
|
};
|
|
2132
2355
|
|
|
2133
2356
|
const JUDGE_SCHEMA = {
|
|
@@ -2141,58 +2364,53 @@ export async function runWorkflow<T = unknown>(
|
|
|
2141
2364
|
): Promise<
|
|
2142
2365
|
{ index: number; attempt: unknown; score: number; judgments: Array<{ score: number; reason?: string }> } | undefined
|
|
2143
2366
|
> => {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
)
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
}
|
|
2192
|
-
let best = normalized[0];
|
|
2193
|
-
for (const s of normalized) if (s.score > best.score || (s.score === best.score && s.index < best.index)) best = s;
|
|
2194
|
-
emitRuntimeEvent({ type: "quality", stage: "end", helper: "judgePanel" });
|
|
2195
|
-
return best;
|
|
2367
|
+
return observeQuality("judgePanel", async () => {
|
|
2368
|
+
if (!Array.isArray(attempts) || attempts.length === 0) return undefined;
|
|
2369
|
+
const judges = Math.min(16, Math.max(1, Math.floor(opts.judges ?? 3)));
|
|
2370
|
+
const rubric = opts.rubric ?? "overall quality and correctness";
|
|
2371
|
+
const scored = (
|
|
2372
|
+
await parallel(
|
|
2373
|
+
(Array.isArray(attempts) ? attempts : []).map((att, idx) => async () => {
|
|
2374
|
+
const text = typeof att === "string" ? att : JSON.stringify(att);
|
|
2375
|
+
const js = (
|
|
2376
|
+
await parallel(
|
|
2377
|
+
Array.from(
|
|
2378
|
+
{ length: judges },
|
|
2379
|
+
(_v, j) => () =>
|
|
2380
|
+
agent(
|
|
2381
|
+
`Score this candidate from 0 to 1 on: ${rubric}. Reply with the score.\n\nCandidate:\n${text}`,
|
|
2382
|
+
{
|
|
2383
|
+
label: `judge ${idx + 1}.${j + 1}`,
|
|
2384
|
+
// Same shape as verify: many narrow scores, aggregated.
|
|
2385
|
+
strength: opts.strength ?? "low",
|
|
2386
|
+
schema: JUDGE_SCHEMA,
|
|
2387
|
+
},
|
|
2388
|
+
),
|
|
2389
|
+
),
|
|
2390
|
+
)
|
|
2391
|
+
).filter(Boolean) as Array<{ score?: number }>;
|
|
2392
|
+
const score = js.length ? js.reduce((s, v) => s + (Number(v?.score) || 0), 0) / js.length : 0;
|
|
2393
|
+
return { index: idx, attempt: att, score, judgments: js };
|
|
2394
|
+
}),
|
|
2395
|
+
)
|
|
2396
|
+
).filter(Boolean) as Array<{
|
|
2397
|
+
index: number;
|
|
2398
|
+
attempt: unknown;
|
|
2399
|
+
score: number;
|
|
2400
|
+
judgments: Array<{ score?: number; reason?: string }>;
|
|
2401
|
+
}>;
|
|
2402
|
+
const normalized = scored.map((s) => ({
|
|
2403
|
+
index: s.index,
|
|
2404
|
+
attempt: s.attempt,
|
|
2405
|
+
score: s.score,
|
|
2406
|
+
judgments: s.judgments.map((j) => ({ score: Number(j?.score) || 0, reason: j?.reason })),
|
|
2407
|
+
}));
|
|
2408
|
+
if (normalized.length === 0) return undefined;
|
|
2409
|
+
let best = normalized[0];
|
|
2410
|
+
for (const s of normalized)
|
|
2411
|
+
if (s.score > best.score || (s.score === best.score && s.index < best.index)) best = s;
|
|
2412
|
+
return best;
|
|
2413
|
+
});
|
|
2196
2414
|
};
|
|
2197
2415
|
|
|
2198
2416
|
const loopUntilDry = async (opts: {
|
|
@@ -2220,15 +2438,20 @@ export async function runWorkflow<T = unknown>(
|
|
|
2220
2438
|
if (code === WorkflowErrorCode.TOKEN_BUDGET_EXHAUSTED || code === WorkflowErrorCode.AGENT_LIMIT_EXCEEDED) break;
|
|
2221
2439
|
throw error;
|
|
2222
2440
|
}
|
|
2223
|
-
const fresh
|
|
2441
|
+
const fresh: Array<{ item: unknown; key: string }> = [];
|
|
2442
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
2443
|
+
if (item == null) continue;
|
|
2444
|
+
const identity = await key(item);
|
|
2445
|
+
if (!seen.has(identity)) fresh.push({ item, key: identity });
|
|
2446
|
+
}
|
|
2224
2447
|
if (!fresh.length) {
|
|
2225
2448
|
dry++;
|
|
2226
2449
|
continue;
|
|
2227
2450
|
}
|
|
2228
2451
|
dry = 0;
|
|
2229
|
-
for (const
|
|
2230
|
-
seen.add(key
|
|
2231
|
-
all.push(
|
|
2452
|
+
for (const entry of fresh) {
|
|
2453
|
+
seen.add(entry.key);
|
|
2454
|
+
all.push(entry.item);
|
|
2232
2455
|
}
|
|
2233
2456
|
}
|
|
2234
2457
|
return all;
|
|
@@ -2244,16 +2467,16 @@ export async function runWorkflow<T = unknown>(
|
|
|
2244
2467
|
results: unknown,
|
|
2245
2468
|
opts: { strength?: WorkflowStrength } = {},
|
|
2246
2469
|
): Promise<{ complete: boolean; missing?: string[] } | null> => {
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2470
|
+
return observeQuality("completenessCheck", async () => {
|
|
2471
|
+
const verdict = await agent(
|
|
2472
|
+
`Given the task and the results gathered so far, list what is still MISSING (modalities not covered, claims unverified, gaps). Be specific and concise.\n\nTask:\n${JSON.stringify(taskArgs)}\n\nResults so far:\n${JSON.stringify(results).slice(0, 4000)}`,
|
|
2473
|
+
// Defaults medium, not low like the other two gates: one call, no vote to
|
|
2474
|
+
// average over, and an open-ended read of the whole task rather than one
|
|
2475
|
+
// claim, so there is no redundancy here to carry a shallow verdict.
|
|
2476
|
+
{ label: "completeness critic", strength: opts.strength ?? "medium", schema: COMPLETENESS_SCHEMA },
|
|
2477
|
+
);
|
|
2478
|
+
return verdict as { complete: boolean; missing?: string[] } | null;
|
|
2479
|
+
});
|
|
2257
2480
|
};
|
|
2258
2481
|
|
|
2259
2482
|
const retry = async (
|
|
@@ -2265,7 +2488,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
2265
2488
|
for (let i = 0; i < attempts; i++) {
|
|
2266
2489
|
throwIfAborted();
|
|
2267
2490
|
last = await thunk(i);
|
|
2268
|
-
const accepted = !opts.until || opts.until(last);
|
|
2491
|
+
const accepted = !opts.until || (await opts.until(last));
|
|
2269
2492
|
emitRuntimeEvent({ type: "control-attempt", helper: "retry", attempt: i + 1, accepted });
|
|
2270
2493
|
if (accepted) return last;
|
|
2271
2494
|
}
|
|
@@ -2302,10 +2525,22 @@ export async function runWorkflow<T = unknown>(
|
|
|
2302
2525
|
const callIndex = nextCallIndex();
|
|
2303
2526
|
const callHash = hashCheckpoint(promptText, checkpointOptions);
|
|
2304
2527
|
const journalKey = `${runId}:${callIndex}`;
|
|
2528
|
+
const taskScope = orchestrationScope.getStore();
|
|
2529
|
+
const observation = {
|
|
2530
|
+
...phaseObservation(taskScope?.phase ?? state.currentPhase),
|
|
2531
|
+
...taskObservation(taskScope),
|
|
2532
|
+
};
|
|
2305
2533
|
const cached = options.resumeJournal?.get(journalKey);
|
|
2306
2534
|
if (cached != null && cached.hash === callHash && callIndex < state.firstMiss) {
|
|
2307
2535
|
shared.agentCount++;
|
|
2308
2536
|
frameAgentCount++;
|
|
2537
|
+
emitRuntimeEvent({
|
|
2538
|
+
type: "control-attempt",
|
|
2539
|
+
helper: "checkpoint",
|
|
2540
|
+
stage: "replay",
|
|
2541
|
+
callIndex,
|
|
2542
|
+
...observation,
|
|
2543
|
+
});
|
|
2309
2544
|
return cached.result; // replay the journaled human reply without spending a slot
|
|
2310
2545
|
}
|
|
2311
2546
|
if (cached == null || cached.hash !== callHash) state.firstMiss = Math.min(state.firstMiss, callIndex);
|
|
@@ -2314,48 +2549,69 @@ export async function runWorkflow<T = unknown>(
|
|
|
2314
2549
|
frameAgentCount++;
|
|
2315
2550
|
shared.physicalAgentCount++;
|
|
2316
2551
|
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2552
|
+
emitRuntimeEvent({
|
|
2553
|
+
type: "control-attempt",
|
|
2554
|
+
helper: "checkpoint",
|
|
2555
|
+
stage: "start",
|
|
2556
|
+
callIndex,
|
|
2557
|
+
...observation,
|
|
2558
|
+
});
|
|
2559
|
+
let outcome: "completed" | "failed" | "cancelled" = "failed";
|
|
2560
|
+
try {
|
|
2561
|
+
let reply: unknown;
|
|
2562
|
+
if (options.confirm) {
|
|
2563
|
+
// Host-only cancellation must not affect author options or replay hashes.
|
|
2564
|
+
const signal = options.signal
|
|
2565
|
+
? AbortSignal.any([options.signal, shared.runFatalController.signal])
|
|
2566
|
+
: shared.runFatalController.signal;
|
|
2567
|
+
const confirm = options.confirm;
|
|
2568
|
+
let onAbort: (() => void) | undefined;
|
|
2569
|
+
try {
|
|
2570
|
+
const cancelled = new Promise<never>((_resolve, reject) => {
|
|
2571
|
+
onAbort = () => {
|
|
2572
|
+
try {
|
|
2573
|
+
throwIfAborted();
|
|
2574
|
+
} catch (error) {
|
|
2575
|
+
reject(error);
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2579
|
+
if (signal.aborted) onAbort();
|
|
2580
|
+
});
|
|
2581
|
+
// Older host callbacks may ignore the optional signal. Still release
|
|
2582
|
+
// the runtime wait, and observe any eventual callback rejection.
|
|
2583
|
+
const answer = Promise.resolve().then(() => {
|
|
2584
|
+
throwIfAborted();
|
|
2585
|
+
return confirm(promptText, checkpointOptions, signal);
|
|
2586
|
+
});
|
|
2587
|
+
reply = await Promise.race([answer, cancelled]);
|
|
2588
|
+
} finally {
|
|
2589
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2590
|
+
}
|
|
2591
|
+
} else if (checkpointOptions.headless === "abort") {
|
|
2592
|
+
throw new WorkflowError(
|
|
2593
|
+
`checkpoint "${promptText}" needs human input but none is available (headless run)`,
|
|
2594
|
+
WorkflowErrorCode.WORKFLOW_ABORTED,
|
|
2595
|
+
{ recoverable: false },
|
|
2596
|
+
);
|
|
2597
|
+
} else {
|
|
2598
|
+
reply = checkpointOptions.default ?? true;
|
|
2346
2599
|
}
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
);
|
|
2353
|
-
|
|
2354
|
-
|
|
2600
|
+
throwIfAborted();
|
|
2601
|
+
options.onAgentJournal?.({ index: callIndex, runId, hash: callHash, result: reply });
|
|
2602
|
+
outcome = "completed";
|
|
2603
|
+
return reply;
|
|
2604
|
+
} finally {
|
|
2605
|
+
if (isAborted()) outcome = "cancelled";
|
|
2606
|
+
emitRuntimeEvent({
|
|
2607
|
+
type: "control-attempt",
|
|
2608
|
+
helper: "checkpoint",
|
|
2609
|
+
stage: "end",
|
|
2610
|
+
callIndex,
|
|
2611
|
+
...observation,
|
|
2612
|
+
outcome,
|
|
2613
|
+
});
|
|
2355
2614
|
}
|
|
2356
|
-
throwIfAborted();
|
|
2357
|
-
options.onAgentJournal?.({ index: callIndex, runId, hash: callHash, result: reply });
|
|
2358
|
-
return reply;
|
|
2359
2615
|
};
|
|
2360
2616
|
|
|
2361
2617
|
const runtimeBindings: WorkflowRuntimeBindings & { console: Record<string, (m: unknown) => void> } = {
|
|
@@ -2397,18 +2653,20 @@ export async function runWorkflow<T = unknown>(
|
|
|
2397
2653
|
);
|
|
2398
2654
|
}
|
|
2399
2655
|
|
|
2400
|
-
const context = vm.createContext({
|
|
2401
|
-
...runtimeBindings,
|
|
2402
|
-
// Object/Array/JSON/Math/Date/Promise/Set/Map/etc. come from the vm realm
|
|
2403
|
-
// itself — we deliberately do NOT inject host built-ins, whose
|
|
2404
|
-
// .constructor would be the host Function (a determinism-guard bypass).
|
|
2405
|
-
// Math/Date are neutered in-realm by DETERMINISM_PRELUDE below.
|
|
2406
|
-
});
|
|
2407
|
-
|
|
2408
2656
|
const wrapped = `${DETERMINISM_PRELUDE}\n(async () => {\n${body}\n})()`;
|
|
2409
2657
|
try {
|
|
2410
|
-
const result = await
|
|
2411
|
-
|
|
2658
|
+
const result = await executeOwnedVm({
|
|
2659
|
+
script: wrapped,
|
|
2660
|
+
filename: `${meta.name || "workflow"}.js`,
|
|
2661
|
+
args: options.args,
|
|
2662
|
+
cwd: baseCwd,
|
|
2663
|
+
bindings: { ...runtimeBindings },
|
|
2664
|
+
total: budget.total,
|
|
2665
|
+
spent: budget.spent,
|
|
2666
|
+
cancel: () => frameController.abort(),
|
|
2667
|
+
signal: options.signal
|
|
2668
|
+
? AbortSignal.any([options.signal, shared.runFatalController.signal])
|
|
2669
|
+
: shared.runFatalController.signal,
|
|
2412
2670
|
});
|
|
2413
2671
|
try {
|
|
2414
2672
|
const serialized = JSON.stringify(result);
|
|
@@ -2439,6 +2697,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
2439
2697
|
// reaching here does NOT seal anything (the parent may still catch
|
|
2440
2698
|
// workflow()'s rejection). Idempotent: aborting an already-aborted
|
|
2441
2699
|
// controller is a no-op.
|
|
2700
|
+
frameController.abort();
|
|
2442
2701
|
if (isTopLevelRun) shared.runFatalController.abort();
|
|
2443
2702
|
throw error;
|
|
2444
2703
|
} finally {
|