@signalridge/pi-workflows 1.8.0 → 1.10.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 +15 -0
- package/README.md +31 -1
- package/package.json +2 -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/engine.ts +1088 -395
- package/src/index.ts +286 -53
- package/src/inspection.ts +544 -0
- package/src/journal.ts +33 -11
- package/src/orchestrator.ts +22 -4
- package/src/runtime.ts +387 -148
- package/src/status-actions.ts +143 -0
- 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 -265
package/src/runtime.ts
CHANGED
|
@@ -123,6 +123,11 @@ export interface WorkflowAgentRunner {
|
|
|
123
123
|
|
|
124
124
|
export interface AgentRunOptions {
|
|
125
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;
|
|
126
131
|
sessionName?: string;
|
|
127
132
|
/** Plain JSON Schema the result must satisfy; the runtime parses+validates client-side. */
|
|
128
133
|
schema?: unknown;
|
|
@@ -212,6 +217,10 @@ export interface CheckpointOptions {
|
|
|
212
217
|
|
|
213
218
|
export interface WorkflowRuntimeEvent {
|
|
214
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;
|
|
215
224
|
[key: string]: unknown;
|
|
216
225
|
}
|
|
217
226
|
|
|
@@ -291,6 +300,8 @@ export interface WorkflowRunOptions {
|
|
|
291
300
|
agentTimeoutMs?: number | null;
|
|
292
301
|
runId?: string;
|
|
293
302
|
resumeJournal?: Map<string, JournalEntry>;
|
|
303
|
+
/** Accepted parent-frame nested generation watermarks, independent of replay eligibility. */
|
|
304
|
+
resumeWorkflowGenerations?: ReadonlyMap<number, number>;
|
|
294
305
|
resumeFromRunId?: string;
|
|
295
306
|
toolset?: string;
|
|
296
307
|
excludeTools?: string[];
|
|
@@ -304,14 +315,23 @@ export interface WorkflowRunOptions {
|
|
|
304
315
|
/** Resolve a nested `workflow(name)` reference to the script it names. */
|
|
305
316
|
loadSavedWorkflow?: (name: string) => string | undefined;
|
|
306
317
|
confirm?: (promptText: string, options: CheckpointOptions, signal?: AbortSignal) => Promise<unknown>;
|
|
307
|
-
onLog?: (message: string) => void;
|
|
318
|
+
onLog?: (message: string, scope?: { frameId: string }) => void;
|
|
308
319
|
onPhase?: (title: string) => void;
|
|
309
320
|
onRuntimeEvent?: (event: WorkflowRuntimeEvent) => void;
|
|
310
321
|
onAgentStart?: (event: {
|
|
311
322
|
id: string;
|
|
312
323
|
label: string;
|
|
313
324
|
phase?: string;
|
|
325
|
+
/** Observation-only exact title identity, scoped to this call's frame. */
|
|
326
|
+
phaseKey?: string;
|
|
314
327
|
prompt: string;
|
|
328
|
+
frameId?: string;
|
|
329
|
+
callIndex?: number;
|
|
330
|
+
agentType?: string;
|
|
331
|
+
replayed?: boolean;
|
|
332
|
+
graphId?: string;
|
|
333
|
+
taskKey?: string;
|
|
334
|
+
taskId?: string;
|
|
315
335
|
model?: string;
|
|
316
336
|
tier?: string;
|
|
317
337
|
}) => void;
|
|
@@ -319,6 +339,7 @@ export interface WorkflowRunOptions {
|
|
|
319
339
|
id: string;
|
|
320
340
|
label: string;
|
|
321
341
|
phase?: string;
|
|
342
|
+
phaseKey?: string;
|
|
322
343
|
tier?: string;
|
|
323
344
|
result: unknown;
|
|
324
345
|
tokens?: number;
|
|
@@ -392,6 +413,45 @@ export const MAX_AGENT_RETRIES = 3;
|
|
|
392
413
|
*/
|
|
393
414
|
export const DEFAULT_AGENT_TIMEOUT_MS: number | null = 1_800_000;
|
|
394
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
|
+
}
|
|
395
455
|
|
|
396
456
|
// ── Determinism guardrail ────────────────────────────────────────────────────
|
|
397
457
|
|
|
@@ -1030,7 +1090,10 @@ function validateValue(value: unknown, schema: SchemaNode, path: string): string
|
|
|
1030
1090
|
if (type !== undefined) {
|
|
1031
1091
|
const types = Array.isArray(type) ? type : [type];
|
|
1032
1092
|
const typeOf = (v: unknown): string => (v === null ? "null" : Array.isArray(v) ? "array" : typeof v);
|
|
1033
|
-
|
|
1093
|
+
const matches = types.some((expected) =>
|
|
1094
|
+
expected === "integer" ? typeof value === "number" && Number.isInteger(value) : expected === typeOf(value),
|
|
1095
|
+
);
|
|
1096
|
+
if (!matches) {
|
|
1034
1097
|
errors.push(`${path}: expected type ${types.join("/")}, got ${typeOf(value)}`);
|
|
1035
1098
|
return errors;
|
|
1036
1099
|
}
|
|
@@ -1230,7 +1293,13 @@ export interface WorkflowRuntimeBindings {
|
|
|
1230
1293
|
/** Fan-out cancellation scope: parallel()/pipeline() batches mark themselves. */
|
|
1231
1294
|
const fanoutScope = new AsyncLocalStorage<{ cancelled: boolean }>();
|
|
1232
1295
|
/** Task-local phase attribution for named orchestration callbacks. */
|
|
1233
|
-
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
|
+
}>();
|
|
1234
1303
|
|
|
1235
1304
|
// ── The run ──────────────────────────────────────────────────────────────────
|
|
1236
1305
|
|
|
@@ -1249,6 +1318,14 @@ export async function runWorkflow<T = unknown>(
|
|
|
1249
1318
|
const maxAgents = normalizeMaxAgents(options.maxAgents ?? MAX_AGENTS_PER_RUN);
|
|
1250
1319
|
const agentTimeoutMs = normalizeAgentTimeout(options.agentTimeoutMs);
|
|
1251
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);
|
|
1252
1329
|
const baseCwd = options.cwd ?? process.cwd();
|
|
1253
1330
|
// The host's catalogue, known before the first call. A script cannot name a
|
|
1254
1331
|
// tier, so this is not a typo check: it is what the strength table is resolved
|
|
@@ -1268,7 +1345,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1268
1345
|
currentPhase: meta.phases?.[0]?.title,
|
|
1269
1346
|
phaseBudgets: new Map(),
|
|
1270
1347
|
callSeq: 0,
|
|
1271
|
-
workflowGenerations:
|
|
1348
|
+
workflowGenerations: copyWorkflowGenerations(options.resumeWorkflowGenerations),
|
|
1272
1349
|
firstMiss: Number.POSITIVE_INFINITY,
|
|
1273
1350
|
};
|
|
1274
1351
|
|
|
@@ -1302,7 +1379,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1302
1379
|
const log = (message: unknown) => {
|
|
1303
1380
|
const text = String(message);
|
|
1304
1381
|
state.logs.push(text);
|
|
1305
|
-
options.onLog?.(text);
|
|
1382
|
+
options.onLog?.(text, { frameId: runId });
|
|
1306
1383
|
};
|
|
1307
1384
|
if (concurrencyClampNotice !== undefined) log(concurrencyClampNotice);
|
|
1308
1385
|
|
|
@@ -1392,14 +1469,39 @@ export async function runWorkflow<T = unknown>(
|
|
|
1392
1469
|
);
|
|
1393
1470
|
};
|
|
1394
1471
|
|
|
1472
|
+
let graphSequence = 0;
|
|
1473
|
+
let helperSequence = 0;
|
|
1395
1474
|
const emitRuntimeEvent = (event: WorkflowRuntimeEvent): void => {
|
|
1396
1475
|
try {
|
|
1397
|
-
|
|
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
|
+
);
|
|
1398
1487
|
} catch {
|
|
1399
1488
|
// Runtime observers are diagnostic surfaces and must not change execution.
|
|
1400
1489
|
}
|
|
1401
1490
|
};
|
|
1402
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
|
+
|
|
1403
1505
|
const phase = (title: string, phaseOptions?: { budget?: number }) => {
|
|
1404
1506
|
state.currentPhase = title;
|
|
1405
1507
|
if (!state.phases.includes(title)) state.phases.push(title);
|
|
@@ -1412,6 +1514,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1412
1514
|
emitRuntimeEvent({
|
|
1413
1515
|
type: "phase",
|
|
1414
1516
|
title,
|
|
1517
|
+
phaseKey: phaseObservation(title).phaseKey,
|
|
1415
1518
|
budget: typeof phaseOptions?.budget === "number" && phaseOptions.budget > 0 ? phaseOptions.budget : null,
|
|
1416
1519
|
});
|
|
1417
1520
|
};
|
|
@@ -1497,6 +1600,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
1497
1600
|
// synchronously, before suspending on the limiter below.
|
|
1498
1601
|
const batch = fanoutScope.getStore();
|
|
1499
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.
|
|
1500
1605
|
const taskScope = orchestrationScope.getStore();
|
|
1501
1606
|
const assignedPhase = agentOptions.phase ?? taskScope?.phase ?? state.currentPhase;
|
|
1502
1607
|
|
|
@@ -1546,6 +1651,13 @@ export async function runWorkflow<T = unknown>(
|
|
|
1546
1651
|
const deltaKey = `${runId}:${callIndex}`;
|
|
1547
1652
|
|
|
1548
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
|
+
};
|
|
1549
1661
|
|
|
1550
1662
|
// Longest-unchanged-prefix resume: replay a cached result only while the
|
|
1551
1663
|
// prefix is still intact. Once any call misses, it AND everything after it
|
|
@@ -1564,6 +1676,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
1564
1676
|
const replayTier = cached.tier ?? effectiveTier;
|
|
1565
1677
|
options.onAgentStart?.({
|
|
1566
1678
|
id: deltaKey,
|
|
1679
|
+
...observation,
|
|
1680
|
+
replayed: true,
|
|
1567
1681
|
label,
|
|
1568
1682
|
phase: assignedPhase,
|
|
1569
1683
|
prompt: promptText,
|
|
@@ -1572,7 +1686,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1572
1686
|
options.onAgentEnd?.({
|
|
1573
1687
|
id: deltaKey,
|
|
1574
1688
|
label,
|
|
1575
|
-
|
|
1689
|
+
...phaseObservation(assignedPhase),
|
|
1576
1690
|
tier: replayTier,
|
|
1577
1691
|
result: cached.result,
|
|
1578
1692
|
tokens: 0,
|
|
@@ -1631,6 +1745,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
1631
1745
|
|
|
1632
1746
|
options.onAgentStart?.({
|
|
1633
1747
|
id: deltaKey,
|
|
1748
|
+
...observation,
|
|
1749
|
+
replayed: false,
|
|
1634
1750
|
label,
|
|
1635
1751
|
phase: assignedPhase,
|
|
1636
1752
|
prompt: promptText,
|
|
@@ -1646,7 +1762,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1646
1762
|
// failed attempt does not double-count the next one.
|
|
1647
1763
|
let usage: AgentUsage | undefined;
|
|
1648
1764
|
const recordTokens = (result: unknown): number => {
|
|
1649
|
-
const tokens = usage
|
|
1765
|
+
const tokens = usage === undefined ? estimateTokens(result) + estimateTokens(promptText) : usage.total;
|
|
1650
1766
|
if (usage) {
|
|
1651
1767
|
shared.tokenUsage.input += usage.input;
|
|
1652
1768
|
shared.tokenUsage.output += usage.output;
|
|
@@ -1708,6 +1824,10 @@ export async function runWorkflow<T = unknown>(
|
|
|
1708
1824
|
let restartTimeout: (() => void) | undefined;
|
|
1709
1825
|
const runPromise = agentRunner.run(promptText + schemaInstruction + repairFeedback, {
|
|
1710
1826
|
label,
|
|
1827
|
+
phase: assignedPhase,
|
|
1828
|
+
...(taskScope
|
|
1829
|
+
? { graphId: taskScope.graphId, taskKey: taskScope.taskKey, taskId: taskScope.taskId }
|
|
1830
|
+
: {}),
|
|
1711
1831
|
sessionName: agentOptions.thread
|
|
1712
1832
|
? `workflow:${runId} thread:${agentOptions.thread}`
|
|
1713
1833
|
: `workflow:${runId} ${label}`,
|
|
@@ -1812,7 +1932,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1812
1932
|
options.onAgentEnd?.({
|
|
1813
1933
|
id: deltaKey,
|
|
1814
1934
|
label,
|
|
1815
|
-
|
|
1935
|
+
...phaseObservation(assignedPhase),
|
|
1816
1936
|
result,
|
|
1817
1937
|
tier: resolvedTier,
|
|
1818
1938
|
tokens,
|
|
@@ -1854,7 +1974,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1854
1974
|
options.onAgentEnd?.({
|
|
1855
1975
|
id: deltaKey,
|
|
1856
1976
|
label,
|
|
1857
|
-
|
|
1977
|
+
...phaseObservation(assignedPhase),
|
|
1858
1978
|
result: null,
|
|
1859
1979
|
tier: resolvedTier,
|
|
1860
1980
|
tokens,
|
|
@@ -1985,11 +2105,20 @@ export async function runWorkflow<T = unknown>(
|
|
|
1985
2105
|
const orchestrate = async (rawTasks: unknown, rawOptions?: unknown): Promise<OrchestrationResult> => {
|
|
1986
2106
|
throwIfAborted();
|
|
1987
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}`]));
|
|
1988
2110
|
const tasks = normalized.tasks.map((task) => ({
|
|
1989
2111
|
...task,
|
|
1990
2112
|
run: (context: OrchestrationTaskContext) =>
|
|
1991
|
-
orchestrationScope.run(
|
|
1992
|
-
|
|
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),
|
|
1993
2122
|
),
|
|
1994
2123
|
}));
|
|
1995
2124
|
return executeOrchestration(tasks, normalized.options, {
|
|
@@ -2000,7 +2129,15 @@ export async function runWorkflow<T = unknown>(
|
|
|
2000
2129
|
return !workflowError.recoverable || workflowError.code === WorkflowErrorCode.PROVIDER_USAGE_LIMIT;
|
|
2001
2130
|
},
|
|
2002
2131
|
formatError: (error: unknown) => wrapError(error).message,
|
|
2003
|
-
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
|
+
},
|
|
2004
2141
|
});
|
|
2005
2142
|
};
|
|
2006
2143
|
|
|
@@ -2017,8 +2154,18 @@ export async function runWorkflow<T = unknown>(
|
|
|
2017
2154
|
});
|
|
2018
2155
|
}
|
|
2019
2156
|
const callIndex = nextCallIndex();
|
|
2020
|
-
const
|
|
2157
|
+
const taskScope = orchestrationScope.getStore();
|
|
2158
|
+
const workflowPhase = taskScope?.phase ?? state.currentPhase;
|
|
2021
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
|
+
};
|
|
2022
2169
|
const argsPresent = childArgs !== undefined;
|
|
2023
2170
|
const callHash = hashIdentity({
|
|
2024
2171
|
kind: "workflow",
|
|
@@ -2038,17 +2185,41 @@ export async function runWorkflow<T = unknown>(
|
|
|
2038
2185
|
});
|
|
2039
2186
|
const cached = options.resumeJournal?.get(`${runId}:${callIndex}`);
|
|
2040
2187
|
if (cached != null && cached.hash === callHash && callIndex < state.firstMiss) {
|
|
2041
|
-
state.workflowGenerations.set(
|
|
2188
|
+
state.workflowGenerations.set(
|
|
2189
|
+
callIndex,
|
|
2190
|
+
Math.max(state.workflowGenerations.get(callIndex) ?? 0, cached.generation ?? 1),
|
|
2191
|
+
);
|
|
2042
2192
|
if (cached.storeDelta) store.applyDelta(cached.storeDelta);
|
|
2043
2193
|
const replayedAgentCount = cached.agentCount ?? 0;
|
|
2044
2194
|
shared.agentCount += replayedAgentCount;
|
|
2045
2195
|
frameAgentCount += replayedAgentCount;
|
|
2196
|
+
emitRuntimeEvent({
|
|
2197
|
+
type: "workflow",
|
|
2198
|
+
stage: "replay",
|
|
2199
|
+
...observation,
|
|
2200
|
+
callIndex,
|
|
2201
|
+
replayedAgentCount,
|
|
2202
|
+
});
|
|
2046
2203
|
return cached.result;
|
|
2047
2204
|
}
|
|
2048
2205
|
if (cached == null || cached.hash !== callHash) state.firstMiss = Math.min(state.firstMiss, callIndex);
|
|
2049
|
-
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
|
+
}
|
|
2050
2212
|
state.workflowGenerations.set(callIndex, generation);
|
|
2051
|
-
|
|
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";
|
|
2052
2223
|
try {
|
|
2053
2224
|
const childDeltas = new Map<number, Record<string, unknown>>();
|
|
2054
2225
|
const child = await runWorkflow(childScript, {
|
|
@@ -2061,6 +2232,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
2061
2232
|
// call results here would duplicate nested calls and make the synthetic
|
|
2062
2233
|
// parent journal entry ambiguous.
|
|
2063
2234
|
resumeJournal: undefined,
|
|
2235
|
+
// These watermarks belong to the parent lexical namespace only.
|
|
2236
|
+
resumeWorkflowGenerations: undefined,
|
|
2064
2237
|
resumeFromRunId: undefined,
|
|
2065
2238
|
onWorkflowJournal: undefined,
|
|
2066
2239
|
onAgentJournal: (entry) => {
|
|
@@ -2072,7 +2245,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
2072
2245
|
// The call index + generation keeps changed nested calls distinct across
|
|
2073
2246
|
// resume. The engine nonce additionally fences an interrupted child whose
|
|
2074
2247
|
// result was never durable before a process/branch restart.
|
|
2075
|
-
runId:
|
|
2248
|
+
runId: childFrameId,
|
|
2076
2249
|
});
|
|
2077
2250
|
const normalizedChildResult = child.result === undefined ? null : child.result;
|
|
2078
2251
|
const storeDelta: Record<string, unknown> = {};
|
|
@@ -2091,14 +2264,51 @@ export async function runWorkflow<T = unknown>(
|
|
|
2091
2264
|
agentCount: child.agentCount,
|
|
2092
2265
|
...(Object.keys(storeDelta).length > 0 ? { storeDelta } : {}),
|
|
2093
2266
|
});
|
|
2267
|
+
outcome = "completed";
|
|
2094
2268
|
return normalizedChildResult;
|
|
2095
2269
|
} finally {
|
|
2096
|
-
|
|
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
|
+
});
|
|
2097
2280
|
}
|
|
2098
2281
|
};
|
|
2099
2282
|
|
|
2100
2283
|
// ── Quality-pattern stdlib ──────────────────────────────────────────────────
|
|
2101
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
|
+
|
|
2102
2312
|
const VERIFY_SCHEMA = {
|
|
2103
2313
|
type: "object",
|
|
2104
2314
|
properties: { real: { type: "boolean" }, reason: { type: "string" } },
|
|
@@ -2113,33 +2323,34 @@ export async function runWorkflow<T = unknown>(
|
|
|
2113
2323
|
total: number;
|
|
2114
2324
|
votes: Array<{ real: boolean; reason?: string }>;
|
|
2115
2325
|
}> => {
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
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
|
+
});
|
|
2143
2354
|
};
|
|
2144
2355
|
|
|
2145
2356
|
const JUDGE_SCHEMA = {
|
|
@@ -2153,58 +2364,53 @@ export async function runWorkflow<T = unknown>(
|
|
|
2153
2364
|
): Promise<
|
|
2154
2365
|
{ index: number; attempt: unknown; score: number; judgments: Array<{ score: number; reason?: string }> } | undefined
|
|
2155
2366
|
> => {
|
|
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
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
}
|
|
2204
|
-
let best = normalized[0];
|
|
2205
|
-
for (const s of normalized) if (s.score > best.score || (s.score === best.score && s.index < best.index)) best = s;
|
|
2206
|
-
emitRuntimeEvent({ type: "quality", stage: "end", helper: "judgePanel" });
|
|
2207
|
-
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
|
+
});
|
|
2208
2414
|
};
|
|
2209
2415
|
|
|
2210
2416
|
const loopUntilDry = async (opts: {
|
|
@@ -2261,16 +2467,16 @@ export async function runWorkflow<T = unknown>(
|
|
|
2261
2467
|
results: unknown,
|
|
2262
2468
|
opts: { strength?: WorkflowStrength } = {},
|
|
2263
2469
|
): Promise<{ complete: boolean; missing?: string[] } | null> => {
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
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
|
+
});
|
|
2274
2480
|
};
|
|
2275
2481
|
|
|
2276
2482
|
const retry = async (
|
|
@@ -2319,10 +2525,22 @@ export async function runWorkflow<T = unknown>(
|
|
|
2319
2525
|
const callIndex = nextCallIndex();
|
|
2320
2526
|
const callHash = hashCheckpoint(promptText, checkpointOptions);
|
|
2321
2527
|
const journalKey = `${runId}:${callIndex}`;
|
|
2528
|
+
const taskScope = orchestrationScope.getStore();
|
|
2529
|
+
const observation = {
|
|
2530
|
+
...phaseObservation(taskScope?.phase ?? state.currentPhase),
|
|
2531
|
+
...taskObservation(taskScope),
|
|
2532
|
+
};
|
|
2322
2533
|
const cached = options.resumeJournal?.get(journalKey);
|
|
2323
2534
|
if (cached != null && cached.hash === callHash && callIndex < state.firstMiss) {
|
|
2324
2535
|
shared.agentCount++;
|
|
2325
2536
|
frameAgentCount++;
|
|
2537
|
+
emitRuntimeEvent({
|
|
2538
|
+
type: "control-attempt",
|
|
2539
|
+
helper: "checkpoint",
|
|
2540
|
+
stage: "replay",
|
|
2541
|
+
callIndex,
|
|
2542
|
+
...observation,
|
|
2543
|
+
});
|
|
2326
2544
|
return cached.result; // replay the journaled human reply without spending a slot
|
|
2327
2545
|
}
|
|
2328
2546
|
if (cached == null || cached.hash !== callHash) state.firstMiss = Math.min(state.firstMiss, callIndex);
|
|
@@ -2331,48 +2549,69 @@ export async function runWorkflow<T = unknown>(
|
|
|
2331
2549
|
frameAgentCount++;
|
|
2332
2550
|
shared.physicalAgentCount++;
|
|
2333
2551
|
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
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;
|
|
2363
2599
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
);
|
|
2370
|
-
|
|
2371
|
-
|
|
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
|
+
});
|
|
2372
2614
|
}
|
|
2373
|
-
throwIfAborted();
|
|
2374
|
-
options.onAgentJournal?.({ index: callIndex, runId, hash: callHash, result: reply });
|
|
2375
|
-
return reply;
|
|
2376
2615
|
};
|
|
2377
2616
|
|
|
2378
2617
|
const runtimeBindings: WorkflowRuntimeBindings & { console: Record<string, (m: unknown) => void> } = {
|