@signalridge/pi-workflows 1.3.2 → 1.4.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 +5 -0
- package/README.md +40 -11
- package/package.json +3 -2
- package/skills/workflow-authoring/SKILL.md +166 -18
- package/skills/workflow-authoring/examples/named-task-graph.js +34 -0
- package/skills/workflow-authoring/references/capabilities.md +2 -1
- package/skills/workflow-authoring/references/capability-details.md +22 -4
- package/skills/workflow-authoring/references/focused-recipes.md +1 -0
- package/skills/workflow-authoring/references/lifecycle.md +3 -3
- package/skills/workflow-authoring/references/pattern-selection.md +1 -0
- package/skills/workflow-authoring/references/registry-ownership.md +1 -1
- package/skills/workflow-authoring/references/review.md +7 -5
- package/skills/workflow-authoring/references/runtime.md +4 -3
- package/skills/workflow-patterns/SKILL.md +72 -33
- package/skills/workflow-review/SKILL.md +70 -0
- package/src/capabilities.ts +28 -3
- package/src/engine.ts +384 -124
- package/src/index.ts +126 -9
- package/src/journal.ts +266 -27
- package/src/orchestrator.ts +478 -0
- package/src/runtime.ts +332 -123
package/src/engine.ts
CHANGED
|
@@ -46,10 +46,17 @@ import {
|
|
|
46
46
|
type AgentRunOptions,
|
|
47
47
|
type AgentUsage,
|
|
48
48
|
type JournalEntry,
|
|
49
|
+
MAX_AGENTS_PER_RUN,
|
|
50
|
+
normalizeAgentRetries,
|
|
51
|
+
normalizeAgentTimeout,
|
|
52
|
+
normalizeConcurrency,
|
|
53
|
+
normalizeMaxAgents,
|
|
54
|
+
normalizeTokenBudget,
|
|
49
55
|
parseWorkflowScript,
|
|
50
56
|
runWorkflow,
|
|
51
57
|
type WorkflowAgentRunner,
|
|
52
58
|
type WorkflowRunResult,
|
|
59
|
+
type WorkflowRuntimeEvent,
|
|
53
60
|
} from "./runtime.js";
|
|
54
61
|
|
|
55
62
|
export const MAX_ATTEMPTS_PER_NODE = 3;
|
|
@@ -106,6 +113,28 @@ export interface ScriptStartResult {
|
|
|
106
113
|
waitAborted?: boolean;
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
interface AgentLifecycleWaiter {
|
|
117
|
+
callIndex: string;
|
|
118
|
+
generation: number;
|
|
119
|
+
onUsage?: (usage: AgentUsage) => void;
|
|
120
|
+
onHistory?: (history: unknown[]) => void;
|
|
121
|
+
executionGeneration: number;
|
|
122
|
+
resolve: (response: ManagedSpawnResponse) => void;
|
|
123
|
+
reject: (error: unknown) => void;
|
|
124
|
+
settled: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface AgentTerminalDelivery {
|
|
128
|
+
response: ManagedSpawnResponse;
|
|
129
|
+
usage?: AgentUsage;
|
|
130
|
+
history?: unknown[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface BufferedAgentTerminal extends AgentTerminalDelivery {
|
|
134
|
+
owner: WorkflowOwner;
|
|
135
|
+
executionGeneration: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
export interface ScriptRunState {
|
|
110
139
|
run: ScriptRun;
|
|
111
140
|
/** Frozen run parameters — resume reuses the original values, ignoring new budget/toolset overrides. */
|
|
@@ -124,21 +153,11 @@ export interface ScriptRunState {
|
|
|
124
153
|
/** Per-call-index generation base for A4 spawnKey rotation (seeded from journal). */
|
|
125
154
|
generations: Map<string, number>;
|
|
126
155
|
/** Live dispatch waiters keyed by agent id. */
|
|
127
|
-
agentWaiters: Map<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
callIndex: string;
|
|
131
|
-
generation: number;
|
|
132
|
-
onUsage?: (usage: AgentUsage) => void;
|
|
133
|
-
onHistory?: (history: unknown[]) => void;
|
|
134
|
-
executionGeneration: number;
|
|
135
|
-
resolve: (r: ManagedSpawnResponse) => void;
|
|
136
|
-
reject: (e: unknown) => void;
|
|
137
|
-
settled: boolean;
|
|
138
|
-
}
|
|
139
|
-
>;
|
|
156
|
+
agentWaiters: Map<string, AgentLifecycleWaiter>;
|
|
157
|
+
/** Owner-validated terminal events emitted before their spawn reply/waiter. */
|
|
158
|
+
bufferedTerminals: Map<string, BufferedAgentTerminal>;
|
|
140
159
|
/** Allocations whose managed-spawn reply has not arrived yet. */
|
|
141
|
-
pendingSpawns: Map<string, { spawnKey: string; owner: WorkflowOwner }>;
|
|
160
|
+
pendingSpawns: Map<string, { spawnKey: string; owner: WorkflowOwner; executionGeneration: number }>;
|
|
142
161
|
/** Journal retry bookkeeping. */
|
|
143
162
|
pendingJournal: Set<string>;
|
|
144
163
|
/** Whether a lifecycle suspension is in effect (branch change). */
|
|
@@ -153,6 +172,47 @@ function managedTerminalStatus(snapshot: { status: string }): "completed" | "fai
|
|
|
153
172
|
return snapshot.status === "completed" ? "completed" : snapshot.status === "failed" ? "failed" : "stopped";
|
|
154
173
|
}
|
|
155
174
|
|
|
175
|
+
function renderWorkflowValue(value: unknown): string {
|
|
176
|
+
if (typeof value === "string") return value;
|
|
177
|
+
try {
|
|
178
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
179
|
+
return serialized === undefined ? String(value) : serialized;
|
|
180
|
+
} catch {
|
|
181
|
+
return String(value);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function cloneWorkflowValue(value: unknown, label: string): unknown {
|
|
186
|
+
if (value === undefined) return undefined;
|
|
187
|
+
try {
|
|
188
|
+
const cloned = structuredClone(value);
|
|
189
|
+
const serialized = JSON.stringify(cloned);
|
|
190
|
+
if (serialized === undefined) throw new Error("value is not JSON-serializable");
|
|
191
|
+
if (serialized.length > 200_000) {
|
|
192
|
+
throw new Error("value exceeds the 200000-character workflow persistence limit");
|
|
193
|
+
}
|
|
194
|
+
return cloned;
|
|
195
|
+
} catch (error: unknown) {
|
|
196
|
+
throw new WorkflowError(
|
|
197
|
+
`${label} must be structured-cloneable and JSON-serializable: ${error instanceof Error ? error.message : String(error)}`,
|
|
198
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
199
|
+
{ recoverable: false, details: error },
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function cloneWorkflowArgs(value: unknown): unknown {
|
|
205
|
+
return cloneWorkflowValue(value, "Workflow args");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function cloneWorkflowResult(value: unknown): unknown {
|
|
209
|
+
return cloneWorkflowValue(value, "Workflow result");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cloneRuntimeEvent(event: WorkflowRuntimeEvent): WorkflowRuntimeEvent {
|
|
213
|
+
return structuredClone(event);
|
|
214
|
+
}
|
|
215
|
+
|
|
156
216
|
function resultFromLifecycle(
|
|
157
217
|
status: "completed" | "failed" | "stopped",
|
|
158
218
|
agentId: string | undefined,
|
|
@@ -179,10 +239,21 @@ function generationFromAttemptId(attemptId: string | undefined): number {
|
|
|
179
239
|
return match ? Number(match[1]) : 0;
|
|
180
240
|
}
|
|
181
241
|
|
|
242
|
+
function sameWorkflowOwner(left: WorkflowOwner, right: WorkflowOwner): boolean {
|
|
243
|
+
return (
|
|
244
|
+
left.extension === right.extension &&
|
|
245
|
+
left.runId === right.runId &&
|
|
246
|
+
left.nodeId === right.nodeId &&
|
|
247
|
+
left.attemptId === right.attemptId
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
182
251
|
export class WorkflowEngine {
|
|
183
252
|
private readonly runs = new Map<string, ScriptRunState>();
|
|
184
253
|
/** Fired when a run settles terminal; wired by the UI surface for widget refresh. */
|
|
185
254
|
onRunSettled: (() => void) | undefined;
|
|
255
|
+
/** Optional in-process observer for phase/task/quality runtime events. */
|
|
256
|
+
onRuntimeEvent: ((runId: string, event: WorkflowRuntimeEvent) => void) | undefined;
|
|
186
257
|
private readonly waiters = new Map<
|
|
187
258
|
string,
|
|
188
259
|
Set<{ resolve: (run: ScriptRun) => void; reject: (e: unknown) => void; settled: boolean; cleanup: () => void }>
|
|
@@ -224,60 +295,105 @@ export class WorkflowEngine {
|
|
|
224
295
|
}
|
|
225
296
|
|
|
226
297
|
private onLifecycle(eventName: string, raw: unknown): void {
|
|
298
|
+
if (eventName !== "subagents:completed" && eventName !== "subagents:failed") return;
|
|
227
299
|
if (!raw || typeof raw !== "object") return;
|
|
228
300
|
const data = raw as Record<string, unknown>;
|
|
229
301
|
const agentId = typeof data.id === "string" ? data.id : typeof data.agentId === "string" ? data.agentId : undefined;
|
|
230
302
|
if (!agentId) return;
|
|
231
303
|
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
},
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
304
|
+
const rawOwner = data.owner;
|
|
305
|
+
if (!rawOwner || typeof rawOwner !== "object" || Array.isArray(rawOwner)) return;
|
|
306
|
+
const ownerRecord = rawOwner as Record<string, unknown>;
|
|
307
|
+
if (
|
|
308
|
+
ownerRecord.extension !== "pi-workflows" ||
|
|
309
|
+
typeof ownerRecord.runId !== "string" ||
|
|
310
|
+
typeof ownerRecord.nodeId !== "string" ||
|
|
311
|
+
typeof ownerRecord.attemptId !== "string"
|
|
312
|
+
)
|
|
313
|
+
return;
|
|
314
|
+
const owner: WorkflowOwner = {
|
|
315
|
+
extension: "pi-workflows",
|
|
316
|
+
runId: ownerRecord.runId,
|
|
317
|
+
nodeId: ownerRecord.nodeId,
|
|
318
|
+
attemptId: ownerRecord.attemptId,
|
|
319
|
+
};
|
|
320
|
+
const state = this.runs.get(owner.runId);
|
|
321
|
+
if (state?.run.status !== "running" || state.lifecycleSuspended || state.controller.signal.aborted) return;
|
|
322
|
+
|
|
323
|
+
const lifecycleStatus =
|
|
324
|
+
eventName === "subagents:completed"
|
|
325
|
+
? "completed"
|
|
326
|
+
: data.status === "stopped" || data.status === "interrupted"
|
|
327
|
+
? "stopped"
|
|
328
|
+
: "failed";
|
|
329
|
+
const tokenCount =
|
|
330
|
+
typeof data.tokenCount === "number"
|
|
331
|
+
? data.tokenCount
|
|
332
|
+
: data.tokens &&
|
|
333
|
+
typeof data.tokens === "object" &&
|
|
334
|
+
typeof (data.tokens as Record<string, unknown>).total === "number"
|
|
335
|
+
? ((data.tokens as Record<string, unknown>).total as number)
|
|
336
|
+
: undefined;
|
|
337
|
+
const delivery: AgentTerminalDelivery = {
|
|
338
|
+
response: {
|
|
339
|
+
id: agentId,
|
|
340
|
+
terminal: {
|
|
341
|
+
status: lifecycleStatus,
|
|
342
|
+
...(typeof data.result === "string" ? { result: data.result } : {}),
|
|
343
|
+
...(typeof data.error === "string" ? { error: data.error } : {}),
|
|
344
|
+
...(typeof data.outputFile === "string" ? { outputFile: data.outputFile } : {}),
|
|
345
|
+
...(tokenCount === undefined ? {} : { tokenCount }),
|
|
346
|
+
compactionCount: typeof data.compactionCount === "number" ? data.compactionCount : 0,
|
|
347
|
+
completedAt: typeof data.completedAt === "number" ? data.completedAt : Date.now(),
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
...(tokenCount === undefined
|
|
351
|
+
? {}
|
|
352
|
+
: { usage: { input: 0, output: tokenCount, total: tokenCount, cost: 0, cacheRead: 0, cacheWrite: 0 } }),
|
|
353
|
+
...(Array.isArray(data.history) ? { history: [...data.history] } : {}),
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const waiter = state.agentWaiters.get(agentId);
|
|
357
|
+
if (waiter) {
|
|
358
|
+
if (
|
|
359
|
+
waiter.settled ||
|
|
360
|
+
waiter.executionGeneration !== state.executionGeneration ||
|
|
361
|
+
owner.nodeId !== `call-${waiter.callIndex}` ||
|
|
362
|
+
owner.attemptId !== `attempt-${waiter.generation}`
|
|
363
|
+
)
|
|
364
|
+
return;
|
|
365
|
+
this.resolveAgentWaiter(state, agentId, waiter, delivery);
|
|
366
|
+
return;
|
|
280
367
|
}
|
|
368
|
+
|
|
369
|
+
// spawn-managed can emit a terminal lifecycle event synchronously before its
|
|
370
|
+
// reply continuation registers the agent-id waiter. Buffer only an owner
|
|
371
|
+
// that is currently awaiting that exact reply in this execution generation.
|
|
372
|
+
const spawnKey = `${owner.runId}/${owner.nodeId}/${owner.attemptId}`;
|
|
373
|
+
const pending = state.pendingSpawns.get(spawnKey);
|
|
374
|
+
if (
|
|
375
|
+
!pending ||
|
|
376
|
+
pending.executionGeneration !== state.executionGeneration ||
|
|
377
|
+
!sameWorkflowOwner(pending.owner, owner) ||
|
|
378
|
+
state.bufferedTerminals.has(spawnKey) ||
|
|
379
|
+
state.bufferedTerminals.size >= MAX_AGENTS_PER_RUN
|
|
380
|
+
)
|
|
381
|
+
return;
|
|
382
|
+
state.bufferedTerminals.set(spawnKey, { ...delivery, owner, executionGeneration: state.executionGeneration });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private resolveAgentWaiter(
|
|
386
|
+
state: ScriptRunState,
|
|
387
|
+
agentId: string,
|
|
388
|
+
waiter: AgentLifecycleWaiter,
|
|
389
|
+
delivery: AgentTerminalDelivery,
|
|
390
|
+
): void {
|
|
391
|
+
if (waiter.settled) return;
|
|
392
|
+
waiter.settled = true;
|
|
393
|
+
state.agentWaiters.delete(agentId);
|
|
394
|
+
if (delivery.usage) waiter.onUsage?.(delivery.usage);
|
|
395
|
+
if (delivery.history) waiter.onHistory?.(delivery.history);
|
|
396
|
+
waiter.resolve(delivery.response);
|
|
281
397
|
}
|
|
282
398
|
|
|
283
399
|
dispose(): void {
|
|
@@ -329,6 +445,7 @@ export class WorkflowEngine {
|
|
|
329
445
|
});
|
|
330
446
|
targets.set(state.run.runId, { agentIds, owners });
|
|
331
447
|
state.lifecycleSuspended = true;
|
|
448
|
+
state.bufferedTerminals.clear();
|
|
332
449
|
state.controller.abort(new DOMException("Workflow branch change", "AbortError"));
|
|
333
450
|
}
|
|
334
451
|
|
|
@@ -394,18 +511,40 @@ export class WorkflowEngine {
|
|
|
394
511
|
const runId = randomUUID();
|
|
395
512
|
const scriptHash = hashScript(script);
|
|
396
513
|
const toolset = options.toolset;
|
|
514
|
+
const durableArgs = cloneWorkflowArgs(options.args);
|
|
515
|
+
const runtimeArgs = cloneWorkflowArgs(durableArgs);
|
|
516
|
+
const journalArgs = cloneWorkflowArgs(durableArgs);
|
|
517
|
+
const frozenArgsPresent = durableArgs !== undefined;
|
|
518
|
+
const frozenMaxAgents = options.maxAgents === undefined ? undefined : normalizeMaxAgents(options.maxAgents);
|
|
519
|
+
const frozenConcurrency = options.concurrency === undefined ? undefined : normalizeConcurrency(options.concurrency);
|
|
520
|
+
const frozenAgentRetries =
|
|
521
|
+
options.agentRetries === undefined ? undefined : normalizeAgentRetries(options.agentRetries);
|
|
522
|
+
const frozenTokenBudget = options.tokenBudget === undefined ? undefined : normalizeTokenBudget(options.tokenBudget);
|
|
523
|
+
const frozenAgentTimeoutMs =
|
|
524
|
+
options.agentTimeoutMs === undefined ? undefined : normalizeAgentTimeout(options.agentTimeoutMs);
|
|
525
|
+
const startOptions: ScriptStartOptions = {
|
|
526
|
+
...options,
|
|
527
|
+
...(runtimeArgs === undefined ? {} : { args: runtimeArgs }),
|
|
528
|
+
...(frozenMaxAgents === undefined ? {} : { maxAgents: frozenMaxAgents }),
|
|
529
|
+
...(frozenConcurrency === undefined ? {} : { concurrency: frozenConcurrency }),
|
|
530
|
+
...(frozenAgentRetries === undefined ? {} : { agentRetries: frozenAgentRetries }),
|
|
531
|
+
...(frozenTokenBudget === undefined ? {} : { tokenBudget: frozenTokenBudget }),
|
|
532
|
+
...(frozenAgentTimeoutMs === undefined ? {} : { agentTimeoutMs: frozenAgentTimeoutMs }),
|
|
533
|
+
};
|
|
397
534
|
const run: ScriptRun = {
|
|
398
535
|
runId,
|
|
399
536
|
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
400
537
|
script,
|
|
401
538
|
scriptHash,
|
|
402
539
|
meta,
|
|
540
|
+
...(durableArgs === undefined ? {} : { args: durableArgs }),
|
|
541
|
+
frozenArgsPresent,
|
|
403
542
|
...(toolset ? { toolset } : {}),
|
|
404
|
-
...(
|
|
405
|
-
...(
|
|
406
|
-
...(
|
|
407
|
-
...(
|
|
408
|
-
...(
|
|
543
|
+
...(frozenMaxAgents !== undefined ? { frozenMaxAgents } : {}),
|
|
544
|
+
...(frozenConcurrency !== undefined ? { frozenConcurrency } : {}),
|
|
545
|
+
...(frozenAgentRetries !== undefined ? { frozenAgentRetries } : {}),
|
|
546
|
+
...(frozenTokenBudget !== undefined ? { frozenTokenBudget } : {}),
|
|
547
|
+
...(frozenAgentTimeoutMs !== undefined ? { frozenAgentTimeoutMs } : {}),
|
|
409
548
|
...(options.excludeTools ? { frozenExcludeTools: [...options.excludeTools] } : {}),
|
|
410
549
|
status: "pending",
|
|
411
550
|
callStatus: {},
|
|
@@ -413,6 +552,7 @@ export class WorkflowEngine {
|
|
|
413
552
|
attempts: {},
|
|
414
553
|
attemptIds: {},
|
|
415
554
|
callResults: {},
|
|
555
|
+
workflowResultGenerations: {},
|
|
416
556
|
compactions: {},
|
|
417
557
|
startedAt: now,
|
|
418
558
|
updatedAt: now,
|
|
@@ -424,29 +564,32 @@ export class WorkflowEngine {
|
|
|
424
564
|
script,
|
|
425
565
|
scriptHash,
|
|
426
566
|
meta,
|
|
567
|
+
...(journalArgs === undefined ? {} : { args: journalArgs }),
|
|
568
|
+
frozenArgsPresent,
|
|
427
569
|
...(toolset ? { toolset } : {}),
|
|
428
|
-
...(
|
|
429
|
-
...(
|
|
430
|
-
...(
|
|
431
|
-
...(
|
|
432
|
-
...(
|
|
570
|
+
...(frozenMaxAgents !== undefined ? { frozenMaxAgents } : {}),
|
|
571
|
+
...(frozenConcurrency !== undefined ? { frozenConcurrency } : {}),
|
|
572
|
+
...(frozenAgentRetries !== undefined ? { frozenAgentRetries } : {}),
|
|
573
|
+
...(frozenTokenBudget !== undefined ? { frozenTokenBudget } : {}),
|
|
574
|
+
...(frozenAgentTimeoutMs !== undefined ? { frozenAgentTimeoutMs } : {}),
|
|
433
575
|
...(options.excludeTools ? { frozenExcludeTools: [...options.excludeTools] } : {}),
|
|
434
576
|
timestamp: now,
|
|
435
577
|
});
|
|
436
578
|
const state: ScriptRunState = {
|
|
437
579
|
run,
|
|
438
580
|
frozenOptions: {
|
|
439
|
-
...(
|
|
440
|
-
...(
|
|
441
|
-
...(
|
|
442
|
-
...(
|
|
443
|
-
...(
|
|
581
|
+
...(frozenTokenBudget !== undefined ? { tokenBudget: frozenTokenBudget } : {}),
|
|
582
|
+
...(frozenMaxAgents !== undefined ? { maxAgents: frozenMaxAgents } : {}),
|
|
583
|
+
...(frozenConcurrency !== undefined ? { concurrency: frozenConcurrency } : {}),
|
|
584
|
+
...(frozenAgentRetries !== undefined ? { agentRetries: frozenAgentRetries } : {}),
|
|
585
|
+
...(frozenAgentTimeoutMs !== undefined ? { agentTimeoutMs: frozenAgentTimeoutMs } : {}),
|
|
444
586
|
...(options.toolset ? { toolset: options.toolset } : {}),
|
|
445
587
|
...(options.excludeTools ? { excludeTools: [...options.excludeTools] } : {}),
|
|
446
588
|
},
|
|
447
589
|
controller: new AbortController(),
|
|
448
590
|
generations: new Map(),
|
|
449
591
|
agentWaiters: new Map(),
|
|
592
|
+
bufferedTerminals: new Map(),
|
|
450
593
|
pendingSpawns: new Map(),
|
|
451
594
|
pendingJournal: new Set(),
|
|
452
595
|
lifecycleSuspended: false,
|
|
@@ -455,7 +598,7 @@ export class WorkflowEngine {
|
|
|
455
598
|
this.runs.set(runId, state);
|
|
456
599
|
this.setWorkflowStatus(run, "running");
|
|
457
600
|
state.executionGeneration += 1;
|
|
458
|
-
state.execution = this.execute(runId, script,
|
|
601
|
+
state.execution = this.execute(runId, script, startOptions, undefined, state.executionGeneration);
|
|
459
602
|
// A run-fatal abort/stop already journals its terminal state; never let a
|
|
460
603
|
// leftover rejection become an unhandled promise rejection.
|
|
461
604
|
void state.execution.catch(() => {});
|
|
@@ -468,7 +611,7 @@ export class WorkflowEngine {
|
|
|
468
611
|
runId,
|
|
469
612
|
status: settled.status,
|
|
470
613
|
background: false,
|
|
471
|
-
...(scriptResult !== undefined ? { result:
|
|
614
|
+
...(scriptResult !== undefined ? { result: renderWorkflowValue(scriptResult) } : {}),
|
|
472
615
|
...(settled.error ? { error: settled.error } : {}),
|
|
473
616
|
};
|
|
474
617
|
} catch (error: unknown) {
|
|
@@ -521,6 +664,26 @@ export class WorkflowEngine {
|
|
|
521
664
|
}),
|
|
522
665
|
);
|
|
523
666
|
}
|
|
667
|
+
let resumeArgs: unknown;
|
|
668
|
+
if (run.frozenArgsPresent === undefined && run.args === undefined && options.args !== undefined) {
|
|
669
|
+
const durableArgs = cloneWorkflowArgs(options.args);
|
|
670
|
+
const runArgs = cloneWorkflowArgs(options.args);
|
|
671
|
+
resumeArgs = cloneWorkflowArgs(options.args);
|
|
672
|
+
const timestamp = Date.now();
|
|
673
|
+
this.appendRequired({
|
|
674
|
+
kind: "run_args",
|
|
675
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
676
|
+
runId,
|
|
677
|
+
args: durableArgs,
|
|
678
|
+
timestamp,
|
|
679
|
+
});
|
|
680
|
+
run.args = runArgs;
|
|
681
|
+
run.updatedAt = timestamp;
|
|
682
|
+
} else {
|
|
683
|
+
// A current false marker freezes intentional omission; resume-supplied
|
|
684
|
+
// args are fallback input only for genuinely legacy schema-v3 runs.
|
|
685
|
+
resumeArgs = cloneWorkflowArgs(run.args);
|
|
686
|
+
}
|
|
524
687
|
const resumeJournal = buildResumeJournal(entries);
|
|
525
688
|
// Seed generation bases from journaled attemptIds so a live re-dispatch of a
|
|
526
689
|
// previously-journaled call never reuses a spawnKey (A4).
|
|
@@ -529,12 +692,13 @@ export class WorkflowEngine {
|
|
|
529
692
|
}
|
|
530
693
|
this.setWorkflowStatus(run, "running");
|
|
531
694
|
state.controller = new AbortController();
|
|
532
|
-
// Freeze semantics: original run parameters win over new options.
|
|
533
|
-
// Only signal/confirm/
|
|
534
|
-
// budget/scale/toolset are frozen from the original start.
|
|
695
|
+
// Freeze semantics: original run parameters and args win over new options.
|
|
696
|
+
// Only signal/confirm/background/loadSavedWorkflow/mainModel are per-resume context;
|
|
697
|
+
// input/budget/scale/toolset are frozen from the original start.
|
|
535
698
|
const frozen = state.frozenOptions;
|
|
536
699
|
const resumeOptions: ScriptStartOptions = {
|
|
537
700
|
...options,
|
|
701
|
+
args: resumeArgs,
|
|
538
702
|
tokenBudget: frozen.tokenBudget,
|
|
539
703
|
maxAgents: frozen.maxAgents,
|
|
540
704
|
concurrency: frozen.concurrency,
|
|
@@ -543,6 +707,7 @@ export class WorkflowEngine {
|
|
|
543
707
|
toolset: frozen.toolset,
|
|
544
708
|
excludeTools: frozen.excludeTools ? [...frozen.excludeTools] : undefined,
|
|
545
709
|
};
|
|
710
|
+
state.bufferedTerminals.clear();
|
|
546
711
|
state.executionGeneration += 1;
|
|
547
712
|
state.execution = this.execute(runId, run.script, resumeOptions, resumeJournal, state.executionGeneration);
|
|
548
713
|
void state.execution.catch(() => {});
|
|
@@ -624,6 +789,7 @@ export class WorkflowEngine {
|
|
|
624
789
|
if (!state) throw new Error(`workflow run not found: ${runId}`);
|
|
625
790
|
if (isTerminalWorkflow(state.run.status) || state.run.status === "paused" || state.run.status === "interrupted")
|
|
626
791
|
return state.run;
|
|
792
|
+
if (signal?.aborted) throw new WorkflowWaitAbortedError();
|
|
627
793
|
const all =
|
|
628
794
|
this.waiters.get(runId) ??
|
|
629
795
|
new Set<{
|
|
@@ -712,24 +878,42 @@ export class WorkflowEngine {
|
|
|
712
878
|
signal: state.controller.signal,
|
|
713
879
|
maxAgents: options.maxAgents,
|
|
714
880
|
runId,
|
|
881
|
+
executionNonce: `${generation}-${randomUUID()}`,
|
|
715
882
|
resumeJournal,
|
|
716
883
|
toolset: options.toolset,
|
|
717
884
|
excludeTools: options.excludeTools,
|
|
718
885
|
confirm: options.confirm,
|
|
719
886
|
loadSavedWorkflow: options.loadSavedWorkflow,
|
|
720
887
|
onAgentJournal: (entry) => {
|
|
721
|
-
|
|
722
|
-
//
|
|
723
|
-
//
|
|
888
|
+
if (state.executionGeneration !== generation) return;
|
|
889
|
+
// Nested workflow frames keep their own lifecycle visibility, but their
|
|
890
|
+
// individual call facts are not top-level replay keys. The parent
|
|
891
|
+
// workflow_result fact below owns the nested replay boundary.
|
|
724
892
|
if (entry.runId === undefined || entry.runId === runId) this.journalCallResult(runId, entry);
|
|
725
893
|
},
|
|
726
|
-
|
|
894
|
+
onWorkflowJournal: (entry) => {
|
|
895
|
+
if (state.executionGeneration !== generation) return;
|
|
896
|
+
this.journalWorkflowResult(runId, entry);
|
|
897
|
+
},
|
|
898
|
+
onRuntimeEvent: (event) => {
|
|
899
|
+
if (state.executionGeneration !== generation) return;
|
|
900
|
+
try {
|
|
901
|
+
this.events.emit("pi-workflows:runtime", { runId, event: cloneRuntimeEvent(event) });
|
|
902
|
+
} catch {
|
|
903
|
+
// Cross-extension observers cannot change workflow execution.
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
this.onRuntimeEvent?.(runId, cloneRuntimeEvent(event));
|
|
907
|
+
} catch {
|
|
908
|
+
// UI observers are diagnostic surfaces and are independently contained.
|
|
909
|
+
}
|
|
910
|
+
},
|
|
727
911
|
});
|
|
728
912
|
const live = this.runs.get(runId);
|
|
729
|
-
if (live && state.executionGeneration === generation) live.result = result.result;
|
|
730
913
|
const run = live?.run;
|
|
731
914
|
if (run && state.executionGeneration === generation && !isTerminalWorkflow(run.status)) {
|
|
732
|
-
this.setWorkflowStatus(run, "completed");
|
|
915
|
+
this.setWorkflowStatus(run, "completed", undefined, undefined, result.result);
|
|
916
|
+
live.result = result.result;
|
|
733
917
|
}
|
|
734
918
|
return result;
|
|
735
919
|
} catch (error: unknown) {
|
|
@@ -758,9 +942,9 @@ export class WorkflowEngine {
|
|
|
758
942
|
|
|
759
943
|
/**
|
|
760
944
|
* Dispatch one live agent() call through spawn-managed with an A4-rotated
|
|
761
|
-
* spawnKey: `${runId}/call-${callIndex}/attempt-${generation}`.
|
|
762
|
-
*
|
|
763
|
-
*
|
|
945
|
+
* spawnKey: `${runId}/call-${callIndex}/attempt-${generation}`. Top-level calls
|
|
946
|
+
* use numeric call indexes; nested workflow frames use a generation/nonce
|
|
947
|
+
* namespace so a changed child cannot reuse a stale managed identity.
|
|
764
948
|
*/
|
|
765
949
|
private async dispatchAgent(runId: string, prompt: string, runOptions: AgentRunOptions): Promise<unknown> {
|
|
766
950
|
const state = this.runs.get(runId);
|
|
@@ -773,13 +957,19 @@ export class WorkflowEngine {
|
|
|
773
957
|
const run = state.run;
|
|
774
958
|
const executionGeneration = state.executionGeneration;
|
|
775
959
|
const callIndex = runOptions.callIndex ?? state.generations.size;
|
|
960
|
+
const frameRunId = runOptions.runId ?? runId;
|
|
961
|
+
const nested = frameRunId !== runId;
|
|
962
|
+
// Top-level calls use their numeric lexical index. Nested frames use a
|
|
963
|
+
// run-local namespace so child call 0 can never collide with the parent's
|
|
964
|
+
// call 0 or with a sibling nested workflow. The managed owner remains the
|
|
965
|
+
// top-level run: nested workflow results are journaled as one parent fact.
|
|
966
|
+
const nodeId = nested ? `${frameRunId}:${callIndex}` : String(callIndex);
|
|
776
967
|
// The spawnKey identity carries the `call-` prefix (A4):
|
|
777
|
-
// spawnKey = `${runId}/call-${
|
|
778
|
-
// The journal node identity is the bare call index
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
const spawnNodeId = `call-${
|
|
782
|
-
const nodeId = String(callIndex);
|
|
968
|
+
// spawnKey = `${runId}/call-${nodeId}/attempt-${generation}`
|
|
969
|
+
// The journal node identity is the bare call index for top-level calls;
|
|
970
|
+
// nested identities are in-memory only and are never persisted as agent
|
|
971
|
+
// facts.
|
|
972
|
+
const spawnNodeId = `call-${nodeId}`;
|
|
783
973
|
const base = state.generations.get(nodeId) ?? 0;
|
|
784
974
|
const generation = base + (runOptions.attempt ?? 1);
|
|
785
975
|
state.generations.set(nodeId, generation);
|
|
@@ -803,23 +993,27 @@ export class WorkflowEngine {
|
|
|
803
993
|
...(runOptions.isolation === undefined ? {} : { isolation: runOptions.isolation }),
|
|
804
994
|
};
|
|
805
995
|
|
|
806
|
-
// Journal the attempt rotation
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
996
|
+
// Journal the attempt rotation only for top-level calls. A nested frame is
|
|
997
|
+
// represented by its parent workflow_result replay boundary instead of
|
|
998
|
+
// leaking child call facts into the parent's numeric call namespace.
|
|
999
|
+
if (!nested) {
|
|
1000
|
+
this.persist(runId, `call-attempt:${nodeId}`, () =>
|
|
1001
|
+
this.write({
|
|
1002
|
+
kind: "call_attempt",
|
|
1003
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
1004
|
+
runId,
|
|
1005
|
+
nodeId,
|
|
1006
|
+
attemptId,
|
|
1007
|
+
generation,
|
|
1008
|
+
owner: { extension: "pi-workflows", runId, nodeId, attemptId },
|
|
1009
|
+
timestamp: Date.now(),
|
|
1010
|
+
}),
|
|
1011
|
+
);
|
|
1012
|
+
run.callStatus[nodeId] = "running";
|
|
1013
|
+
run.attempts[nodeId] = generation;
|
|
1014
|
+
run.attemptIds[nodeId] = attemptId;
|
|
1015
|
+
run.updatedAt = Date.now();
|
|
1016
|
+
}
|
|
823
1017
|
|
|
824
1018
|
// Abort linkage: an external signal (pause/stop/Esc) cancels the wait and
|
|
825
1019
|
// stops the owned agent. Track the spawn key before the RPC reply arrives so
|
|
@@ -832,9 +1026,9 @@ export class WorkflowEngine {
|
|
|
832
1026
|
nodeId: spawnNodeId,
|
|
833
1027
|
attemptId: spawnAttemptId,
|
|
834
1028
|
};
|
|
835
|
-
|
|
836
|
-
state.pendingSpawns.set(pendingKey, { spawnKey, owner });
|
|
1029
|
+
state.pendingSpawns.set(spawnKey, { spawnKey, owner, executionGeneration });
|
|
837
1030
|
const reconcilePending = () => {
|
|
1031
|
+
state.bufferedTerminals.delete(spawnKey);
|
|
838
1032
|
const reconciliation = this.client.reconcileManaged?.(spawnKey, owner);
|
|
839
1033
|
void reconciliation?.catch(() => {});
|
|
840
1034
|
};
|
|
@@ -848,7 +1042,7 @@ export class WorkflowEngine {
|
|
|
848
1042
|
response = await this.client.spawn(task, runId, spawnNodeId, spawnAttemptId, undefined, externalSignal);
|
|
849
1043
|
} finally {
|
|
850
1044
|
externalSignal.removeEventListener("abort", onPendingAbort);
|
|
851
|
-
state.pendingSpawns.delete(
|
|
1045
|
+
state.pendingSpawns.delete(spawnKey);
|
|
852
1046
|
}
|
|
853
1047
|
const responseId = typeof response === "string" ? response : response.id;
|
|
854
1048
|
if (
|
|
@@ -856,6 +1050,7 @@ export class WorkflowEngine {
|
|
|
856
1050
|
state.controller.signal.aborted ||
|
|
857
1051
|
externalSignal.aborted
|
|
858
1052
|
) {
|
|
1053
|
+
state.bufferedTerminals.delete(spawnKey);
|
|
859
1054
|
await this.client
|
|
860
1055
|
.stopOwned?.(responseId, { extension: "pi-workflows", runId, nodeId: spawnNodeId, attemptId: spawnAttemptId })
|
|
861
1056
|
.catch(() => {});
|
|
@@ -868,6 +1063,7 @@ export class WorkflowEngine {
|
|
|
868
1063
|
if (typeof response === "string") {
|
|
869
1064
|
pendingId = response;
|
|
870
1065
|
} else if (response.terminal) {
|
|
1066
|
+
state.bufferedTerminals.delete(spawnKey);
|
|
871
1067
|
// The spawn response already carries a terminal snapshot (fast completion
|
|
872
1068
|
// or interrupted replay). The runtime journals successful calls itself
|
|
873
1069
|
// (onAgentJournal); a terminal snapshot that is NOT a success is
|
|
@@ -893,7 +1089,7 @@ export class WorkflowEngine {
|
|
|
893
1089
|
terminal.tokenCount,
|
|
894
1090
|
);
|
|
895
1091
|
result.attemptId = attemptId;
|
|
896
|
-
this.commitCallTerminal(runId, nodeId, result);
|
|
1092
|
+
if (!nested) this.commitCallTerminal(runId, nodeId, result);
|
|
897
1093
|
if (status === "failed" && terminal.error) {
|
|
898
1094
|
throw wrapError(new Error(terminal.error), { agentLabel: runOptions.label });
|
|
899
1095
|
}
|
|
@@ -908,6 +1104,7 @@ export class WorkflowEngine {
|
|
|
908
1104
|
if (waiter && !waiter.settled) {
|
|
909
1105
|
waiter.settled = true;
|
|
910
1106
|
state.agentWaiters.delete(pendingId);
|
|
1107
|
+
state.bufferedTerminals.delete(spawnKey);
|
|
911
1108
|
void this.client
|
|
912
1109
|
.stopOwned?.(pendingId, {
|
|
913
1110
|
extension: "pi-workflows",
|
|
@@ -920,10 +1117,11 @@ export class WorkflowEngine {
|
|
|
920
1117
|
}
|
|
921
1118
|
};
|
|
922
1119
|
|
|
923
|
-
// Wait for the terminal lifecycle event
|
|
1120
|
+
// Wait for the terminal lifecycle event, consuming a matching terminal that
|
|
1121
|
+
// arrived while the managed-spawn reply was still pending.
|
|
924
1122
|
const terminal = await new Promise<ManagedSpawnResponse>((resolve, reject) => {
|
|
925
|
-
const waiter = {
|
|
926
|
-
callIndex:
|
|
1123
|
+
const waiter: AgentLifecycleWaiter = {
|
|
1124
|
+
callIndex: nodeId,
|
|
927
1125
|
generation,
|
|
928
1126
|
executionGeneration,
|
|
929
1127
|
onUsage: runOptions.onUsage,
|
|
@@ -934,7 +1132,24 @@ export class WorkflowEngine {
|
|
|
934
1132
|
};
|
|
935
1133
|
state.agentWaiters.set(pendingId, waiter);
|
|
936
1134
|
externalSignal.addEventListener("abort", waitAbort, { once: true });
|
|
937
|
-
if (externalSignal.aborted)
|
|
1135
|
+
if (externalSignal.aborted) {
|
|
1136
|
+
waitAbort();
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const buffered = state.bufferedTerminals.get(spawnKey);
|
|
1141
|
+
if (!buffered) return;
|
|
1142
|
+
state.bufferedTerminals.delete(spawnKey);
|
|
1143
|
+
if (
|
|
1144
|
+
buffered.executionGeneration !== executionGeneration ||
|
|
1145
|
+
state.executionGeneration !== executionGeneration ||
|
|
1146
|
+
state.lifecycleSuspended ||
|
|
1147
|
+
state.controller.signal.aborted ||
|
|
1148
|
+
buffered.response.id !== pendingId ||
|
|
1149
|
+
!sameWorkflowOwner(buffered.owner, owner)
|
|
1150
|
+
)
|
|
1151
|
+
return;
|
|
1152
|
+
this.resolveAgentWaiter(state, pendingId, waiter, buffered);
|
|
938
1153
|
}).finally(() => {
|
|
939
1154
|
externalSignal.removeEventListener("abort", waitAbort);
|
|
940
1155
|
});
|
|
@@ -958,7 +1173,7 @@ export class WorkflowEngine {
|
|
|
958
1173
|
snapshot.tokenCount,
|
|
959
1174
|
);
|
|
960
1175
|
result.attemptId = attemptId;
|
|
961
|
-
this.commitCallTerminal(runId, nodeId, result);
|
|
1176
|
+
if (!nested) this.commitCallTerminal(runId, nodeId, result);
|
|
962
1177
|
if (status === "failed" && snapshot.error) {
|
|
963
1178
|
throw wrapError(new Error(snapshot.error), { agentLabel: runOptions.label });
|
|
964
1179
|
}
|
|
@@ -1000,6 +1215,40 @@ export class WorkflowEngine {
|
|
|
1000
1215
|
run.updatedAt = Date.now();
|
|
1001
1216
|
}
|
|
1002
1217
|
|
|
1218
|
+
private journalWorkflowResult(runId: string, entry: JournalEntry): void {
|
|
1219
|
+
const state = this.runs.get(runId);
|
|
1220
|
+
if (!state) return;
|
|
1221
|
+
const nodeId = String(entry.index);
|
|
1222
|
+
const result: CallResult = {
|
|
1223
|
+
status: "completed",
|
|
1224
|
+
...(entry.result === undefined ? {} : { result: entry.result }),
|
|
1225
|
+
compactionCount: 0,
|
|
1226
|
+
updatedAt: Date.now(),
|
|
1227
|
+
};
|
|
1228
|
+
const event: JournalEvent = {
|
|
1229
|
+
kind: "workflow_result",
|
|
1230
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
1231
|
+
runId,
|
|
1232
|
+
nodeId,
|
|
1233
|
+
result,
|
|
1234
|
+
generation: entry.generation ?? 1,
|
|
1235
|
+
...(entry.agentCount === undefined ? {} : { agentCount: entry.agentCount }),
|
|
1236
|
+
callHash: entry.hash,
|
|
1237
|
+
...(entry.storeDelta !== undefined && Object.keys(entry.storeDelta).length > 0
|
|
1238
|
+
? { storeDelta: entry.storeDelta }
|
|
1239
|
+
: {}),
|
|
1240
|
+
timestamp: Date.now(),
|
|
1241
|
+
};
|
|
1242
|
+
// The parent cannot become completed until this replay boundary is durable;
|
|
1243
|
+
// otherwise a transient append retry would arrive after the terminal
|
|
1244
|
+
// transition and be discarded as stale during restore.
|
|
1245
|
+
this.appendRequired(event);
|
|
1246
|
+
state.run.workflowResultGenerations[nodeId] = event.generation;
|
|
1247
|
+
state.run.callResults[nodeId] = result;
|
|
1248
|
+
state.run.callStatus[nodeId] = "completed";
|
|
1249
|
+
state.run.updatedAt = Date.now();
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1003
1252
|
private commitCallTerminal(runId: string, nodeId: string, result: CallResult): void {
|
|
1004
1253
|
const state = this.runs.get(runId);
|
|
1005
1254
|
if (!state) return;
|
|
@@ -1031,6 +1280,7 @@ export class WorkflowEngine {
|
|
|
1031
1280
|
void reconciliation?.catch(() => {});
|
|
1032
1281
|
}
|
|
1033
1282
|
state.pendingSpawns.clear();
|
|
1283
|
+
state.bufferedTerminals.clear();
|
|
1034
1284
|
}
|
|
1035
1285
|
|
|
1036
1286
|
private async quiescePendingSpawns(state: ScriptRunState): Promise<WorkflowQuiesceResult> {
|
|
@@ -1183,7 +1433,7 @@ export class WorkflowEngine {
|
|
|
1183
1433
|
this.journal.append(event);
|
|
1184
1434
|
} catch (error: unknown) {
|
|
1185
1435
|
throw new WorkflowError(
|
|
1186
|
-
`Could not persist workflow
|
|
1436
|
+
`Could not persist workflow ${event.kind}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1187
1437
|
WorkflowErrorCode.PERSISTENCE_ERROR,
|
|
1188
1438
|
{ recoverable: true, details: error },
|
|
1189
1439
|
);
|
|
@@ -1271,8 +1521,13 @@ export class WorkflowEngine {
|
|
|
1271
1521
|
status: ScriptRun["status"],
|
|
1272
1522
|
error?: string,
|
|
1273
1523
|
terminalIntent?: "stop" | "failure",
|
|
1524
|
+
finalResult?: unknown,
|
|
1274
1525
|
): void {
|
|
1275
1526
|
if (isTerminalWorkflow(run.status) && !isTerminalWorkflow(status)) return;
|
|
1527
|
+
// Validate and isolate terminal values before best-effort persistence or any
|
|
1528
|
+
// status mutation. A bad result must still be able to transition the run to failed.
|
|
1529
|
+
const durableFinalResult = finalResult === undefined ? undefined : cloneWorkflowResult(finalResult);
|
|
1530
|
+
const runFinalResult = finalResult === undefined ? undefined : cloneWorkflowResult(finalResult);
|
|
1276
1531
|
const timestamp = Date.now();
|
|
1277
1532
|
this.persist(run.runId, `transition:${status}`, () =>
|
|
1278
1533
|
this.write({
|
|
@@ -1281,15 +1536,18 @@ export class WorkflowEngine {
|
|
|
1281
1536
|
runId: run.runId,
|
|
1282
1537
|
status,
|
|
1283
1538
|
...(error !== undefined ? { error } : {}),
|
|
1539
|
+
...(durableFinalResult !== undefined ? { finalResult: durableFinalResult } : {}),
|
|
1284
1540
|
...(terminalIntent !== undefined ? { terminalIntent } : {}),
|
|
1285
1541
|
timestamp,
|
|
1286
1542
|
}),
|
|
1287
1543
|
);
|
|
1288
1544
|
run.status = status;
|
|
1289
1545
|
if (error !== undefined) run.error = error;
|
|
1546
|
+
if (runFinalResult !== undefined) run.finalResult = runFinalResult;
|
|
1290
1547
|
if (terminalIntent !== undefined) run.terminalIntent = terminalIntent;
|
|
1291
1548
|
run.updatedAt = timestamp;
|
|
1292
1549
|
if (isTerminalWorkflow(status)) {
|
|
1550
|
+
this.runs.get(run.runId)?.bufferedTerminals.clear();
|
|
1293
1551
|
this.settleWaiters(run.runId, run);
|
|
1294
1552
|
this.onRunSettled?.();
|
|
1295
1553
|
}
|
|
@@ -1326,6 +1584,7 @@ export class WorkflowEngine {
|
|
|
1326
1584
|
controller: new AbortController(),
|
|
1327
1585
|
generations: new Map(),
|
|
1328
1586
|
agentWaiters: new Map(),
|
|
1587
|
+
bufferedTerminals: new Map(),
|
|
1329
1588
|
pendingSpawns: new Map(),
|
|
1330
1589
|
pendingJournal: new Set(),
|
|
1331
1590
|
lifecycleSuspended: false,
|
|
@@ -1374,6 +1633,7 @@ export class WorkflowEngine {
|
|
|
1374
1633
|
startedAt: run.startedAt,
|
|
1375
1634
|
updatedAt: run.updatedAt,
|
|
1376
1635
|
...(run.error ? { error: run.error.slice(0, 2_000) } : {}),
|
|
1636
|
+
...(run.finalResult !== undefined ? { resultPreview: renderWorkflowValue(run.finalResult).slice(0, 8_000) } : {}),
|
|
1377
1637
|
};
|
|
1378
1638
|
if (!detailed) return base;
|
|
1379
1639
|
const calls = Object.entries(run.callStatus)
|