@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
|
@@ -84,6 +84,8 @@ export interface StageControlHandle {
|
|
|
84
84
|
* before the session exists are buffered and bound on first attach.
|
|
85
85
|
*/
|
|
86
86
|
subscribe(listener: AgentSessionEventListener): () => void;
|
|
87
|
+
/** Release the underlying SDK session and unregister this direct chat handle. */
|
|
88
|
+
dispose?(): void | Promise<void>;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
/**
|
|
@@ -131,8 +133,9 @@ export interface StageControlRegistry {
|
|
|
131
133
|
/** Build a run-level control aggregate. Cheap; not memoised. */
|
|
132
134
|
run(runId: string): WorkflowRunControlHandle;
|
|
133
135
|
/**
|
|
134
|
-
* Drop every registration
|
|
135
|
-
*
|
|
136
|
+
* Drop every registration and invoke each handle's optional dispose hook.
|
|
137
|
+
* Used on session boundaries to release retained direct chat handles and
|
|
138
|
+
* their subscriptions when the host store is cleared.
|
|
136
139
|
*/
|
|
137
140
|
clear(): void;
|
|
138
141
|
}
|
|
@@ -267,7 +270,15 @@ export function createStageControlRegistry(): StageControlRegistry {
|
|
|
267
270
|
return makeRunHandle(runId);
|
|
268
271
|
},
|
|
269
272
|
clear(): void {
|
|
273
|
+
const handles = [..._byRun.values()].flatMap((runMap) =>
|
|
274
|
+
[...runMap.values()].map((entry) => entry.handle),
|
|
275
|
+
);
|
|
270
276
|
_byRun.clear();
|
|
277
|
+
for (const handle of handles) {
|
|
278
|
+
void Promise.resolve(handle.dispose?.()).catch((err: unknown) => {
|
|
279
|
+
console.warn("pi-workflows: stage handle dispose failed", err);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
271
282
|
},
|
|
272
283
|
};
|
|
273
284
|
}
|
|
@@ -157,7 +157,14 @@ function stripWorkflowOnlyOptions(options: StageOptions | undefined): CreateAgen
|
|
|
157
157
|
return sessionOptions as CreateAgentSessionOptions;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
type AgentSessionConsumer = "prompt" | "complete";
|
|
161
|
+
|
|
162
|
+
function missingAdapter(consumer: AgentSessionConsumer): never {
|
|
163
|
+
if (consumer === "complete") {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"pi-workflows: ctx.complete requires either RunOpts.adapters.complete or RunOpts.adapters.agentSession",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
161
168
|
throw new Error(
|
|
162
169
|
"pi-workflows: prompt adapter not configured — provide an AgentSessionAdapter via RunOpts.adapters.agentSession",
|
|
163
170
|
);
|
|
@@ -457,27 +464,30 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
457
464
|
return created;
|
|
458
465
|
}
|
|
459
466
|
|
|
460
|
-
async function createSession(
|
|
467
|
+
async function createSession(
|
|
468
|
+
candidate: WorkflowResolvedModelCandidate | undefined,
|
|
469
|
+
consumer: AgentSessionConsumer,
|
|
470
|
+
): Promise<StageSessionRuntime> {
|
|
461
471
|
const created = adapters.agentSession
|
|
462
472
|
? await adapters.agentSession.create(stripWorkflowOnlyOptions(stageOptionsForCandidate(candidate)) as StageSessionCreateOptions, {
|
|
463
473
|
...meta,
|
|
464
474
|
stageOptions: stageOptionsForCandidate(candidate),
|
|
465
475
|
})
|
|
466
|
-
: missingAdapter();
|
|
476
|
+
: missingAdapter(consumer);
|
|
467
477
|
return attachSession(created);
|
|
468
478
|
}
|
|
469
479
|
|
|
470
|
-
async function ensureSession(): Promise<StageSessionRuntime> {
|
|
480
|
+
async function ensureSession(consumer: AgentSessionConsumer = "prompt"): Promise<StageSessionRuntime> {
|
|
471
481
|
if (disposed) throw new Error(`pi-workflows: stage "${stageName}" session has been disposed`);
|
|
472
482
|
if (!sessionPromise) {
|
|
473
483
|
sessionPromise = (async () => {
|
|
474
|
-
if (!hasExplicitModelFallbackConfig) return createSession(undefined);
|
|
484
|
+
if (!hasExplicitModelFallbackConfig) return createSession(undefined, consumer);
|
|
475
485
|
const candidates = await modelCandidates();
|
|
476
486
|
const first = candidates[0];
|
|
477
|
-
if (first === undefined) return createSession(undefined);
|
|
487
|
+
if (first === undefined) return createSession(undefined, consumer);
|
|
478
488
|
activeCandidateIndex = 0;
|
|
479
489
|
selectedModel = first.id;
|
|
480
|
-
return createSession(first);
|
|
490
|
+
return createSession(first, consumer);
|
|
481
491
|
})();
|
|
482
492
|
}
|
|
483
493
|
return sessionPromise;
|
|
@@ -531,15 +541,19 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
531
541
|
}
|
|
532
542
|
}
|
|
533
543
|
|
|
534
|
-
async function promptWithFallback(
|
|
544
|
+
async function promptWithFallback(
|
|
545
|
+
text: string,
|
|
546
|
+
sdkOptions: PromptOptions | undefined,
|
|
547
|
+
consumer: AgentSessionConsumer = "prompt",
|
|
548
|
+
): Promise<void> {
|
|
535
549
|
if (!hasExplicitModelFallbackConfig) {
|
|
536
|
-
await promptWithPauseResume(await ensureSession(), text, sdkOptions);
|
|
550
|
+
await promptWithPauseResume(await ensureSession(consumer), text, sdkOptions);
|
|
537
551
|
return;
|
|
538
552
|
}
|
|
539
553
|
|
|
540
554
|
const candidates = await modelCandidates();
|
|
541
555
|
if (candidates.length === 0) {
|
|
542
|
-
await promptWithPauseResume(await ensureSession(), text, sdkOptions);
|
|
556
|
+
await promptWithPauseResume(await ensureSession(consumer), text, sdkOptions);
|
|
543
557
|
return;
|
|
544
558
|
}
|
|
545
559
|
|
|
@@ -548,7 +562,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
548
562
|
const candidate = candidates[index]!;
|
|
549
563
|
const activeSession = session && activeCandidateIndex === index
|
|
550
564
|
? session
|
|
551
|
-
: await createSession(candidate);
|
|
565
|
+
: await createSession(candidate, consumer);
|
|
552
566
|
activeCandidateIndex = index;
|
|
553
567
|
selectedModel = candidate.id;
|
|
554
568
|
try {
|
|
@@ -594,13 +608,25 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
594
608
|
},
|
|
595
609
|
|
|
596
610
|
async complete(text, completeOpts) {
|
|
597
|
-
if (
|
|
611
|
+
if (adapters.complete) {
|
|
612
|
+
lastAssistantText = await adapters.complete.complete(text, completeOpts, meta);
|
|
613
|
+
adapterMessages = assistantMessage(lastAssistantText);
|
|
614
|
+
return lastAssistantText;
|
|
615
|
+
}
|
|
616
|
+
if (
|
|
617
|
+
completeOpts?.model !== undefined ||
|
|
618
|
+
completeOpts?.maxTokens !== undefined ||
|
|
619
|
+
completeOpts?.fallbackModels !== undefined
|
|
620
|
+
) {
|
|
598
621
|
throw new Error(
|
|
599
|
-
"pi-workflows: complete
|
|
622
|
+
"pi-workflows: complete options require a CompleteAdapter via RunOpts.adapters.complete",
|
|
600
623
|
);
|
|
601
624
|
}
|
|
602
|
-
|
|
603
|
-
|
|
625
|
+
// Intentional fallback: when a CompleteAdapter is not configured,
|
|
626
|
+
// `ctx.complete()` can still use the stage AgentSession for simple text
|
|
627
|
+
// completions. Completion-specific options require the dedicated adapter.
|
|
628
|
+
await promptWithFallback(text, undefined, "complete");
|
|
629
|
+
lastAssistantText = lastAssistantTextFromSession(session, lastAssistantText) ?? "";
|
|
604
630
|
return lastAssistantText;
|
|
605
631
|
},
|
|
606
632
|
|
|
@@ -36,6 +36,37 @@ export class GraphFrontierTracker {
|
|
|
36
36
|
return parents;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Snapshot the current frontier without registering or mutating a stage.
|
|
41
|
+
*
|
|
42
|
+
* Use this when an already-spawned stage needs its parents refreshed before
|
|
43
|
+
* it starts; `onSpawn` must only be called for the initial `ctx.stage()`
|
|
44
|
+
* invocation that creates the graph node.
|
|
45
|
+
*/
|
|
46
|
+
currentParents(): string[] {
|
|
47
|
+
return Array.from(this.frontier);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Replace the recorded parents for a stage before it settles.
|
|
52
|
+
*
|
|
53
|
+
* Continuation replay uses source-run topology as authoritative: a replayed
|
|
54
|
+
* stage may spawn with provisional parents inferred from the continuation's
|
|
55
|
+
* current frontier, then install the translated source parents before the
|
|
56
|
+
* stage is recorded or settled.
|
|
57
|
+
*/
|
|
58
|
+
replaceParents(stageId: string, parentIds: readonly string[]): void {
|
|
59
|
+
const parents = Array.from(parentIds);
|
|
60
|
+
this.stageParents.set(stageId, parents);
|
|
61
|
+
const node = this.nodes.get(stageId);
|
|
62
|
+
if (node !== undefined) {
|
|
63
|
+
this.nodes.set(stageId, {
|
|
64
|
+
...node,
|
|
65
|
+
parentIds: Object.freeze(parents),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
/**
|
|
40
71
|
* Call when the stage's Promise settles.
|
|
41
72
|
* Removes the stage's parents from the frontier and adds stageId to frontier.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt replay keys include a normalized workflow-author callsite so two
|
|
3
|
+
* `ctx.ui.*` prompts with identical descriptors can still replay the matching
|
|
4
|
+
* answer after continuation. Runtime/framework frames are filtered out; the
|
|
5
|
+
* selected frame is hashed by the executor and never persisted in raw form.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { relative } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
export interface PromptCallsiteFrame {
|
|
12
|
+
readonly normalizedPath: string;
|
|
13
|
+
readonly lineNumber: string;
|
|
14
|
+
readonly columnNumber: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PACKAGED_WORKFLOW_RUNTIME_ROOTS = [
|
|
18
|
+
"/dist/builtin/workflows/src/",
|
|
19
|
+
"/node_modules/@bastani/workflows/src/",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
function normalizeSlashes(path: string): string {
|
|
23
|
+
return path.replace(/\\/g, "/");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeStackPath(framePath: string): string {
|
|
27
|
+
let normalized = framePath;
|
|
28
|
+
if (normalized.startsWith("file://")) {
|
|
29
|
+
try {
|
|
30
|
+
normalized = fileURLToPath(normalized);
|
|
31
|
+
} catch {
|
|
32
|
+
try {
|
|
33
|
+
normalized = decodeURIComponent(new URL(normalized).pathname);
|
|
34
|
+
} catch {
|
|
35
|
+
// Keep the original string; it will only be hashed, not persisted raw.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
normalized = normalizeSlashes(normalized).replace(/^\/([A-Za-z]:\/)/, "$1");
|
|
41
|
+
|
|
42
|
+
const cwd = normalizeSlashes(process.cwd());
|
|
43
|
+
if (normalized.startsWith(`${cwd}/`)) {
|
|
44
|
+
normalized = normalizeSlashes(relative(process.cwd(), normalized));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function currentModuleRuntimeRoot(): string | undefined {
|
|
51
|
+
const modulePath = normalizeStackPath(fileURLToPath(import.meta.url));
|
|
52
|
+
const markers = ["packages/workflows/src/", ...PACKAGED_WORKFLOW_RUNTIME_ROOTS.map((root) => root.slice(1))];
|
|
53
|
+
for (const marker of markers) {
|
|
54
|
+
const index = modulePath.indexOf(marker);
|
|
55
|
+
if (index >= 0) return modulePath.slice(0, index + marker.length);
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CURRENT_WORKFLOW_RUNTIME_ROOT = currentModuleRuntimeRoot();
|
|
61
|
+
|
|
62
|
+
export function isWorkflowRuntimeFrame(normalizedPath: string): boolean {
|
|
63
|
+
const path = normalizeSlashes(normalizedPath);
|
|
64
|
+
if (CURRENT_WORKFLOW_RUNTIME_ROOT !== undefined && path.startsWith(CURRENT_WORKFLOW_RUNTIME_ROOT)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const comparable = path.startsWith("/") ? path : `/${path}`;
|
|
68
|
+
return PACKAGED_WORKFLOW_RUNTIME_ROOTS.some((runtimeRoot) => comparable.includes(runtimeRoot));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parsePromptStackFrame(stackLine: string): PromptCallsiteFrame | undefined {
|
|
72
|
+
const line = stackLine.trim();
|
|
73
|
+
if (line.length === 0) return undefined;
|
|
74
|
+
if (line.includes("node:internal") || line.includes("bun:") || line.includes("(native:")) return undefined;
|
|
75
|
+
|
|
76
|
+
const match = line.match(/(\S+):(\d+):(\d+)\)?$/);
|
|
77
|
+
if (!match) return undefined;
|
|
78
|
+
const [, rawFramePath, lineNumber, columnNumber] = match;
|
|
79
|
+
if (rawFramePath === undefined || lineNumber === undefined || columnNumber === undefined) return undefined;
|
|
80
|
+
|
|
81
|
+
const normalizedPath = normalizeStackPath(rawFramePath.replace(/^\(/, ""));
|
|
82
|
+
return { normalizedPath, lineNumber, columnNumber };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizedPromptCallsiteFrame(stackLine: string): string | undefined {
|
|
86
|
+
const frame = parsePromptStackFrame(stackLine);
|
|
87
|
+
if (frame === undefined) return undefined;
|
|
88
|
+
if (isWorkflowRuntimeFrame(frame.normalizedPath)) return undefined;
|
|
89
|
+
return `${frame.normalizedPath}:${frame.lineNumber}:${frame.columnNumber}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function selectPromptCallsiteFrame(stack: string): string | undefined {
|
|
93
|
+
return stack
|
|
94
|
+
.split("\n")
|
|
95
|
+
.slice(1)
|
|
96
|
+
.map(normalizedPromptCallsiteFrame)
|
|
97
|
+
.find((candidate): candidate is string => candidate !== undefined);
|
|
98
|
+
}
|
|
@@ -278,10 +278,12 @@ function _buildStageSnapshots(
|
|
|
278
278
|
return [...stageMap.values()];
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
function replayMetadata(payload: Record<string, unknown>): Pick<StageSnapshot, "replayedFromStageId" | "replayed"> {
|
|
281
|
+
function replayMetadata(payload: Record<string, unknown>): Pick<StageSnapshot, "replayKey" | "replayedFromStageId" | "replayed"> {
|
|
282
|
+
const replayKey = payload["replayKey"];
|
|
282
283
|
const replayedFromStageId = payload["replayedFromStageId"];
|
|
283
284
|
const replayed = payload["replayed"];
|
|
284
285
|
return {
|
|
286
|
+
...(typeof replayKey === "string" ? { replayKey } : {}),
|
|
285
287
|
...(typeof replayedFromStageId === "string" ? { replayedFromStageId } : {}),
|
|
286
288
|
...(typeof replayed === "boolean" ? { replayed } : {}),
|
|
287
289
|
};
|
|
@@ -42,6 +42,7 @@ export interface StageStartPayload {
|
|
|
42
42
|
readonly name: string;
|
|
43
43
|
readonly parentIds: readonly string[];
|
|
44
44
|
readonly model?: string;
|
|
45
|
+
readonly replayKey?: string;
|
|
45
46
|
readonly replayedFromStageId?: string;
|
|
46
47
|
readonly replayed?: boolean;
|
|
47
48
|
readonly ts: number;
|
|
@@ -64,6 +65,7 @@ export interface StageEndPayload {
|
|
|
64
65
|
readonly failureKind?: string;
|
|
65
66
|
readonly failureMessage?: string;
|
|
66
67
|
readonly skippedReason?: string;
|
|
68
|
+
readonly replayKey?: string;
|
|
67
69
|
readonly replayedFromStageId?: string;
|
|
68
70
|
readonly replayed?: boolean;
|
|
69
71
|
}
|
|
@@ -113,6 +115,7 @@ export function appendStageStart(api: PersistenceAPI, payload: StageStartPayload
|
|
|
113
115
|
name: payload.name,
|
|
114
116
|
parentIds: [...payload.parentIds],
|
|
115
117
|
...(payload.model !== undefined ? { model: payload.model } : {}),
|
|
118
|
+
...(payload.replayKey !== undefined ? { replayKey: payload.replayKey } : {}),
|
|
116
119
|
...(payload.replayedFromStageId !== undefined ? { replayedFromStageId: payload.replayedFromStageId } : {}),
|
|
117
120
|
...(payload.replayed !== undefined ? { replayed: payload.replayed } : {}),
|
|
118
121
|
ts: payload.ts,
|
|
@@ -147,6 +150,7 @@ export function appendStageEnd(
|
|
|
147
150
|
...(payload.failureKind !== undefined ? { failureKind: payload.failureKind } : {}),
|
|
148
151
|
...(payload.failureMessage !== undefined ? { failureMessage: payload.failureMessage } : {}),
|
|
149
152
|
...(payload.skippedReason !== undefined ? { skippedReason: payload.skippedReason } : {}),
|
|
153
|
+
...(payload.replayKey !== undefined ? { replayKey: payload.replayKey } : {}),
|
|
150
154
|
...(payload.replayedFromStageId !== undefined ? { replayedFromStageId: payload.replayedFromStageId } : {}),
|
|
151
155
|
...(payload.replayed !== undefined ? { replayed: payload.replayed } : {}),
|
|
152
156
|
});
|
|
@@ -63,7 +63,12 @@ export interface StageSnapshot {
|
|
|
63
63
|
readonly id: string;
|
|
64
64
|
readonly name: string;
|
|
65
65
|
status: StageStatus;
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Parent stage ids. Treat as immutable from consumer code; the executor may
|
|
68
|
+
* replace the frozen array before a stage starts when late topology inference
|
|
69
|
+
* refreshes parents, so do not cache this reference across store updates.
|
|
70
|
+
*/
|
|
71
|
+
parentIds: readonly string[];
|
|
67
72
|
startedAt?: number;
|
|
68
73
|
endedAt?: number;
|
|
69
74
|
durationMs?: number;
|
|
@@ -75,6 +80,12 @@ export interface StageSnapshot {
|
|
|
75
80
|
failureMessage?: string;
|
|
76
81
|
/** Reason for stages skipped by fail-fast/cascade handling. */
|
|
77
82
|
skippedReason?: string;
|
|
83
|
+
/** Stable continuation replay identity, separate from display name. */
|
|
84
|
+
replayKey?: string;
|
|
85
|
+
/** Snapshot-safe prompt answer availability marker; never contains the raw answer. */
|
|
86
|
+
promptAnswerState?: "available" | "unavailable" | "ambiguous";
|
|
87
|
+
/** Snapshot-safe descriptor of the prompt UI shown by this stage; never contains the raw answer. */
|
|
88
|
+
promptFootprint?: PendingPrompt;
|
|
78
89
|
/** Source stage id when this stage was replayed during failed-run continuation. */
|
|
79
90
|
replayedFromStageId?: string;
|
|
80
91
|
/** True when provider work was skipped by continuation replay. */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
7
|
PendingPrompt,
|
|
8
|
+
PromptKind,
|
|
8
9
|
RunSnapshot,
|
|
9
10
|
StageSnapshot,
|
|
10
11
|
StageNotice,
|
|
@@ -43,6 +44,23 @@ export interface RunEndMetadata {
|
|
|
43
44
|
readonly resumable?: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export interface PromptAnswerRecord {
|
|
48
|
+
readonly runId: string;
|
|
49
|
+
readonly stageId: string;
|
|
50
|
+
readonly promptId: string;
|
|
51
|
+
readonly kind: PromptKind;
|
|
52
|
+
readonly value: unknown;
|
|
53
|
+
readonly answeredAt: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResolveStagePendingPromptOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Whether to retain the response in the live-only prompt answer ledger for
|
|
59
|
+
* continuation replay. Abort/default resolutions should set this to false.
|
|
60
|
+
*/
|
|
61
|
+
readonly recordAnswer?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
export interface Store {
|
|
47
65
|
runs(): readonly RunSnapshot[];
|
|
48
66
|
notices(): readonly WorkflowNotice[];
|
|
@@ -118,9 +136,20 @@ export interface Store {
|
|
|
118
136
|
stageId: string,
|
|
119
137
|
promptId: string,
|
|
120
138
|
response: unknown,
|
|
139
|
+
options?: ResolveStagePendingPromptOptions,
|
|
121
140
|
): boolean;
|
|
122
141
|
/** Wait for a stage/node-scoped HIL prompt to resolve. */
|
|
123
142
|
awaitStagePendingPrompt(runId: string, stageId: string, promptId: string): Promise<unknown>;
|
|
143
|
+
/**
|
|
144
|
+
* Return the live-only prompt answer record for a completed prompt stage, if
|
|
145
|
+
* still available. The returned value may contain secrets and must never be
|
|
146
|
+
* logged, serialized, or copied into snapshots/persistence. Answers remain
|
|
147
|
+
* resident in memory until explicitly cleared, the run is removed, or the
|
|
148
|
+
* store is cleared.
|
|
149
|
+
*/
|
|
150
|
+
getStagePromptAnswer(runId: string, stageId: string): PromptAnswerRecord | undefined;
|
|
151
|
+
/** Clear the live-only prompt answer record for a stage. Primarily used by tests/cleanup. */
|
|
152
|
+
clearStagePromptAnswer(runId: string, stageId: string): void;
|
|
124
153
|
/**
|
|
125
154
|
* Record Pi/pi SDK session metadata for a stage after lazy
|
|
126
155
|
* attach. The serializable snapshot tracks this so post-mortem reopen
|
|
@@ -188,6 +217,7 @@ export function createStore(): Store {
|
|
|
188
217
|
const _runs: RunSnapshot[] = [];
|
|
189
218
|
const _notices: WorkflowNotice[] = [];
|
|
190
219
|
const _listeners: Set<(snap: StoreSnapshot) => void> = new Set();
|
|
220
|
+
const _stagePromptAnswers = new Map<string, PromptAnswerRecord>();
|
|
191
221
|
let _version = 0;
|
|
192
222
|
|
|
193
223
|
/**
|
|
@@ -230,6 +260,10 @@ export function createStore(): Store {
|
|
|
230
260
|
entry.reject(new Error(reason));
|
|
231
261
|
}
|
|
232
262
|
|
|
263
|
+
function stagePromptAnswerKey(runId: string, stageId: string): string {
|
|
264
|
+
return JSON.stringify([runId, stageId]);
|
|
265
|
+
}
|
|
266
|
+
|
|
233
267
|
function rejectStagePrompt(stage: StageSnapshot, reason: string): void {
|
|
234
268
|
const prompt = stage.pendingPrompt;
|
|
235
269
|
if (!prompt) return;
|
|
@@ -334,8 +368,10 @@ export function createStore(): Store {
|
|
|
334
368
|
existing.failureKind = stage.failureKind;
|
|
335
369
|
existing.failureMessage = stage.failureMessage;
|
|
336
370
|
existing.skippedReason = stage.skippedReason;
|
|
337
|
-
existing.
|
|
338
|
-
existing.
|
|
371
|
+
if (stage.replayKey !== undefined) existing.replayKey = stage.replayKey;
|
|
372
|
+
if (stage.promptAnswerState !== undefined) existing.promptAnswerState = stage.promptAnswerState;
|
|
373
|
+
if (stage.replayedFromStageId !== undefined) existing.replayedFromStageId = stage.replayedFromStageId;
|
|
374
|
+
if (stage.replayed !== undefined) existing.replayed = stage.replayed;
|
|
339
375
|
delete existing.awaitingInputSince;
|
|
340
376
|
rejectStagePrompt(existing, `pi-workflows: stage ${stage.id} ended before prompt resolved`);
|
|
341
377
|
_version++;
|
|
@@ -399,6 +435,9 @@ export function createStore(): Store {
|
|
|
399
435
|
rejectPrompt(pending.id, `pi-workflows: run ${runId} was removed before prompt resolved`);
|
|
400
436
|
}
|
|
401
437
|
rejectAllStagePrompts(run, `pi-workflows: run ${runId} was removed before prompt resolved`);
|
|
438
|
+
for (const stage of run.stages) {
|
|
439
|
+
_stagePromptAnswers.delete(stagePromptAnswerKey(runId, stage.id));
|
|
440
|
+
}
|
|
402
441
|
_runs.splice(index, 1);
|
|
403
442
|
for (let i = _notices.length - 1; i >= 0; i--) {
|
|
404
443
|
if (_notices[i]?.runId === runId) _notices.splice(i, 1);
|
|
@@ -485,6 +524,7 @@ export function createStore(): Store {
|
|
|
485
524
|
if (isTerminalStageStatus(stage.status)) return false;
|
|
486
525
|
if (stage.pendingPrompt !== undefined) return false;
|
|
487
526
|
stage.pendingPrompt = { ...prompt };
|
|
527
|
+
stage.promptFootprint = { ...prompt };
|
|
488
528
|
stage.status = "awaiting_input";
|
|
489
529
|
stage.awaitingInputSince = prompt.createdAt;
|
|
490
530
|
_version++;
|
|
@@ -497,6 +537,7 @@ export function createStore(): Store {
|
|
|
497
537
|
stageId: string,
|
|
498
538
|
promptId: string,
|
|
499
539
|
response: unknown,
|
|
540
|
+
options: ResolveStagePendingPromptOptions = {},
|
|
500
541
|
): boolean {
|
|
501
542
|
const run = findRun(runId);
|
|
502
543
|
if (!run) return false;
|
|
@@ -504,6 +545,20 @@ export function createStore(): Store {
|
|
|
504
545
|
if (!stage) return false;
|
|
505
546
|
const pending = stage.pendingPrompt;
|
|
506
547
|
if (!pending || pending.id !== promptId) return false;
|
|
548
|
+
if (options.recordAnswer !== false) {
|
|
549
|
+
_stagePromptAnswers.set(stagePromptAnswerKey(runId, stageId), {
|
|
550
|
+
runId,
|
|
551
|
+
stageId,
|
|
552
|
+
promptId,
|
|
553
|
+
kind: pending.kind,
|
|
554
|
+
value: response,
|
|
555
|
+
answeredAt: Date.now(),
|
|
556
|
+
});
|
|
557
|
+
stage.promptAnswerState = "available";
|
|
558
|
+
} else {
|
|
559
|
+
_stagePromptAnswers.delete(stagePromptAnswerKey(runId, stageId));
|
|
560
|
+
delete stage.promptAnswerState;
|
|
561
|
+
}
|
|
507
562
|
stage.pendingPrompt = undefined;
|
|
508
563
|
if (stage.status === "awaiting_input") {
|
|
509
564
|
stage.status = "running";
|
|
@@ -544,6 +599,24 @@ export function createStore(): Store {
|
|
|
544
599
|
});
|
|
545
600
|
},
|
|
546
601
|
|
|
602
|
+
getStagePromptAnswer(runId: string, stageId: string): PromptAnswerRecord | undefined {
|
|
603
|
+
return _stagePromptAnswers.get(stagePromptAnswerKey(runId, stageId));
|
|
604
|
+
},
|
|
605
|
+
|
|
606
|
+
clearStagePromptAnswer(runId: string, stageId: string): void {
|
|
607
|
+
const removed = _stagePromptAnswers.delete(stagePromptAnswerKey(runId, stageId));
|
|
608
|
+
const run = findRun(runId);
|
|
609
|
+
const stage = run ? findStage(run, stageId) : undefined;
|
|
610
|
+
const clearAvailabilityMarker = stage?.promptAnswerState === "available";
|
|
611
|
+
if (clearAvailabilityMarker) {
|
|
612
|
+
delete stage.promptAnswerState;
|
|
613
|
+
}
|
|
614
|
+
if (removed || clearAvailabilityMarker) {
|
|
615
|
+
_version++;
|
|
616
|
+
notify();
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
|
|
547
620
|
recordStageSession(
|
|
548
621
|
runId: string,
|
|
549
622
|
stageId: string,
|
|
@@ -742,7 +815,7 @@ export function createStore(): Store {
|
|
|
742
815
|
},
|
|
743
816
|
|
|
744
817
|
clear(): void {
|
|
745
|
-
if (_runs.length === 0 && _notices.length === 0 && _resolvers.size === 0) return;
|
|
818
|
+
if (_runs.length === 0 && _notices.length === 0 && _resolvers.size === 0 && _stagePromptAnswers.size === 0) return;
|
|
746
819
|
_runs.length = 0;
|
|
747
820
|
_notices.length = 0;
|
|
748
821
|
// Reject any outstanding HIL waiters so background promises terminate
|
|
@@ -752,6 +825,7 @@ export function createStore(): Store {
|
|
|
752
825
|
entry.reject(new Error("pi-workflows: store cleared"));
|
|
753
826
|
}
|
|
754
827
|
_resolvers.clear();
|
|
828
|
+
_stagePromptAnswers.clear();
|
|
755
829
|
_version++;
|
|
756
830
|
notify();
|
|
757
831
|
},
|
|
@@ -250,7 +250,8 @@ export class GraphView implements Component {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
const previousFocusedStageId = this.cachedLayout[this.focusedIndex]?.stage.id;
|
|
253
|
-
const
|
|
253
|
+
const graphStages = this._graphStages(run);
|
|
254
|
+
const nextLayout = computeLayout(graphStages, { orientation: "vertical" });
|
|
254
255
|
this.cachedLayout = nextLayout;
|
|
255
256
|
|
|
256
257
|
let focusNeedsReveal = this.pendingEnsureFocusedVisible;
|
|
@@ -289,6 +290,21 @@ export class GraphView implements Component {
|
|
|
289
290
|
this._syncPromptState(run.pendingPrompt);
|
|
290
291
|
}
|
|
291
292
|
|
|
293
|
+
private _graphStages(run: RunSnapshot): StageSnapshot[] {
|
|
294
|
+
const hasStagePrompt = run.stages.some((stage) => stage.pendingPrompt !== undefined);
|
|
295
|
+
if (!hasStagePrompt) return [...run.stages];
|
|
296
|
+
return run.stages.filter((stage) => {
|
|
297
|
+
// Prompt-node injection can leave unstarted author stages in the store
|
|
298
|
+
// while the prompt node owns focus; hide only these inert placeholders.
|
|
299
|
+
const isUnstartedPlaceholder =
|
|
300
|
+
stage.status === "pending" &&
|
|
301
|
+
stage.startedAt === undefined &&
|
|
302
|
+
stage.pendingPrompt === undefined &&
|
|
303
|
+
stage.toolEvents.length === 0;
|
|
304
|
+
return !isUnstartedPlaceholder;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
292
308
|
/**
|
|
293
309
|
* Mirror the run's `pendingPrompt` into a UI working state. A new prompt
|
|
294
310
|
* id resets the state (caret + buffer); a cleared prompt drops the state
|