@bastani/atomic 0.8.19-0 → 0.8.20-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 +19 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +5 -0
- package/dist/builtin/mcp/package.json +2 -2
- package/dist/builtin/subagents/CHANGELOG.md +12 -2
- package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
- package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/debugger.md +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/skills/subagent/SKILL.md +12 -12
- package/dist/builtin/subagents/src/agents/agent-management.ts +16 -11
- package/dist/builtin/subagents/src/agents/skills.ts +13 -1
- package/dist/builtin/subagents/src/extension/index.ts +14 -3
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +8 -0
- package/dist/builtin/subagents/src/runs/background/run-status.ts +2 -3
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +11 -1
- package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +31 -23
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +13 -7
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +160 -93
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
- package/dist/builtin/subagents/src/runs/shared/run-history.ts +1 -1
- package/dist/builtin/subagents/src/shared/settings.ts +1 -0
- package/dist/builtin/subagents/src/shared/types.ts +78 -4
- package/dist/builtin/subagents/src/tui/render.ts +203 -19
- package/dist/builtin/web-access/CHANGELOG.md +5 -0
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +19 -0
- package/dist/builtin/workflows/README.md +22 -3
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +1 -1
- package/dist/builtin/workflows/builtin/open-claude-design.ts +12 -4
- package/dist/builtin/workflows/builtin/ralph.ts +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/config-loader.ts +68 -0
- package/dist/builtin/workflows/src/extension/index.ts +246 -55
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +372 -0
- package/dist/builtin/workflows/src/extension/render-call.ts +1 -1
- package/dist/builtin/workflows/src/extension/wiring.ts +32 -3
- package/dist/builtin/workflows/src/runs/background/status.ts +14 -74
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +5 -3
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +3 -13
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +2 -10
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +5 -5
- package/dist/builtin/workflows/src/tui/session-confirm.ts +6 -7
- package/dist/builtin/workflows/src/tui/session-picker.ts +18 -14
- package/dist/builtin/workflows/src/tui/status-list.ts +2 -2
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +125 -30
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +3 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +3 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +63 -17
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +29 -0
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +46 -13
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts.map +1 -1
- package/dist/utils/pi-user-agent.js +2 -1
- package/dist/utils/pi-user-agent.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +1 -1
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +3 -5
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/models.md +52 -52
- package/docs/quickstart.md +2 -2
- package/docs/workflows.md +22 -5
- package/package.json +9 -9
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
PiMessageRenderComponent,
|
|
4
|
+
PiMessageRendererResult,
|
|
5
|
+
} from "./index.js";
|
|
6
|
+
import type { Store } from "../shared/store.js";
|
|
7
|
+
import type {
|
|
8
|
+
PendingPrompt,
|
|
9
|
+
PromptKind,
|
|
10
|
+
RunSnapshot,
|
|
11
|
+
RunStatus,
|
|
12
|
+
StageSnapshot,
|
|
13
|
+
StageStatus,
|
|
14
|
+
StoreSnapshot,
|
|
15
|
+
} from "../shared/store-types.js";
|
|
16
|
+
import { wrapPlainText } from "../tui/text-helpers.js";
|
|
17
|
+
|
|
18
|
+
export const LIFECYCLE_NOTICE_CUSTOM_TYPE = "workflows:lifecycle-notice";
|
|
19
|
+
export const LIFECYCLE_NOTICE_SNIPPET_LIMIT = 240;
|
|
20
|
+
|
|
21
|
+
export type WorkflowLifecycleNoticeKind = "completed" | "failed" | "awaiting_input";
|
|
22
|
+
|
|
23
|
+
export const WORKFLOW_LIFECYCLE_NOTICE_KINDS = [
|
|
24
|
+
"completed",
|
|
25
|
+
"failed",
|
|
26
|
+
"awaiting_input",
|
|
27
|
+
] as const satisfies readonly WorkflowLifecycleNoticeKind[];
|
|
28
|
+
|
|
29
|
+
export interface WorkflowLifecycleNotificationConfig {
|
|
30
|
+
readonly enabled: boolean;
|
|
31
|
+
readonly notifyOn: readonly WorkflowLifecycleNoticeKind[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WorkflowLifecycleNoticeDetails {
|
|
35
|
+
readonly kind: WorkflowLifecycleNoticeKind;
|
|
36
|
+
readonly scope: "run" | "stage";
|
|
37
|
+
readonly runId: string;
|
|
38
|
+
readonly workflowName: string;
|
|
39
|
+
readonly status: RunStatus | StageStatus;
|
|
40
|
+
readonly stageId?: string;
|
|
41
|
+
readonly stageName?: string;
|
|
42
|
+
readonly promptId?: string;
|
|
43
|
+
readonly promptKind?: PromptKind;
|
|
44
|
+
readonly promptMessage?: string;
|
|
45
|
+
readonly error?: string;
|
|
46
|
+
readonly failedStageId?: string;
|
|
47
|
+
readonly durationMs?: number;
|
|
48
|
+
readonly createdAt: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WorkflowLifecycleNotificationState {
|
|
52
|
+
readonly deliveredTerminalRuns: Set<string>;
|
|
53
|
+
readonly deliveredInputPrompts: Set<string>;
|
|
54
|
+
suppressionDepth: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WorkflowLifecycleNotificationOptions {
|
|
58
|
+
readonly store: Store;
|
|
59
|
+
readonly sendMessage?: ExtensionAPI["sendMessage"];
|
|
60
|
+
readonly registerMessageRenderer?: ExtensionAPI["registerMessageRenderer"];
|
|
61
|
+
readonly rendererHost?: object;
|
|
62
|
+
readonly config: WorkflowLifecycleNotificationConfig;
|
|
63
|
+
readonly state?: WorkflowLifecycleNotificationState;
|
|
64
|
+
readonly seedExisting?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type RawRenderer = (payload: unknown) => PiMessageRendererResult;
|
|
68
|
+
|
|
69
|
+
// Process-lifetime registration dedupe: extension hosts are object identities
|
|
70
|
+
// and may be garbage-collected, but renderer registrations are not unregistered.
|
|
71
|
+
const rendererRegisteredHosts = new WeakSet<object>();
|
|
72
|
+
|
|
73
|
+
export function createWorkflowLifecycleNotificationState(): WorkflowLifecycleNotificationState {
|
|
74
|
+
return {
|
|
75
|
+
deliveredTerminalRuns: new Set<string>(),
|
|
76
|
+
deliveredInputPrompts: new Set<string>(),
|
|
77
|
+
suppressionDepth: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resetWorkflowLifecycleNotificationState(
|
|
82
|
+
state: WorkflowLifecycleNotificationState,
|
|
83
|
+
): void {
|
|
84
|
+
state.deliveredTerminalRuns.clear();
|
|
85
|
+
state.deliveredInputPrompts.clear();
|
|
86
|
+
state.suppressionDepth = 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function seedWorkflowLifecycleNotificationState(
|
|
90
|
+
state: WorkflowLifecycleNotificationState,
|
|
91
|
+
snapshot: StoreSnapshot,
|
|
92
|
+
): void {
|
|
93
|
+
for (const run of snapshot.runs) {
|
|
94
|
+
if ((run.status === "completed" || run.status === "failed") && run.endedAt !== undefined) {
|
|
95
|
+
state.deliveredTerminalRuns.add(terminalRunKey(run.status, run.id));
|
|
96
|
+
}
|
|
97
|
+
if (run.pendingPrompt !== undefined) {
|
|
98
|
+
state.deliveredInputPrompts.add(runAwaitingInputKey(run.id, run.pendingPrompt));
|
|
99
|
+
}
|
|
100
|
+
for (const stage of run.stages) {
|
|
101
|
+
if (stage.status === "awaiting_input") {
|
|
102
|
+
state.deliveredInputPrompts.add(awaitingInputKey(run.id, stage));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Suppress lifecycle notice emission while still observing snapshot changes and
|
|
110
|
+
* marking matching lifecycle states as delivered. This is intended for restore
|
|
111
|
+
* or replay paths where historical workflow states should seed dedupe state
|
|
112
|
+
* without notifying the current chat; it is not a generic temporary mute that
|
|
113
|
+
* should emit the same notices later.
|
|
114
|
+
*/
|
|
115
|
+
export function withWorkflowLifecycleNotificationsSuppressed<T>(
|
|
116
|
+
state: WorkflowLifecycleNotificationState,
|
|
117
|
+
fn: () => T,
|
|
118
|
+
): T {
|
|
119
|
+
state.suppressionDepth += 1;
|
|
120
|
+
try {
|
|
121
|
+
return fn();
|
|
122
|
+
} finally {
|
|
123
|
+
state.suppressionDepth -= 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function installWorkflowLifecycleNotifications(
|
|
128
|
+
options: WorkflowLifecycleNotificationOptions,
|
|
129
|
+
): () => void {
|
|
130
|
+
registerLifecycleNoticeRenderer(options);
|
|
131
|
+
|
|
132
|
+
if (!options.config.enabled) return () => undefined;
|
|
133
|
+
const send = options.sendMessage;
|
|
134
|
+
if (typeof send !== "function") return () => undefined;
|
|
135
|
+
|
|
136
|
+
const notifyOn = new Set<WorkflowLifecycleNoticeKind>(options.config.notifyOn);
|
|
137
|
+
const state = options.state ?? createWorkflowLifecycleNotificationState();
|
|
138
|
+
if (options.seedExisting !== false) {
|
|
139
|
+
seedWorkflowLifecycleNotificationState(state, options.store.snapshot());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const emit = (details: WorkflowLifecycleNoticeDetails): void => {
|
|
143
|
+
const content = formatWorkflowLifecycleNoticeText(details);
|
|
144
|
+
try {
|
|
145
|
+
// Store subscribers are notified in a tight loop. A lifecycle notice
|
|
146
|
+
// failure must never abort sibling subscribers such as status writers.
|
|
147
|
+
void Promise.resolve(
|
|
148
|
+
send(
|
|
149
|
+
{
|
|
150
|
+
customType: LIFECYCLE_NOTICE_CUSTOM_TYPE,
|
|
151
|
+
content,
|
|
152
|
+
display: true,
|
|
153
|
+
details,
|
|
154
|
+
},
|
|
155
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
156
|
+
),
|
|
157
|
+
).catch((error: unknown) => warnLifecycleSendFailure(error));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
warnLifecycleSendFailure(error);
|
|
160
|
+
// Best-effort notification only; keep store delivery isolated.
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const emitTerminalNoticeOnce = (
|
|
165
|
+
run: RunSnapshot,
|
|
166
|
+
kind: "completed" | "failed",
|
|
167
|
+
): void => {
|
|
168
|
+
if (run.status !== kind || run.endedAt === undefined || !notifyOn.has(kind)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const key = terminalRunKey(kind, run.id);
|
|
173
|
+
if (state.deliveredTerminalRuns.has(key)) return;
|
|
174
|
+
|
|
175
|
+
state.deliveredTerminalRuns.add(key);
|
|
176
|
+
if (state.suppressionDepth > 0) return;
|
|
177
|
+
emit(makeTerminalNotice(run, kind));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const emitStageAwaitingInputNoticeOnce = (
|
|
181
|
+
run: RunSnapshot,
|
|
182
|
+
stage: StageSnapshot,
|
|
183
|
+
): void => {
|
|
184
|
+
if (stage.status !== "awaiting_input") return;
|
|
185
|
+
|
|
186
|
+
const key = awaitingInputKey(run.id, stage);
|
|
187
|
+
if (state.deliveredInputPrompts.has(key)) return;
|
|
188
|
+
|
|
189
|
+
state.deliveredInputPrompts.add(key);
|
|
190
|
+
if (state.suppressionDepth > 0) return;
|
|
191
|
+
emit(makeStageAwaitingInputNotice(run, stage));
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const emitRunAwaitingInputNoticeOnce = (run: RunSnapshot): void => {
|
|
195
|
+
if (run.pendingPrompt === undefined) return;
|
|
196
|
+
|
|
197
|
+
const key = runAwaitingInputKey(run.id, run.pendingPrompt);
|
|
198
|
+
if (state.deliveredInputPrompts.has(key)) return;
|
|
199
|
+
|
|
200
|
+
state.deliveredInputPrompts.add(key);
|
|
201
|
+
if (state.suppressionDepth > 0) return;
|
|
202
|
+
emit(makeRunAwaitingInputNotice(run, run.pendingPrompt));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const inspect = (snapshot: StoreSnapshot): void => {
|
|
206
|
+
for (const run of snapshot.runs) {
|
|
207
|
+
emitTerminalNoticeOnce(run, "completed");
|
|
208
|
+
emitTerminalNoticeOnce(run, "failed");
|
|
209
|
+
|
|
210
|
+
if (!notifyOn.has("awaiting_input")) continue;
|
|
211
|
+
emitRunAwaitingInputNoticeOnce(run);
|
|
212
|
+
for (const stage of run.stages) {
|
|
213
|
+
emitStageAwaitingInputNoticeOnce(run, stage);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return options.store.subscribe(inspect);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function registerLifecycleNoticeRenderer(
|
|
222
|
+
options: Pick<WorkflowLifecycleNotificationOptions, "registerMessageRenderer" | "rendererHost">,
|
|
223
|
+
): void {
|
|
224
|
+
const register = options.registerMessageRenderer;
|
|
225
|
+
if (typeof register !== "function") return;
|
|
226
|
+
|
|
227
|
+
const host = options.rendererHost ?? register;
|
|
228
|
+
if (rendererRegisteredHosts.has(host)) return;
|
|
229
|
+
|
|
230
|
+
const renderer: RawRenderer = (raw) => {
|
|
231
|
+
const message = raw as { details?: WorkflowLifecycleNoticeDetails };
|
|
232
|
+
if (!message.details) return undefined;
|
|
233
|
+
return makeNoticeComponent(message.details);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
register(LIFECYCLE_NOTICE_CUSTOM_TYPE, renderer);
|
|
237
|
+
rendererRegisteredHosts.add(host);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function formatWorkflowLifecycleNoticeText(details: WorkflowLifecycleNoticeDetails): string {
|
|
241
|
+
const workflowName = escapeQuotedText(details.workflowName);
|
|
242
|
+
if (details.kind === "completed") {
|
|
243
|
+
return `✅ Workflow "${workflowName}" completed (run ${details.runId}). Inspect: /workflow status ${details.runId}`;
|
|
244
|
+
}
|
|
245
|
+
if (details.kind === "failed") {
|
|
246
|
+
const stage = details.stageName ?? details.failedStageId;
|
|
247
|
+
const stageText = stage ? `, stage ${stage}` : "";
|
|
248
|
+
const errorText = details.error ? `: ${details.error}` : "";
|
|
249
|
+
return `❌ Workflow "${workflowName}" failed (run ${details.runId}${stageText})${errorText}. Inspect: /workflow status ${details.runId}`;
|
|
250
|
+
}
|
|
251
|
+
const prompt = details.promptMessage ? ` Prompt: ${details.promptMessage}` : "";
|
|
252
|
+
if (details.scope === "run") {
|
|
253
|
+
return `❓ Workflow "${workflowName}" needs input (run ${details.runId}).${prompt} Respond: /workflow connect ${details.runId} to answer this run-level prompt.`;
|
|
254
|
+
}
|
|
255
|
+
const stage = details.stageName ?? details.stageId ?? "unknown";
|
|
256
|
+
const responseHint = details.stageId && details.promptId
|
|
257
|
+
? `/workflow connect ${details.runId} or workflow({ action: "send", runId: ${jsonString(details.runId)}, stageId: ${jsonString(details.stageId)}, promptId: ${jsonString(details.promptId)}, response: ... })`
|
|
258
|
+
: `/workflow connect ${details.runId}`;
|
|
259
|
+
return `❓ Workflow "${workflowName}" needs input (run ${details.runId}, stage ${stage}).${prompt} Respond: ${responseHint}.`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function makeTerminalNotice(
|
|
263
|
+
run: RunSnapshot,
|
|
264
|
+
kind: "completed" | "failed",
|
|
265
|
+
): WorkflowLifecycleNoticeDetails {
|
|
266
|
+
const failedStage = run.failedStageId
|
|
267
|
+
? run.stages.find((stage) => stage.id === run.failedStageId)
|
|
268
|
+
: undefined;
|
|
269
|
+
return {
|
|
270
|
+
kind,
|
|
271
|
+
scope: "run",
|
|
272
|
+
runId: run.id,
|
|
273
|
+
workflowName: run.name,
|
|
274
|
+
status: run.status,
|
|
275
|
+
...(run.error ? { error: truncateSnippet(run.error) } : {}),
|
|
276
|
+
...(run.failedStageId ? { failedStageId: run.failedStageId } : {}),
|
|
277
|
+
...(failedStage ? { stageId: failedStage.id, stageName: failedStage.name } : {}),
|
|
278
|
+
...(run.durationMs !== undefined ? { durationMs: run.durationMs } : {}),
|
|
279
|
+
// Normal store paths stamp endedAt; Date.now() is defensive for malformed restored snapshots.
|
|
280
|
+
createdAt: run.endedAt ?? Date.now(),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function makeStageAwaitingInputNotice(run: RunSnapshot, stage: StageSnapshot): WorkflowLifecycleNoticeDetails {
|
|
285
|
+
const prompt = stage.pendingPrompt;
|
|
286
|
+
return {
|
|
287
|
+
kind: "awaiting_input",
|
|
288
|
+
scope: "stage",
|
|
289
|
+
runId: run.id,
|
|
290
|
+
workflowName: run.name,
|
|
291
|
+
status: stage.status,
|
|
292
|
+
stageId: stage.id,
|
|
293
|
+
stageName: stage.name,
|
|
294
|
+
...(prompt ? promptFields(prompt) : {}),
|
|
295
|
+
// Normal store paths stamp awaitingInputSince; Date.now() is defensive for malformed restored snapshots.
|
|
296
|
+
createdAt: prompt?.createdAt ?? stage.awaitingInputSince ?? Date.now(),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function makeRunAwaitingInputNotice(run: RunSnapshot, prompt: PendingPrompt): WorkflowLifecycleNoticeDetails {
|
|
301
|
+
return {
|
|
302
|
+
kind: "awaiting_input",
|
|
303
|
+
scope: "run",
|
|
304
|
+
runId: run.id,
|
|
305
|
+
workflowName: run.name,
|
|
306
|
+
status: run.status,
|
|
307
|
+
...promptFields(prompt),
|
|
308
|
+
createdAt: prompt.createdAt,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function warnLifecycleSendFailure(error: unknown): void {
|
|
313
|
+
if (process.env.ATOMIC_WORKFLOW_DEBUG !== "1") return;
|
|
314
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
315
|
+
console.warn("[workflows] workflow lifecycle notice send failed", message);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function escapeQuotedText(value: string): string {
|
|
319
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function jsonString(value: string): string {
|
|
323
|
+
return JSON.stringify(value);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function terminalRunKey(kind: "completed" | "failed", runId: string): string {
|
|
327
|
+
return `${kind}:${runId}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function promptFields(
|
|
331
|
+
prompt: PendingPrompt,
|
|
332
|
+
): Pick<WorkflowLifecycleNoticeDetails, "promptId" | "promptKind" | "promptMessage"> {
|
|
333
|
+
return {
|
|
334
|
+
promptId: prompt.id,
|
|
335
|
+
promptKind: prompt.kind,
|
|
336
|
+
promptMessage: truncateSnippet(prompt.message),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function awaitingInputKey(runId: string, stage: StageSnapshot): string {
|
|
341
|
+
const promptId = stage.pendingPrompt?.id;
|
|
342
|
+
if (promptId) return `awaiting_input:${runId}:stage:${stage.id}:${promptId}`;
|
|
343
|
+
return `awaiting_input:${runId}:stage:${stage.id}:${stage.awaitingInputSince ?? "active"}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function runAwaitingInputKey(runId: string, prompt: PendingPrompt): string {
|
|
347
|
+
return `awaiting_input:${runId}:run:${prompt.id}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function truncateSnippet(value: string): string {
|
|
351
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
352
|
+
if (normalized.length <= LIFECYCLE_NOTICE_SNIPPET_LIMIT) return normalized;
|
|
353
|
+
return `${normalized.slice(0, LIFECYCLE_NOTICE_SNIPPET_LIMIT - 1)}…`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function makeNoticeComponent(details: WorkflowLifecycleNoticeDetails): PiMessageRenderComponent {
|
|
357
|
+
const text = formatWorkflowLifecycleNoticeText(details);
|
|
358
|
+
return {
|
|
359
|
+
render(width: number): string[] {
|
|
360
|
+
// Wrap to the render width so a long run id / workflow name never emits a
|
|
361
|
+
// line wider than the terminal. pi-tui hard-throws ("Rendered line N
|
|
362
|
+
// exceeds terminal width") on any over-wide rendered line, which would
|
|
363
|
+
// crash the whole TUI on narrow terminals or after a resize (#1109).
|
|
364
|
+
// `wrapPlainText` hard-breaks long unbreakable tokens (e.g. UUIDs), so
|
|
365
|
+
// every returned line is guaranteed to fit within `width`.
|
|
366
|
+
return wrapPlainText(text, width);
|
|
367
|
+
},
|
|
368
|
+
invalidate() {
|
|
369
|
+
/* stored lifecycle notices are immutable */
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
@@ -67,7 +67,7 @@ export function renderCall(args: WorkflowToolArgs, opts: RenderCallOpts = {}): s
|
|
|
67
67
|
line = "workflow: list registered workflows";
|
|
68
68
|
break;
|
|
69
69
|
case "status":
|
|
70
|
-
line = "workflow: list
|
|
70
|
+
line = "workflow: list retained runs";
|
|
71
71
|
break;
|
|
72
72
|
case "inputs":
|
|
73
73
|
line = name === undefined
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { basename } from "node:path";
|
|
25
|
-
import type { CreateAgentSessionOptions } from "@bastani/atomic";
|
|
26
|
-
import type { ChatMessageRenderOptions } from "@bastani/atomic";
|
|
25
|
+
import type { ChatMessageRenderOptions, CreateAgentSessionOptions } from "@bastani/atomic";
|
|
27
26
|
import type { StageAdapters, StageSessionRuntime } from "../runs/foreground/stage-runner.js";
|
|
28
27
|
import type { StageExecutionMeta, StageOptions } from "../shared/types.js";
|
|
29
28
|
import { stageUiBroker, type StageUiBroker } from "../shared/stage-ui-broker.js";
|
|
@@ -237,6 +236,33 @@ function stripWorkflowOnlyOptions(options: (StageOptions | CreateAgentSessionOpt
|
|
|
237
236
|
return sessionOptions as CreateAgentSessionOptions;
|
|
238
237
|
}
|
|
239
238
|
|
|
239
|
+
function makeWorkflowStageOrchestrationContext(meta: StageExecutionMeta): NonNullable<CreateAgentSessionOptions["orchestrationContext"]> {
|
|
240
|
+
return {
|
|
241
|
+
kind: "workflow-stage",
|
|
242
|
+
workflowRunId: meta.runId,
|
|
243
|
+
workflowStageId: meta.stageId,
|
|
244
|
+
workflowStageName: meta.stageName,
|
|
245
|
+
constraints: {
|
|
246
|
+
disableWorkflowTool: true,
|
|
247
|
+
maxSubagentDepth: 1,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function withWorkflowStageSessionOptions(
|
|
253
|
+
options: CreateAgentSessionOptions,
|
|
254
|
+
meta: StageExecutionMeta | undefined,
|
|
255
|
+
): CreateAgentSessionOptions {
|
|
256
|
+
// Workflow stage sessions should never see the workflow tool, even when older
|
|
257
|
+
// meta-less callers cannot receive the richer runtime orchestration context.
|
|
258
|
+
const excludedTools = Array.from(new Set([...(options.excludedTools ?? []), "workflow"]));
|
|
259
|
+
return {
|
|
260
|
+
...options,
|
|
261
|
+
excludedTools,
|
|
262
|
+
...(meta ? { orchestrationContext: makeWorkflowStageOrchestrationContext(meta) } : {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
240
266
|
function makeStageExtensionUiContext(
|
|
241
267
|
ui: PiUISurface,
|
|
242
268
|
meta: StageExecutionMeta | undefined,
|
|
@@ -325,7 +351,10 @@ export function buildRuntimeAdapters(
|
|
|
325
351
|
// extensions, tools, prompts, and skills as the parent chat. Callers
|
|
326
352
|
// can still opt into a custom resource set by passing `resourceLoader`
|
|
327
353
|
// through `stage(name, options)`.
|
|
328
|
-
const sessionOptions
|
|
354
|
+
const sessionOptions = withWorkflowStageSessionOptions(
|
|
355
|
+
stripWorkflowOnlyOptions(stageOptions) ?? {},
|
|
356
|
+
meta,
|
|
357
|
+
);
|
|
329
358
|
const result = await createSession(sessionOptions);
|
|
330
359
|
const bindable = result.session as BindableStageSession;
|
|
331
360
|
if ((pi.ui !== undefined || meta !== undefined) && typeof bindable.bindExtensions === "function") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Status / kill / resume
|
|
2
|
+
* Status / kill / resume helpers for retained workflow runs and live controls.
|
|
3
3
|
*
|
|
4
4
|
* These helpers operate against the singleton store and are consumed by:
|
|
5
5
|
* - The `workflow` tool execute handler (action: "status" | "kill" | "resume")
|
|
@@ -34,10 +34,6 @@ export type KillResult =
|
|
|
34
34
|
| { ok: true; runId: string; previousStatus: RunStatus }
|
|
35
35
|
| { ok: false; runId: string; reason: "not_found" | "already_ended" };
|
|
36
36
|
|
|
37
|
-
export type DestroyRunResult =
|
|
38
|
-
| { ok: true; runId: string; previousStatus: RunStatus; wasInFlight: boolean }
|
|
39
|
-
| { ok: false; runId: string; reason: "not_found" };
|
|
40
|
-
|
|
41
37
|
export type ResumeResult =
|
|
42
38
|
| {
|
|
43
39
|
ok: true;
|
|
@@ -95,27 +91,22 @@ export type InspectRunResult =
|
|
|
95
91
|
// ---------------------------------------------------------------------------
|
|
96
92
|
|
|
97
93
|
/**
|
|
98
|
-
* Returns a summary of all
|
|
99
|
-
*
|
|
94
|
+
* Returns a summary of all retained runs in the current store/session.
|
|
95
|
+
*
|
|
96
|
+
* Terminal snapshots are retained for inspection and are visible by default;
|
|
97
|
+
* the legacy `all` option is accepted as a compatibility no-op.
|
|
100
98
|
*/
|
|
101
99
|
export function statusRuns(opts?: { all?: boolean; store?: Store }): RunStatusEntry[] {
|
|
102
100
|
const activeStore = opts?.store ?? defaultStore;
|
|
103
|
-
const runs = activeStore.runs();
|
|
104
|
-
const result: RunStatusEntry[] = [];
|
|
105
|
-
|
|
106
|
-
for (const run of runs) {
|
|
107
|
-
if (!opts?.all && run.endedAt !== undefined) continue;
|
|
108
|
-
result.push({
|
|
109
|
-
runId: run.id,
|
|
110
|
-
name: run.name,
|
|
111
|
-
status: run.status,
|
|
112
|
-
startedAt: run.startedAt,
|
|
113
|
-
durationMs: run.durationMs,
|
|
114
|
-
stageCount: run.stages.length,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
101
|
|
|
118
|
-
return
|
|
102
|
+
return activeStore.runs().map((run) => ({
|
|
103
|
+
runId: run.id,
|
|
104
|
+
name: run.name,
|
|
105
|
+
status: run.status,
|
|
106
|
+
startedAt: run.startedAt,
|
|
107
|
+
durationMs: run.durationMs,
|
|
108
|
+
stageCount: run.stages.length,
|
|
109
|
+
}));
|
|
119
110
|
}
|
|
120
111
|
|
|
121
112
|
// ---------------------------------------------------------------------------
|
|
@@ -179,57 +170,6 @@ export function killAllRuns(opts?: {
|
|
|
179
170
|
);
|
|
180
171
|
}
|
|
181
172
|
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
// destroyRun
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Destructively kills a workflow run and removes it from live history/status.
|
|
188
|
-
*
|
|
189
|
-
* In-flight runs are aborted and persisted with a terminal `killed` entry so
|
|
190
|
-
* session restore will not resurrect them. Ended runs are simply removed from
|
|
191
|
-
* the live store without appending a duplicate terminal event.
|
|
192
|
-
*/
|
|
193
|
-
export function destroyRun(
|
|
194
|
-
runId: string,
|
|
195
|
-
opts?: { store?: Store; cancellation?: CancellationRegistry; persistence?: WorkflowPersistencePort },
|
|
196
|
-
): DestroyRunResult {
|
|
197
|
-
const activeStore = opts?.store ?? defaultStore;
|
|
198
|
-
const run = activeStore.runs().find((r) => r.id === runId);
|
|
199
|
-
|
|
200
|
-
if (!run) {
|
|
201
|
-
return { ok: false, runId, reason: "not_found" };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const previousStatus = run.status;
|
|
205
|
-
const wasInFlight = run.endedAt === undefined;
|
|
206
|
-
|
|
207
|
-
if (wasInFlight) {
|
|
208
|
-
opts?.cancellation?.abort(runId, "workflow killed");
|
|
209
|
-
if (opts?.persistence) {
|
|
210
|
-
appendRunEnd(opts.persistence, { runId, status: "killed", ts: Date.now() });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
activeStore.removeRun(runId);
|
|
215
|
-
return { ok: true, runId, previousStatus, wasInFlight };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Destructively kills and removes all in-flight runs.
|
|
220
|
-
*/
|
|
221
|
-
export function destroyAllRuns(opts?: {
|
|
222
|
-
store?: Store;
|
|
223
|
-
cancellation?: CancellationRegistry;
|
|
224
|
-
persistence?: WorkflowPersistencePort;
|
|
225
|
-
}): DestroyRunResult[] {
|
|
226
|
-
const activeStore = opts?.store ?? defaultStore;
|
|
227
|
-
const inFlight = activeStore.runs().filter((r) => r.endedAt === undefined);
|
|
228
|
-
return inFlight.map((r) =>
|
|
229
|
-
destroyRun(r.id, { store: activeStore, cancellation: opts?.cancellation, persistence: opts?.persistence }),
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
173
|
// ---------------------------------------------------------------------------
|
|
234
174
|
// resumeRun
|
|
235
175
|
// ---------------------------------------------------------------------------
|
|
@@ -408,7 +348,7 @@ export function pauseAllRuns(opts?: {
|
|
|
408
348
|
|
|
409
349
|
/**
|
|
410
350
|
* Interrupt a run in a resumable way by pausing live stage handles when
|
|
411
|
-
* available.
|
|
351
|
+
* available. This never aborts the workflow controller and
|
|
412
352
|
* never removes the run from status/history.
|
|
413
353
|
*/
|
|
414
354
|
export function interruptRun(
|
|
@@ -163,7 +163,7 @@ export function restoreOnSessionStart(
|
|
|
163
163
|
|
|
164
164
|
const entries = getEntries.call(sessionManager);
|
|
165
165
|
const sessionEntries = entries as readonly SessionEntry[];
|
|
166
|
-
|
|
166
|
+
restoreTerminalRuns(sessionEntries, store);
|
|
167
167
|
const inFlight = scanInFlightRuns(sessionEntries);
|
|
168
168
|
if (inFlight.length === 0) return;
|
|
169
169
|
|
|
@@ -300,7 +300,7 @@ function restoreStageStatus(status: unknown): StageStatus {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
function
|
|
303
|
+
function restoreTerminalRuns(entries: readonly SessionEntry[], store: Store): void {
|
|
304
304
|
const started = new Map<string, { readonly name: string; readonly inputs: Readonly<Record<string, unknown>>; readonly startTs: number }>();
|
|
305
305
|
const ended = new Map<string, Record<string, unknown>>();
|
|
306
306
|
|
|
@@ -334,6 +334,7 @@ function restoreEndedFailedRuns(entries: readonly SessionEntry[], store: Store):
|
|
|
334
334
|
|
|
335
335
|
const runMeta = findRunStartMetadata(entries, runId);
|
|
336
336
|
const stages = _buildStageSnapshots(entries, runId);
|
|
337
|
+
if (status === "completed" && stages.some((stage) => stage.status !== "completed")) continue;
|
|
337
338
|
store.recordRunStart({
|
|
338
339
|
id: runId,
|
|
339
340
|
name: start.name,
|
|
@@ -365,8 +366,9 @@ function restoreEndedFailedRuns(entries: readonly SessionEntry[], store: Store):
|
|
|
365
366
|
}
|
|
366
367
|
}
|
|
367
368
|
|
|
368
|
-
function restoreTerminalRunStatus(status: unknown): "failed" | "killed" | undefined {
|
|
369
|
+
function restoreTerminalRunStatus(status: unknown): "completed" | "failed" | "killed" | undefined {
|
|
369
370
|
switch (status) {
|
|
371
|
+
case "completed":
|
|
370
372
|
case "failed":
|
|
371
373
|
case "killed":
|
|
372
374
|
return status;
|
|
@@ -79,12 +79,11 @@ export interface DetailPayload {
|
|
|
79
79
|
detail: RunDetail;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** Inline notice after a workflow run is
|
|
82
|
+
/** Inline notice after a workflow run is killed and retained for inspection. */
|
|
83
83
|
export interface KilledPayload {
|
|
84
84
|
kind: "killed";
|
|
85
85
|
run: RunSnapshot;
|
|
86
86
|
previousStatus: RunStatus;
|
|
87
|
-
wasInFlight: boolean;
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
export type ChatSurfacePayload =
|
|
@@ -136,16 +135,8 @@ export function registerChatSurfaceRenderer(
|
|
|
136
135
|
return makeComponent(payload, theme);
|
|
137
136
|
};
|
|
138
137
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
// docs/extensions.md §Custom UI). Cast through `unknown` so the call
|
|
142
|
-
// typechecks against both shapes. `.call(pi, …)` preserves `this` for
|
|
143
|
-
// pi's class-backed ExtensionAPI.
|
|
144
|
-
(register as unknown as (event: string, r: RawRenderer) => void).call(
|
|
145
|
-
pi,
|
|
146
|
-
CHAT_SURFACE_CUSTOM_TYPE,
|
|
147
|
-
renderer,
|
|
148
|
-
);
|
|
138
|
+
// `.call(pi, …)` preserves `this` for pi's class-backed ExtensionAPI.
|
|
139
|
+
register.call(pi, CHAT_SURFACE_CUSTOM_TYPE, renderer);
|
|
149
140
|
rendererRegisteredHosts.add(pi);
|
|
150
141
|
}
|
|
151
142
|
|
|
@@ -234,7 +225,6 @@ function renderPayload(
|
|
|
234
225
|
theme,
|
|
235
226
|
run: payload.run,
|
|
236
227
|
previousStatus: payload.previousStatus,
|
|
237
|
-
wasInFlight: payload.wasInFlight,
|
|
238
228
|
}).join("\n");
|
|
239
229
|
}
|
|
240
230
|
}
|
|
@@ -124,16 +124,8 @@ export function registerInlineFormRenderer(pi: ExtensionAPI, theme: GraphTheme):
|
|
|
124
124
|
};
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
// (see docs/extensions.md §Custom UI). Cast through `unknown` so the call
|
|
130
|
-
// typechecks against both shapes. Call through `pi` so pi's
|
|
131
|
-
// class-backed ExtensionAPI keeps its `this` binding.
|
|
132
|
-
(register as unknown as (event: string, r: RawRenderer) => void).call(
|
|
133
|
-
pi,
|
|
134
|
-
CUSTOM_TYPE,
|
|
135
|
-
renderer,
|
|
136
|
-
);
|
|
127
|
+
// Call through `pi` so pi's class-backed ExtensionAPI keeps its `this` binding.
|
|
128
|
+
register.call(pi, CUSTOM_TYPE, renderer);
|
|
137
129
|
rendererRegisteredHosts.add(pi);
|
|
138
130
|
}
|
|
139
131
|
|
|
@@ -19,7 +19,7 @@ import type { Store } from "../shared/store.js";
|
|
|
19
19
|
import type { ChatMessageRenderOptions, ReadonlyFooterDataProvider } from "@bastani/atomic";
|
|
20
20
|
import { WorkflowAttachPane } from "./workflow-attach-pane.js";
|
|
21
21
|
import { deriveGraphThemeFromPiTheme } from "./graph-theme.js";
|
|
22
|
-
import {
|
|
22
|
+
import { killRun as defaultKillRun } from "../runs/background/status.js";
|
|
23
23
|
import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
|
|
24
24
|
import { stageControlRegistry as defaultStageControlRegistry } from "../runs/foreground/stage-control-registry.js";
|
|
25
25
|
import type { StageControlRegistry } from "../runs/foreground/stage-control-registry.js";
|
|
@@ -109,9 +109,9 @@ export interface BuildGraphOverlayAdapterOpts {
|
|
|
109
109
|
/** Broker used to route stage-local custom UI into attached stage chats. */
|
|
110
110
|
stageUiBroker?: StageUiBroker;
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
112
|
+
* Kill hook used by graph-mode `q`. The extension factory supplies this so
|
|
113
|
+
* persistence can record a terminal event while retaining the run for
|
|
114
|
+
* inspection.
|
|
115
115
|
*/
|
|
116
116
|
onKillRun?: (runId: string) => void;
|
|
117
117
|
}
|
|
@@ -124,7 +124,7 @@ export function buildGraphOverlayAdapter(
|
|
|
124
124
|
const registry = buildOpts.stageControlRegistry ?? defaultStageControlRegistry;
|
|
125
125
|
const stageUiBroker = buildOpts.stageUiBroker;
|
|
126
126
|
const killRun = buildOpts.onKillRun ?? ((id: string): void => {
|
|
127
|
-
|
|
127
|
+
defaultKillRun(id, { store, cancellation: cancellationRegistry });
|
|
128
128
|
});
|
|
129
129
|
let currentView: WorkflowAttachPane | null = null;
|
|
130
130
|
// pi-tui returns an OverlayHandle via `options.onHandle`. We hold onto
|