@bastani/atomic 0.8.14-0 → 0.8.15-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 +35 -0
- package/README.md +0 -8
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +3 -0
- package/dist/builtin/mcp/index.ts +4 -8
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/skills/tmux/SKILL.md +220 -0
- package/dist/builtin/subagents/skills/tmux/scripts/find-sessions.sh +112 -0
- package/dist/builtin/subagents/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +10 -1
- package/dist/builtin/workflows/README.md +3 -1
- package/dist/builtin/workflows/builtin/ralph.ts +222 -295
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +20 -11
- package/dist/builtin/workflows/src/extension/index.ts +1 -0
- package/dist/builtin/workflows/src/extension/status-writer.ts +18 -3
- package/dist/builtin/workflows/src/runs/background/runner.ts +8 -10
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +484 -91
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +13 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +41 -15
- package/dist/builtin/workflows/src/runs/shared/graph-inference.ts +31 -0
- package/dist/builtin/workflows/src/runs/shared/prompt-callsite.ts +98 -0
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +3 -1
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
- package/dist/builtin/workflows/src/shared/store-types.ts +12 -1
- package/dist/builtin/workflows/src/shared/store.ts +77 -3
- package/dist/builtin/workflows/src/tui/graph-view.ts +17 -1
- package/dist/builtin/workflows/src/tui/prompt-card.ts +185 -30
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +386 -21
- package/docs/changelog.mdx +41 -14
- package/docs/docs.json +1 -0
- package/docs/extensions.md +19 -19
- package/docs/images/workflow-input-picker.png +0 -0
- package/docs/images/workflow-list.png +0 -0
- package/docs/index.md +33 -27
- package/docs/providers.md +2 -2
- package/docs/quickstart.md +15 -15
- package/docs/sdk.md +8 -8
- package/docs/sessions.md +5 -5
- package/docs/settings.md +27 -1
- package/docs/skills.md +2 -2
- package/docs/subagents.md +157 -0
- package/docs/usage.md +7 -7
- package/docs/windows.md +8 -0
- package/docs/workflows.md +62 -9
- package/package.json +2 -1
- package/docs/images/doom-extension.png +0 -0
- package/docs/images/exy.png +0 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Main DAG executor: run(def, inputs, opts) → RunResult
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
7
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
7
8
|
import { CONFIG_DIR_NAME } from "@bastani/atomic";
|
|
@@ -40,6 +41,8 @@ import type {
|
|
|
40
41
|
RunSnapshot,
|
|
41
42
|
WorkflowOverlayAdapter,
|
|
42
43
|
WorkflowFailureKind,
|
|
44
|
+
PendingPrompt,
|
|
45
|
+
PromptKind,
|
|
43
46
|
} from "../../shared/store-types.js";
|
|
44
47
|
import type { StageControlHandle, StageControlRegistry, AgentSessionEventListener } from "./stage-control-registry.js";
|
|
45
48
|
import type { Store } from "../../shared/store.js";
|
|
@@ -68,6 +71,7 @@ import {
|
|
|
68
71
|
import { validateWorkflowModels } from "../shared/model-fallback.js";
|
|
69
72
|
import type { WorkflowFailure } from "../../shared/workflow-failures.js";
|
|
70
73
|
import { classifyWorkflowFailure } from "../../shared/workflow-failures.js";
|
|
74
|
+
import { selectPromptCallsiteFrame } from "../shared/prompt-callsite.js";
|
|
71
75
|
|
|
72
76
|
export interface ResolvedInputs extends Record<string, unknown> {}
|
|
73
77
|
|
|
@@ -80,6 +84,8 @@ export interface RunOpts {
|
|
|
80
84
|
adapters?: StageAdapters;
|
|
81
85
|
/** HIL adapter injected by the pi runtime or test harness. */
|
|
82
86
|
ui?: WorkflowUIAdapter;
|
|
87
|
+
/** Internal detached-run mode: surface ctx.ui.* as node-local workflow prompt stages. */
|
|
88
|
+
usePromptNodesForUi?: boolean;
|
|
83
89
|
/** Store override (for testing; defaults to singleton store) */
|
|
84
90
|
store?: Store;
|
|
85
91
|
/** Persistence port for writing session entries (run.start, stage.start, etc.). */
|
|
@@ -184,6 +190,69 @@ function resolveInputConcurrency(
|
|
|
184
190
|
// HIL unavailable fallback — rejects with precise per-primitive error
|
|
185
191
|
// ---------------------------------------------------------------------------
|
|
186
192
|
|
|
193
|
+
interface PromptDescriptor {
|
|
194
|
+
readonly kind: PromptKind;
|
|
195
|
+
readonly message: string;
|
|
196
|
+
readonly choices?: readonly string[];
|
|
197
|
+
readonly initial?: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function fallbackForPromptDescriptor(descriptor: PromptDescriptor): unknown {
|
|
201
|
+
switch (descriptor.kind) {
|
|
202
|
+
case "input":
|
|
203
|
+
case "editor":
|
|
204
|
+
return descriptor.initial ?? "";
|
|
205
|
+
case "confirm":
|
|
206
|
+
return false;
|
|
207
|
+
case "select":
|
|
208
|
+
return descriptor.choices?.[0] ?? "";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function makePrompt(descriptor: PromptDescriptor): PendingPrompt {
|
|
213
|
+
return {
|
|
214
|
+
id: `hil-${crypto.randomUUID()}`,
|
|
215
|
+
kind: descriptor.kind,
|
|
216
|
+
message: descriptor.message,
|
|
217
|
+
...(descriptor.choices !== undefined ? { choices: descriptor.choices } : {}),
|
|
218
|
+
...(descriptor.initial !== undefined ? { initial: descriptor.initial } : {}),
|
|
219
|
+
createdAt: Date.now(),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function stableHash(value: unknown): string {
|
|
224
|
+
// 128 bits is plenty for replay-key identity while keeping graph labels compact.
|
|
225
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 32);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function promptDescriptorHash(descriptor: PromptDescriptor): string {
|
|
229
|
+
return stableHash({
|
|
230
|
+
kind: descriptor.kind,
|
|
231
|
+
message: descriptor.message,
|
|
232
|
+
choices: descriptor.choices ?? [],
|
|
233
|
+
// Include input/editor initial text because it is visible prompt context;
|
|
234
|
+
// changing it should not replay a stale answer from the same callsite.
|
|
235
|
+
initial: descriptor.initial ?? null,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function promptReplayKey(descriptor: PromptDescriptor): string {
|
|
240
|
+
return `prompt:${descriptor.kind}:${promptDescriptorHash(descriptor)}:${promptCallsiteHash()}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function promptCallsiteHash(): string {
|
|
244
|
+
// Capturing an Error stack is intentional here: HIL prompts are an
|
|
245
|
+
// interactive slow path, and the author callsite is part of the replay key.
|
|
246
|
+
const frame = selectPromptCallsiteFrame(new Error().stack ?? "") ?? "unknown";
|
|
247
|
+
return stableHash(frame);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function hilAbortError(signal: AbortSignal): Error {
|
|
251
|
+
return signal.reason instanceof Error
|
|
252
|
+
? signal.reason
|
|
253
|
+
: new Error("pi-workflows: HIL aborted");
|
|
254
|
+
}
|
|
255
|
+
|
|
187
256
|
function makeUnavailableUIContext(): WorkflowUIContext {
|
|
188
257
|
const msg = (primitive: string): string =>
|
|
189
258
|
`pi-workflows: HIL ctx.ui.${primitive} is unavailable because pi runtime did not provide a UI adapter`;
|
|
@@ -196,8 +265,8 @@ function makeUnavailableUIContext(): WorkflowUIContext {
|
|
|
196
265
|
}
|
|
197
266
|
|
|
198
267
|
type AskUserQuestionToolEvent =
|
|
199
|
-
| { phase: "start"; callId
|
|
200
|
-
| { phase: "end"; callId
|
|
268
|
+
| { phase: "start"; callId?: string }
|
|
269
|
+
| { phase: "end"; callId?: string; nameMatched: boolean };
|
|
201
270
|
|
|
202
271
|
function stringField(value: Record<string, unknown>, keys: readonly string[]): string | undefined {
|
|
203
272
|
for (const key of keys) {
|
|
@@ -217,7 +286,7 @@ function askUserQuestionToolEvent(event: unknown): AskUserQuestionToolEvent | un
|
|
|
217
286
|
const record = event as Record<string, unknown>;
|
|
218
287
|
const type = typeof record["type"] === "string" ? record["type"] : "";
|
|
219
288
|
const toolName = stringField(record, ["toolName", "tool_name", "name"]);
|
|
220
|
-
const callId = stringField(record, ["toolCallId", "tool_call_id", "toolUseId", "tool_use_id", "id"])
|
|
289
|
+
const callId = stringField(record, ["toolCallId", "tool_call_id", "toolUseId", "tool_use_id", "id"]);
|
|
221
290
|
|
|
222
291
|
if (type === "tool_execution_start" && isAskUserQuestionToolName(toolName)) {
|
|
223
292
|
return { phase: "start", callId };
|
|
@@ -1094,12 +1163,52 @@ function runFailureMetadata(err: unknown, stages: readonly StageSnapshot[]): Run
|
|
|
1094
1163
|
};
|
|
1095
1164
|
}
|
|
1096
1165
|
|
|
1166
|
+
function stageReplayFields(stage: StageSnapshot): Partial<Pick<StageSnapshot, "replayKey" | "replayedFromStageId" | "replayed">> {
|
|
1167
|
+
return {
|
|
1168
|
+
...(stage.replayKey !== undefined ? { replayKey: stage.replayKey } : {}),
|
|
1169
|
+
...(stage.replayedFromStageId !== undefined ? { replayedFromStageId: stage.replayedFromStageId } : {}),
|
|
1170
|
+
...(stage.replayed !== undefined ? { replayed: stage.replayed } : {}),
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
type PromptAnswerReplaySafety = "allowed" | "unavailable" | "ambiguous";
|
|
1175
|
+
|
|
1176
|
+
function getPromptAnswerState(
|
|
1177
|
+
hasReplayAnswer: boolean,
|
|
1178
|
+
replaySourceId: string | undefined,
|
|
1179
|
+
answerReplay: PromptAnswerReplaySafety,
|
|
1180
|
+
): StageSnapshot["promptAnswerState"] {
|
|
1181
|
+
if (replaySourceId === undefined) return undefined;
|
|
1182
|
+
if (hasReplayAnswer) return "available";
|
|
1183
|
+
if (answerReplay === "ambiguous") return "ambiguous";
|
|
1184
|
+
return "unavailable";
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1097
1187
|
type ContinuationReplayDecision =
|
|
1098
|
-
| {
|
|
1099
|
-
|
|
1188
|
+
| {
|
|
1189
|
+
readonly kind: "execute";
|
|
1190
|
+
readonly source?: StageSnapshot;
|
|
1191
|
+
readonly parentIds: readonly string[];
|
|
1192
|
+
readonly answerReplay: PromptAnswerReplaySafety;
|
|
1193
|
+
}
|
|
1194
|
+
| {
|
|
1195
|
+
readonly kind: "replay";
|
|
1196
|
+
readonly source: StageSnapshot;
|
|
1197
|
+
readonly parentIds: readonly string[];
|
|
1198
|
+
readonly answerReplay: PromptAnswerReplaySafety;
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
interface ContinuationReplayInput {
|
|
1202
|
+
readonly displayName: string;
|
|
1203
|
+
readonly replayKey: string;
|
|
1204
|
+
readonly parentIds: readonly string[];
|
|
1205
|
+
readonly stageId: string;
|
|
1206
|
+
readonly kind: "stage" | "prompt";
|
|
1207
|
+
}
|
|
1100
1208
|
|
|
1101
1209
|
interface ContinuationReplayIndex {
|
|
1102
|
-
decide(
|
|
1210
|
+
decide(input: ContinuationReplayInput): ContinuationReplayDecision;
|
|
1211
|
+
markPromptAnswerReplayed(stageId: string): void;
|
|
1103
1212
|
}
|
|
1104
1213
|
|
|
1105
1214
|
function sameStringSet(left: readonly string[], right: readonly string[]): boolean {
|
|
@@ -1108,46 +1217,135 @@ function sameStringSet(left: readonly string[], right: readonly string[]): boole
|
|
|
1108
1217
|
return left.every((value) => rightSet.has(value));
|
|
1109
1218
|
}
|
|
1110
1219
|
|
|
1220
|
+
function sortedIdentity(values: readonly string[]): string {
|
|
1221
|
+
return [...values].sort().join("\u0000");
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1111
1224
|
function createContinuationReplayIndex(continuation: RunContinuationOpts | undefined): ContinuationReplayIndex {
|
|
1112
|
-
if (continuation === undefined)
|
|
1225
|
+
if (continuation === undefined) {
|
|
1226
|
+
return {
|
|
1227
|
+
decide: (input) => ({
|
|
1228
|
+
kind: "execute",
|
|
1229
|
+
parentIds: input.parentIds,
|
|
1230
|
+
answerReplay: "unavailable",
|
|
1231
|
+
}),
|
|
1232
|
+
markPromptAnswerReplayed: () => {},
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1113
1235
|
const resumeStage = continuation.source.stages.find((stage) => stage.id === continuation.resumeFromStageId);
|
|
1114
1236
|
if (resumeStage === undefined) {
|
|
1115
1237
|
throw new Error(`pi-workflows: insufficient_state: resume stage ${continuation.resumeFromStageId} was not found in source run ${continuation.source.id}`);
|
|
1116
1238
|
}
|
|
1117
1239
|
|
|
1118
|
-
const
|
|
1240
|
+
const stagesByReplayIdentity = new Map<string, StageSnapshot[]>();
|
|
1241
|
+
const promptDuplicateCounts = new Map<string, number>();
|
|
1119
1242
|
for (const stage of continuation.source.stages) {
|
|
1120
|
-
const
|
|
1243
|
+
const identity = stage.replayKey ?? stage.name;
|
|
1244
|
+
const stages = stagesByReplayIdentity.get(identity);
|
|
1121
1245
|
if (stages === undefined) {
|
|
1122
|
-
|
|
1246
|
+
stagesByReplayIdentity.set(identity, [stage]);
|
|
1123
1247
|
} else {
|
|
1124
1248
|
stages.push(stage);
|
|
1125
1249
|
}
|
|
1250
|
+
const duplicateKey = `${identity}\u0001${sortedIdentity(stage.parentIds)}`;
|
|
1251
|
+
promptDuplicateCounts.set(duplicateKey, (promptDuplicateCounts.get(duplicateKey) ?? 0) + 1);
|
|
1126
1252
|
}
|
|
1127
1253
|
|
|
1128
1254
|
const consumedSourceStageIds = new Set<string>();
|
|
1129
|
-
const
|
|
1255
|
+
const continuationStageIdBySourceStageId = new Map<string, string>();
|
|
1256
|
+
const replayablePromptContinuationStageIds = new Set<string>();
|
|
1257
|
+
|
|
1258
|
+
const failTopology = (displayName: string, replayKey: string, reason: "mismatch" | "ambiguous"): never => {
|
|
1259
|
+
throw new Error(`pi-workflows: insufficient_state: replay topology ${reason} for stage "${displayName}" (replayKey "${replayKey}") in source run ${continuation.source.id}`);
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const translateSourceParents = (source: StageSnapshot): string[] | undefined => {
|
|
1263
|
+
const parentIds: string[] = [];
|
|
1264
|
+
for (const sourceParentId of source.parentIds) {
|
|
1265
|
+
const continuationParentId = continuationStageIdBySourceStageId.get(sourceParentId);
|
|
1266
|
+
if (continuationParentId === undefined) return undefined;
|
|
1267
|
+
parentIds.push(continuationParentId);
|
|
1268
|
+
}
|
|
1269
|
+
return parentIds;
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
const allSameParentSet = (candidates: readonly { readonly parentIds: readonly string[] }[]): boolean => {
|
|
1273
|
+
const first = candidates[0]?.parentIds;
|
|
1274
|
+
if (first === undefined) return false;
|
|
1275
|
+
return candidates.every((candidate) => sameStringSet(candidate.parentIds, first));
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const hasOnlyReplayablePromptParentDrift = (
|
|
1279
|
+
sourceParentIds: readonly string[],
|
|
1280
|
+
provisionalParentIds: readonly string[],
|
|
1281
|
+
): boolean => {
|
|
1282
|
+
const sourceParentSet = new Set(sourceParentIds);
|
|
1283
|
+
const provisionalParentSet = new Set(provisionalParentIds);
|
|
1284
|
+
const driftParentIds = [
|
|
1285
|
+
...sourceParentIds.filter((parentId) => !provisionalParentSet.has(parentId)),
|
|
1286
|
+
...provisionalParentIds.filter((parentId) => !sourceParentSet.has(parentId)),
|
|
1287
|
+
];
|
|
1288
|
+
return driftParentIds.length > 0 && driftParentIds.every((parentId) => replayablePromptContinuationStageIds.has(parentId));
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1130
1291
|
return {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1292
|
+
markPromptAnswerReplayed(stageId: string): void {
|
|
1293
|
+
replayablePromptContinuationStageIds.add(stageId);
|
|
1294
|
+
},
|
|
1295
|
+
|
|
1296
|
+
decide(input: ContinuationReplayInput): ContinuationReplayDecision {
|
|
1297
|
+
const { displayName, replayKey, parentIds, stageId, kind } = input;
|
|
1298
|
+
let identity = replayKey;
|
|
1299
|
+
let candidates = stagesByReplayIdentity.get(replayKey)?.filter((stage) => !consumedSourceStageIds.has(stage.id)) ?? [];
|
|
1300
|
+
if (candidates.length === 0) {
|
|
1301
|
+
// Legacy snapshots created before replayKey existed can only be matched
|
|
1302
|
+
// by display name. Current stage and prompt nodes always carry replayKey.
|
|
1303
|
+
identity = displayName;
|
|
1304
|
+
candidates = stagesByReplayIdentity.get(displayName)?.filter((stage) => !consumedSourceStageIds.has(stage.id) && stage.replayKey === undefined) ?? [];
|
|
1144
1305
|
}
|
|
1306
|
+
if (candidates.length === 0) {
|
|
1307
|
+
return { kind: "execute", parentIds, answerReplay: "unavailable" };
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const mappedCandidates = candidates
|
|
1311
|
+
.map((source) => ({ source, parentIds: translateSourceParents(source) }))
|
|
1312
|
+
.filter((candidate): candidate is { readonly source: StageSnapshot; readonly parentIds: string[] } => candidate.parentIds !== undefined);
|
|
1145
1313
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1314
|
+
if (mappedCandidates.length === 0) {
|
|
1315
|
+
failTopology(displayName, replayKey, "mismatch");
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const provisionalMatches = mappedCandidates.filter((candidate) => sameStringSet(candidate.parentIds, parentIds));
|
|
1319
|
+
const hasPromptDriftMatch = kind === "prompt" &&
|
|
1320
|
+
allSameParentSet(mappedCandidates) &&
|
|
1321
|
+
hasOnlyReplayablePromptParentDrift(mappedCandidates[0]!.parentIds, parentIds);
|
|
1322
|
+
let matches: typeof mappedCandidates | undefined;
|
|
1323
|
+
if (provisionalMatches.length > 0) {
|
|
1324
|
+
matches = provisionalMatches;
|
|
1325
|
+
} else if (hasPromptDriftMatch) {
|
|
1326
|
+
matches = mappedCandidates;
|
|
1327
|
+
}
|
|
1328
|
+
if (matches === undefined) {
|
|
1329
|
+
return failTopology(displayName, replayKey, "mismatch");
|
|
1330
|
+
}
|
|
1331
|
+
if (matches.length > 1 && (kind !== "prompt" || !allSameParentSet(matches))) {
|
|
1332
|
+
failTopology(displayName, replayKey, "ambiguous");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const selected = matches[0]!;
|
|
1336
|
+
const duplicateKey = `${identity}\u0001${sortedIdentity(selected.source.parentIds)}`;
|
|
1337
|
+
const ambiguousPromptAnswer = kind === "prompt" && (promptDuplicateCounts.get(duplicateKey) ?? 0) > 1;
|
|
1338
|
+
const answerReplay: PromptAnswerReplaySafety = ambiguousPromptAnswer
|
|
1339
|
+
? "ambiguous"
|
|
1340
|
+
: selected.source.status === "completed"
|
|
1341
|
+
? "allowed"
|
|
1342
|
+
: "unavailable";
|
|
1343
|
+
consumedSourceStageIds.add(selected.source.id);
|
|
1344
|
+
continuationStageIdBySourceStageId.set(selected.source.id, stageId);
|
|
1345
|
+
if (selected.source.status === "completed" && answerReplay === "allowed") {
|
|
1346
|
+
return { kind: "replay", source: selected.source, parentIds: selected.parentIds, answerReplay };
|
|
1347
|
+
}
|
|
1348
|
+
return { kind: "execute", source: selected.source, parentIds: selected.parentIds, answerReplay };
|
|
1151
1349
|
},
|
|
1152
1350
|
};
|
|
1153
1351
|
}
|
|
@@ -1211,6 +1409,9 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1211
1409
|
): Promise<RunResult> {
|
|
1212
1410
|
const activeStore = opts.store ?? defaultStore;
|
|
1213
1411
|
const adapters = opts.adapters ?? {};
|
|
1412
|
+
if (opts.usePromptNodesForUi === true && opts.ui !== undefined) {
|
|
1413
|
+
console.warn("pi-workflows: usePromptNodesForUi ignores the provided RunOpts.ui adapter");
|
|
1414
|
+
}
|
|
1214
1415
|
|
|
1215
1416
|
// 0. maxDepth guard — reject before any store/persistence side effects.
|
|
1216
1417
|
const depth = opts.depth ?? 0;
|
|
@@ -1309,6 +1510,12 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1309
1510
|
const stageById = (stageId: string): StageSnapshot | undefined =>
|
|
1310
1511
|
runSnapshot.stages.find((stage) => stage.id === stageId);
|
|
1311
1512
|
|
|
1513
|
+
const setStageParentIds = (stage: StageSnapshot, parentIds: readonly string[]): void => {
|
|
1514
|
+
// Keep tracker and snapshot parent arrays in sync when topology is refreshed;
|
|
1515
|
+
// consumers should not cache the old parentIds reference across updates.
|
|
1516
|
+
stage.parentIds = Object.freeze([...parentIds]);
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1312
1519
|
const hasAncestor = (stage: StageSnapshot, ancestorId: string): boolean => {
|
|
1313
1520
|
const queue = [...stage.parentIds];
|
|
1314
1521
|
const seen = new Set<string>();
|
|
@@ -1432,26 +1639,217 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1432
1639
|
{ once: true },
|
|
1433
1640
|
);
|
|
1434
1641
|
|
|
1642
|
+
const buildPromptNodeUiAdapter = (): WorkflowUIAdapter => {
|
|
1643
|
+
const ask = async (descriptor: PromptDescriptor): Promise<unknown> => {
|
|
1644
|
+
if (ownController.signal.aborted) {
|
|
1645
|
+
return fallbackForPromptDescriptor(descriptor);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
const prompt = makePrompt(descriptor);
|
|
1649
|
+
const stageId = crypto.randomUUID();
|
|
1650
|
+
const provisionalParentIds = tracker.onSpawn(stageId, descriptor.kind);
|
|
1651
|
+
const replayKey = promptReplayKey(descriptor);
|
|
1652
|
+
const replayDecision = replayIndex.decide({
|
|
1653
|
+
displayName: descriptor.kind,
|
|
1654
|
+
replayKey,
|
|
1655
|
+
parentIds: provisionalParentIds,
|
|
1656
|
+
stageId,
|
|
1657
|
+
kind: "prompt",
|
|
1658
|
+
});
|
|
1659
|
+
const parentIds = replayDecision.parentIds;
|
|
1660
|
+
if (!sameStringSet(parentIds, provisionalParentIds)) {
|
|
1661
|
+
tracker.replaceParents(stageId, parentIds);
|
|
1662
|
+
}
|
|
1663
|
+
const replaySource = replayDecision.source;
|
|
1664
|
+
const replayAnswer = replayDecision.kind === "replay"
|
|
1665
|
+
// Replay decisions are only produced when continuation is present.
|
|
1666
|
+
? activeStore.getStagePromptAnswer(opts.continuation!.source.id, replayDecision.source.id)
|
|
1667
|
+
: undefined;
|
|
1668
|
+
const shouldReplay = replayAnswer !== undefined;
|
|
1669
|
+
if (shouldReplay) {
|
|
1670
|
+
replayIndex.markPromptAnswerReplayed(stageId);
|
|
1671
|
+
}
|
|
1672
|
+
const replaySourceId = replaySource?.id;
|
|
1673
|
+
const promptAnswerStatus = getPromptAnswerState(shouldReplay, replaySourceId, replayDecision.answerReplay);
|
|
1674
|
+
const stageSnapshot: StageSnapshot = {
|
|
1675
|
+
id: stageId,
|
|
1676
|
+
name: descriptor.kind,
|
|
1677
|
+
replayKey,
|
|
1678
|
+
status: shouldReplay ? "completed" : "running",
|
|
1679
|
+
parentIds: Object.freeze(parentIds),
|
|
1680
|
+
startedAt: prompt.createdAt,
|
|
1681
|
+
promptFootprint: { ...prompt },
|
|
1682
|
+
toolEvents: [],
|
|
1683
|
+
attachable: !shouldReplay,
|
|
1684
|
+
...(shouldReplay ? {
|
|
1685
|
+
endedAt: prompt.createdAt,
|
|
1686
|
+
durationMs: 0,
|
|
1687
|
+
promptAnswerState: promptAnswerStatus,
|
|
1688
|
+
replayedFromStageId: replaySourceId,
|
|
1689
|
+
replayed: true,
|
|
1690
|
+
} : replaySourceId !== undefined ? {
|
|
1691
|
+
promptAnswerState: promptAnswerStatus,
|
|
1692
|
+
replayedFromStageId: replaySourceId,
|
|
1693
|
+
replayed: false,
|
|
1694
|
+
} : {}),
|
|
1695
|
+
};
|
|
1696
|
+
let finalized = false;
|
|
1697
|
+
const finalizePromptStage = (status: "completed" | "failed" | "skipped"): void => {
|
|
1698
|
+
if (finalized) return;
|
|
1699
|
+
finalized = true;
|
|
1700
|
+
stageSnapshot.status = status;
|
|
1701
|
+
stageSnapshot.endedAt = Date.now();
|
|
1702
|
+
stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
|
|
1703
|
+
activeStore.recordStageAttachable(runId, stageId, false);
|
|
1704
|
+
activeStore.recordStageEnd(runId, stageSnapshot);
|
|
1705
|
+
opts.onStageEnd?.(runId, stageSnapshot);
|
|
1706
|
+
if (opts.persistence) {
|
|
1707
|
+
appendStageEnd(opts.persistence, {
|
|
1708
|
+
runId,
|
|
1709
|
+
stageId,
|
|
1710
|
+
status: stageSnapshot.status,
|
|
1711
|
+
durationMs: stageSnapshot.durationMs,
|
|
1712
|
+
...(stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {}),
|
|
1713
|
+
...(stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {}),
|
|
1714
|
+
...(stageSnapshot.failureMessage !== undefined ? { failureMessage: stageSnapshot.failureMessage } : {}),
|
|
1715
|
+
...(stageSnapshot.skippedReason !== undefined ? { skippedReason: stageSnapshot.skippedReason } : {}),
|
|
1716
|
+
...stageReplayFields(stageSnapshot),
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
tracker.onSettle(stageId);
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
activeStore.recordStageStart(runId, stageSnapshot);
|
|
1723
|
+
opts.onStageStart?.(runId, stageSnapshot);
|
|
1724
|
+
if (opts.persistence) {
|
|
1725
|
+
appendStageStart(opts.persistence, {
|
|
1726
|
+
runId,
|
|
1727
|
+
stageId,
|
|
1728
|
+
name: stageSnapshot.name,
|
|
1729
|
+
parentIds: stageSnapshot.parentIds,
|
|
1730
|
+
...stageReplayFields(stageSnapshot),
|
|
1731
|
+
ts: prompt.createdAt,
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
if (shouldReplay) {
|
|
1735
|
+
await Promise.resolve();
|
|
1736
|
+
finalizePromptStage("completed");
|
|
1737
|
+
return replayAnswer.value;
|
|
1738
|
+
}
|
|
1739
|
+
const accepted = activeStore.recordStagePendingPrompt(runId, stageId, prompt);
|
|
1740
|
+
if (!accepted) {
|
|
1741
|
+
stageSnapshot.skippedReason = "prompt-unavailable";
|
|
1742
|
+
finalizePromptStage("skipped");
|
|
1743
|
+
return fallbackForPromptDescriptor(descriptor);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
const waiter = activeStore.awaitStagePendingPrompt(runId, stageId, prompt.id);
|
|
1747
|
+
try {
|
|
1748
|
+
const response = await new Promise<unknown>((resolve, reject) => {
|
|
1749
|
+
const onAbort = (): void => {
|
|
1750
|
+
activeStore.resolveStagePendingPrompt(
|
|
1751
|
+
runId,
|
|
1752
|
+
stageId,
|
|
1753
|
+
prompt.id,
|
|
1754
|
+
fallbackForPromptDescriptor(descriptor),
|
|
1755
|
+
{ recordAnswer: false },
|
|
1756
|
+
);
|
|
1757
|
+
reject(hilAbortError(ownController.signal));
|
|
1758
|
+
};
|
|
1759
|
+
if (ownController.signal.aborted) {
|
|
1760
|
+
onAbort();
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
ownController.signal.addEventListener("abort", onAbort, { once: true });
|
|
1764
|
+
waiter.then(
|
|
1765
|
+
(value) => {
|
|
1766
|
+
ownController.signal.removeEventListener("abort", onAbort);
|
|
1767
|
+
resolve(value);
|
|
1768
|
+
},
|
|
1769
|
+
(err: unknown) => {
|
|
1770
|
+
ownController.signal.removeEventListener("abort", onAbort);
|
|
1771
|
+
reject(err);
|
|
1772
|
+
},
|
|
1773
|
+
);
|
|
1774
|
+
});
|
|
1775
|
+
finalizePromptStage("completed");
|
|
1776
|
+
return response;
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
if (ownController.signal.aborted) {
|
|
1779
|
+
stageSnapshot.skippedReason = "run-aborted";
|
|
1780
|
+
finalizePromptStage("skipped");
|
|
1781
|
+
} else {
|
|
1782
|
+
applyFailureToStage(stageSnapshot, classifyWorkflowFailure(err));
|
|
1783
|
+
finalizePromptStage("failed");
|
|
1784
|
+
}
|
|
1785
|
+
throw err;
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
return {
|
|
1790
|
+
async input(promptText: string): Promise<string> {
|
|
1791
|
+
const response = await ask({ kind: "input", message: promptText });
|
|
1792
|
+
return typeof response === "string" ? response : String(response ?? "");
|
|
1793
|
+
},
|
|
1794
|
+
async confirm(message: string): Promise<boolean> {
|
|
1795
|
+
const response = await ask({ kind: "confirm", message });
|
|
1796
|
+
return response === true;
|
|
1797
|
+
},
|
|
1798
|
+
async select<T extends string>(message: string, options: readonly T[]): Promise<T> {
|
|
1799
|
+
if (options.length === 0) {
|
|
1800
|
+
throw new Error("pi-workflows: ctx.ui.select requires at least one option");
|
|
1801
|
+
}
|
|
1802
|
+
const response = await ask({ kind: "select", message, choices: options });
|
|
1803
|
+
if (typeof response === "string" && (options as readonly string[]).includes(response)) {
|
|
1804
|
+
return response as T;
|
|
1805
|
+
}
|
|
1806
|
+
return options[0]!;
|
|
1807
|
+
},
|
|
1808
|
+
async editor(initial?: string): Promise<string> {
|
|
1809
|
+
const response = await ask({
|
|
1810
|
+
kind: "editor",
|
|
1811
|
+
message: "Edit and save to continue.",
|
|
1812
|
+
initial,
|
|
1813
|
+
});
|
|
1814
|
+
return typeof response === "string" ? response : initial ?? "";
|
|
1815
|
+
},
|
|
1816
|
+
};
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1435
1819
|
// 5. Build WorkflowRunContext
|
|
1436
1820
|
const ctx: WorkflowRunContext<TInputs> = {
|
|
1437
1821
|
inputs: resolvedInputs as TInputs,
|
|
1438
|
-
|
|
1822
|
+
// Prompt nodes and caller-provided UI adapters are mutually exclusive;
|
|
1823
|
+
// executor-owned prompt nodes intentionally take precedence when enabled.
|
|
1824
|
+
ui: opts.usePromptNodesForUi === true ? buildPromptNodeUiAdapter() : opts.ui ?? makeUnavailableUIContext(),
|
|
1439
1825
|
|
|
1440
1826
|
stage(name: string, options?: StageOptions, stageFailFastScope?: ParallelFailFastScope) {
|
|
1441
1827
|
// a. Generate stageId
|
|
1442
1828
|
const stageId = crypto.randomUUID();
|
|
1443
1829
|
|
|
1444
|
-
// b. tracker.onSpawn → parentIds
|
|
1445
|
-
const
|
|
1830
|
+
// b. tracker.onSpawn → provisional parentIds
|
|
1831
|
+
const provisionalParentIds = tracker.onSpawn(stageId, name);
|
|
1446
1832
|
|
|
1447
1833
|
// c. Create StageSnapshot as "pending"
|
|
1448
|
-
const
|
|
1834
|
+
const replayKey = `stage:${name}`;
|
|
1835
|
+
const replayDecision = replayIndex.decide({
|
|
1836
|
+
displayName: name,
|
|
1837
|
+
replayKey,
|
|
1838
|
+
parentIds: provisionalParentIds,
|
|
1839
|
+
stageId,
|
|
1840
|
+
kind: "stage",
|
|
1841
|
+
});
|
|
1842
|
+
const parentIds = replayDecision.parentIds;
|
|
1843
|
+
if (!sameStringSet(parentIds, provisionalParentIds)) {
|
|
1844
|
+
tracker.replaceParents(stageId, parentIds);
|
|
1845
|
+
}
|
|
1449
1846
|
const replaySource = replayDecision.kind === "replay" ? replayDecision.source : undefined;
|
|
1450
1847
|
const shouldReplay = replaySource !== undefined;
|
|
1451
1848
|
|
|
1452
1849
|
const stageSnapshot: StageSnapshot = {
|
|
1453
1850
|
id: stageId,
|
|
1454
1851
|
name,
|
|
1852
|
+
replayKey,
|
|
1455
1853
|
status: shouldReplay ? "completed" : "pending",
|
|
1456
1854
|
parentIds: Object.freeze(parentIds),
|
|
1457
1855
|
toolEvents: [],
|
|
@@ -1482,8 +1880,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1482
1880
|
stageId,
|
|
1483
1881
|
name,
|
|
1484
1882
|
parentIds: stageSnapshot.parentIds,
|
|
1485
|
-
...(stageSnapshot
|
|
1486
|
-
...(stageSnapshot.replayed !== undefined ? { replayed: stageSnapshot.replayed } : {}),
|
|
1883
|
+
...stageReplayFields(stageSnapshot),
|
|
1487
1884
|
ts: stageSnapshot.startedAt ?? Date.now(),
|
|
1488
1885
|
});
|
|
1489
1886
|
};
|
|
@@ -1505,8 +1902,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1505
1902
|
status: "completed",
|
|
1506
1903
|
durationMs: 0,
|
|
1507
1904
|
...(stageSnapshot.result !== undefined ? { summary: stageSnapshot.result } : {}),
|
|
1508
|
-
|
|
1509
|
-
replayed: true,
|
|
1905
|
+
...stageReplayFields(stageSnapshot),
|
|
1510
1906
|
});
|
|
1511
1907
|
}
|
|
1512
1908
|
tracker.onSettle(stageId);
|
|
@@ -1520,7 +1916,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1520
1916
|
const rejectReplayMutation = (action: string): never => {
|
|
1521
1917
|
throw new Error(`pi-workflows: replayed stage "${name}" cannot ${action}`);
|
|
1522
1918
|
};
|
|
1523
|
-
const replayContext:
|
|
1919
|
+
const replayContext: InternalStageContext = {
|
|
1524
1920
|
name,
|
|
1525
1921
|
prompt: replayText,
|
|
1526
1922
|
complete: replayText,
|
|
@@ -1542,11 +1938,24 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1542
1938
|
compact: async () => rejectReplayMutation("compact"),
|
|
1543
1939
|
abortCompaction: () => rejectReplayMutation("abort compaction"),
|
|
1544
1940
|
abort: async () => rejectReplayMutation("abort"),
|
|
1941
|
+
__dispose: async () => {},
|
|
1942
|
+
__getLastAssistantText: () => replayResult,
|
|
1943
|
+
getLastAssistantText: () => replayResult,
|
|
1944
|
+
__ensureSession: async () => {},
|
|
1945
|
+
__sessionMeta: () => ({
|
|
1946
|
+
sessionId: replaySource.sessionId,
|
|
1947
|
+
sessionFile: replaySource.sessionFile,
|
|
1948
|
+
}),
|
|
1949
|
+
__agentSession: () => undefined,
|
|
1950
|
+
__pendingMessageCount: () => 0,
|
|
1545
1951
|
__modelFallbackMeta: () => ({
|
|
1546
1952
|
...(replaySource.model !== undefined ? { model: replaySource.model } : {}),
|
|
1547
1953
|
...(replaySource.attemptedModels !== undefined ? { attemptedModels: replaySource.attemptedModels } : {}),
|
|
1548
1954
|
...(replaySource.modelAttempts !== undefined ? { modelAttempts: replaySource.modelAttempts } : {}),
|
|
1549
1955
|
}),
|
|
1956
|
+
__requestPause: async () => rejectReplayMutation("pause"),
|
|
1957
|
+
__resume: async () => rejectReplayMutation("resume"),
|
|
1958
|
+
__isPaused: () => false,
|
|
1550
1959
|
};
|
|
1551
1960
|
return replayContext;
|
|
1552
1961
|
}
|
|
@@ -1564,25 +1973,35 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1564
1973
|
models: opts.models,
|
|
1565
1974
|
});
|
|
1566
1975
|
const activeAskUserQuestionCalls = new Set<string>();
|
|
1976
|
+
let activeAskUserQuestionAnonymousCalls = 0;
|
|
1977
|
+
const hasActiveAskUserQuestion = (): boolean =>
|
|
1978
|
+
activeAskUserQuestionCalls.size > 0 || activeAskUserQuestionAnonymousCalls > 0;
|
|
1567
1979
|
const unsubscribeAskUserQuestionWatcher = innerCtx.subscribe((event) => {
|
|
1568
1980
|
const toolEvent = askUserQuestionToolEvent(event);
|
|
1569
1981
|
if (!toolEvent) return;
|
|
1570
1982
|
if (toolEvent.phase === "start") {
|
|
1571
|
-
activeAskUserQuestionCalls.add(toolEvent.callId);
|
|
1983
|
+
if (toolEvent.callId !== undefined) activeAskUserQuestionCalls.add(toolEvent.callId);
|
|
1984
|
+
else activeAskUserQuestionAnonymousCalls += 1;
|
|
1572
1985
|
activeStore.recordStageAwaitingInput(runId, stageId, true);
|
|
1573
1986
|
return;
|
|
1574
1987
|
}
|
|
1575
1988
|
|
|
1576
|
-
if (toolEvent.
|
|
1989
|
+
if (toolEvent.callId !== undefined && activeAskUserQuestionCalls.has(toolEvent.callId)) {
|
|
1577
1990
|
activeAskUserQuestionCalls.delete(toolEvent.callId);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1991
|
+
} else if (toolEvent.callId === undefined && toolEvent.nameMatched) {
|
|
1992
|
+
activeAskUserQuestionAnonymousCalls = Math.max(0, activeAskUserQuestionAnonymousCalls - 1);
|
|
1993
|
+
} else {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
if (!hasActiveAskUserQuestion()) {
|
|
1998
|
+
activeStore.recordStageAwaitingInput(runId, stageId, false);
|
|
1581
1999
|
}
|
|
1582
2000
|
});
|
|
1583
2001
|
const disposeInnerContext = async (): Promise<void> => {
|
|
1584
2002
|
unsubscribeAskUserQuestionWatcher();
|
|
1585
2003
|
activeAskUserQuestionCalls.clear();
|
|
2004
|
+
activeAskUserQuestionAnonymousCalls = 0;
|
|
1586
2005
|
activeStore.recordStageAwaitingInput(runId, stageId, false);
|
|
1587
2006
|
await innerCtx.__dispose();
|
|
1588
2007
|
};
|
|
@@ -1596,46 +2015,13 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1596
2015
|
unregisterStageHandle();
|
|
1597
2016
|
await disposeInnerContext();
|
|
1598
2017
|
};
|
|
1599
|
-
const
|
|
1600
|
-
|
|
1601
|
-
|
|
2018
|
+
const dropStageControlForCompletion = async (): Promise<void> => {
|
|
2019
|
+
// Completion removes the stage from workflow-level pause/resume and
|
|
2020
|
+
// dependency cascades, but must not turn the attached/reopenable chat
|
|
2021
|
+
// into a read-only archive. Keep the direct live handle registered for
|
|
2022
|
+
// post-completion follow-ups until the registry/store is explicitly
|
|
2023
|
+
// cleared by the host.
|
|
1602
2024
|
dropStageControlHandle();
|
|
1603
|
-
if (!hasQueuedLiveWork()) {
|
|
1604
|
-
await releaseLiveHandle();
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
// The queued-work branch installs asynchronous cleanup and returns once
|
|
1609
|
-
// the release watcher is armed. Inner-context events normally trigger
|
|
1610
|
-
// the subscription when streaming/pending-message counters change, but
|
|
1611
|
-
// SDK prompt/tool cleanup can also drain after the stage has stopped
|
|
1612
|
-
// emitting workflow-visible events. The unref'd 250 ms interval is a
|
|
1613
|
-
// fallback for that silent drain path and is cleared as soon as the
|
|
1614
|
-
// handle becomes idle.
|
|
1615
|
-
let unsubscribe = (): void => {};
|
|
1616
|
-
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
1617
|
-
const cleanupWatcher = (): void => {
|
|
1618
|
-
unsubscribe();
|
|
1619
|
-
if (pollTimer !== undefined) {
|
|
1620
|
-
clearInterval(pollTimer);
|
|
1621
|
-
pollTimer = undefined;
|
|
1622
|
-
}
|
|
1623
|
-
};
|
|
1624
|
-
const releaseIfIdle = (): void => {
|
|
1625
|
-
if (liveHandleReleased) {
|
|
1626
|
-
cleanupWatcher();
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
if (hasQueuedLiveWork()) return;
|
|
1630
|
-
cleanupWatcher();
|
|
1631
|
-
void releaseLiveHandle().catch((error: unknown) => {
|
|
1632
|
-
console.debug("pi-workflows: failed to release idle stage handle", error);
|
|
1633
|
-
});
|
|
1634
|
-
};
|
|
1635
|
-
unsubscribe = innerCtx.subscribe(() => queueMicrotask(releaseIfIdle));
|
|
1636
|
-
pollTimer = setInterval(releaseIfIdle, 250);
|
|
1637
|
-
pollTimer.unref?.();
|
|
1638
|
-
releaseIfIdle();
|
|
1639
2025
|
};
|
|
1640
2026
|
|
|
1641
2027
|
// e. Register a live stage-control handle so attached panes can
|
|
@@ -1711,6 +2097,9 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1711
2097
|
subscribe(listener: AgentSessionEventListener) {
|
|
1712
2098
|
return innerCtx.subscribe(listener);
|
|
1713
2099
|
},
|
|
2100
|
+
async dispose() {
|
|
2101
|
+
await releaseLiveHandle();
|
|
2102
|
+
},
|
|
1714
2103
|
};
|
|
1715
2104
|
let stageFinalized = false;
|
|
1716
2105
|
const finalizeStageSnapshot = (): boolean => {
|
|
@@ -1739,8 +2128,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1739
2128
|
...(stageSnapshot.failureMessage !== undefined ? { failureMessage: stageSnapshot.failureMessage } : {}),
|
|
1740
2129
|
...(stageSnapshot.skippedReason !== undefined ? { skippedReason: stageSnapshot.skippedReason } : {}),
|
|
1741
2130
|
...(stageSnapshot.result !== undefined && stageSnapshot.status === "completed" ? { summary: stageSnapshot.result } : {}),
|
|
1742
|
-
...(stageSnapshot
|
|
1743
|
-
...(stageSnapshot.replayed !== undefined ? { replayed: stageSnapshot.replayed } : {}),
|
|
2131
|
+
...stageReplayFields(stageSnapshot),
|
|
1744
2132
|
});
|
|
1745
2133
|
}
|
|
1746
2134
|
|
|
@@ -1761,7 +2149,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1761
2149
|
markSkippedForParallelFailFast();
|
|
1762
2150
|
finalizeStageSnapshot();
|
|
1763
2151
|
void innerCtx.abort().catch(() => {});
|
|
1764
|
-
void
|
|
2152
|
+
void dropStageControlForCompletion().catch(() => {});
|
|
1765
2153
|
};
|
|
1766
2154
|
stageFailFastScope?.activeStages.set(stageId, { skip: skipForParallelFailFast });
|
|
1767
2155
|
|
|
@@ -1815,6 +2203,13 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1815
2203
|
throw err;
|
|
1816
2204
|
}
|
|
1817
2205
|
|
|
2206
|
+
if (opts.continuation === undefined && stageSnapshot.startedAt === undefined) {
|
|
2207
|
+
const actualParentIds = tracker.currentParents();
|
|
2208
|
+
if (!sameStringSet(actualParentIds, stageSnapshot.parentIds)) {
|
|
2209
|
+
tracker.replaceParents(stageId, actualParentIds);
|
|
2210
|
+
setStageParentIds(stageSnapshot, actualParentIds);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
1818
2213
|
stageSnapshot.status = "running";
|
|
1819
2214
|
stageSnapshot.startedAt = Date.now();
|
|
1820
2215
|
activeStore.recordStageStart(runId, stageSnapshot);
|
|
@@ -1880,12 +2275,10 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1880
2275
|
|
|
1881
2276
|
finalizeStageSnapshot();
|
|
1882
2277
|
// The stage has finished participating in workflow scheduling. Drop it
|
|
1883
|
-
// from run-level pause/resume and cascade-pause lookups immediately
|
|
1884
|
-
//
|
|
1885
|
-
//
|
|
1886
|
-
|
|
1887
|
-
// drained.
|
|
1888
|
-
await releaseLiveHandleWhenIdle().catch(() => {});
|
|
2278
|
+
// from run-level pause/resume and cascade-pause lookups immediately,
|
|
2279
|
+
// while retaining the direct chat handle so completed nodes can be
|
|
2280
|
+
// reopened and continued instead of becoming read-only archives.
|
|
2281
|
+
await dropStageControlForCompletion().catch(() => {});
|
|
1889
2282
|
limiter.release();
|
|
1890
2283
|
}
|
|
1891
2284
|
};
|