@jameslovespancakes/pi-plus 1.0.20 → 1.0.21

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.
Files changed (34) hide show
  1. package/README.md +21 -3
  2. package/package.json +1 -1
  3. package/src/domains/workflows/index.ts +56 -104
  4. package/src/domains/workflows/runtime/advisory-challenge.ts +3 -3
  5. package/src/domains/workflows/runtime/agent-attempt.ts +3 -3
  6. package/src/domains/workflows/runtime/agent-options.ts +18 -0
  7. package/src/domains/workflows/runtime/agent-runner-types.ts +7 -3
  8. package/src/domains/workflows/runtime/agent-runner.ts +14 -6
  9. package/src/domains/workflows/runtime/agent-session.ts +34 -3
  10. package/src/domains/workflows/runtime/cancellation.ts +5 -0
  11. package/src/domains/workflows/runtime/engine.ts +19 -40
  12. package/src/domains/workflows/runtime/journal.ts +4 -4
  13. package/src/domains/workflows/runtime/live-agent.ts +37 -0
  14. package/src/domains/workflows/runtime/model-profiles.ts +2 -6
  15. package/src/domains/workflows/runtime/progress-types.ts +3 -1
  16. package/src/domains/workflows/runtime/progress.ts +54 -14
  17. package/src/domains/workflows/runtime/review/review-fix-workflow.ts +3 -3
  18. package/src/domains/workflows/runtime/types.ts +16 -15
  19. package/src/domains/workflows/runtime/ui/agent-transcript.ts +59 -0
  20. package/src/domains/workflows/runtime/ui/workflow-format.ts +6 -2
  21. package/src/domains/workflows/runtime/ui/workflow-inspector.ts +75 -61
  22. package/src/domains/workflows/runtime/ui/workflow-widget.ts +26 -66
  23. package/src/domains/workflows/runtime/workflow-advisory-utils.ts +5 -5
  24. package/src/domains/workflows/runtime/{background-workflows.ts → workflow-lifecycle.ts} +91 -75
  25. package/src/domains/workflows/runtime/workflow-management.ts +66 -0
  26. package/src/domains/workflows/runtime/workflow-run-controller.ts +16 -20
  27. package/src/domains/workflows/runtime/{workflow-run-background.ts → workflow-run-delivery.ts} +3 -3
  28. package/src/domains/workflows/runtime/workflow-run-record.ts +7 -4
  29. package/src/domains/workflows/workflows/code-review.ts +1 -1
  30. package/src/domains/workflows/workflows/diagnose.ts +1 -1
  31. package/src/domains/workflows/workflows/perf-review.ts +1 -1
  32. package/src/domains/workflows/workflows/refactor-scout.ts +1 -1
  33. package/src/domains/workflows/workflows/research.ts +4 -4
  34. package/src/domains/workflows/runtime/background-workflow-tool.ts +0 -75
@@ -1,49 +1,62 @@
1
1
  import { SessionManager, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { WorkflowAbortError, WorkflowPauseError } from "./cancellation.ts";
3
- import type { WorkflowBackgroundOrigin } from "./types.ts";
3
+ import type { WorkflowOrigin } from "./types.ts";
4
+ import { createWorkflowRunId } from "./journal.ts";
5
+ import type { ResolvedWorkflowRunOptions } from "./options.ts";
4
6
  import {
5
7
  transitionWorkflowRun,
6
8
  type WorkflowRunRecord,
7
9
  type WorkflowRunState,
8
10
  } from "./workflow-run-record.ts";
9
- import { updateWorkflowRunDelivery } from "./workflow-run-background.ts";
11
+ import { updateWorkflowRunDelivery } from "./workflow-run-delivery.ts";
10
12
  import { ProjectWorkflowRunStore, type WorkflowRunStore } from "./workflow-run-store.ts";
11
13
  import { unknownErrorMessage } from "./unknown-error.ts";
12
14
  import { emptyWorkflowUsageTotals } from "./usage.ts";
13
15
 
14
- const BACKGROUND_DELIVERY_CUSTOM_TYPE = "workflow-result";
15
- const BACKGROUND_WIDGET_KEY = "workflow-background";
16
+ const WORKFLOW_DELIVERY_CUSTOM_TYPE = "workflow-result";
16
17
  const SHUTDOWN_WAIT_MS = 5_000;
17
18
  const SUMMARY_LIMIT = 500;
18
19
 
19
- export interface BackgroundWorkflowStartInput {
20
+ interface WorkflowStartInput {
20
21
  readonly ctx: ExtensionContext;
21
22
  readonly runId: string;
22
23
  readonly name: string;
23
24
  readonly run: (signal: AbortSignal, onStarted: () => void) => Promise<void>;
24
25
  }
25
26
 
26
- export interface BackgroundWorkflowResultDetails {
27
+ export interface WorkflowCompletionDetails {
27
28
  readonly name: string;
28
29
  readonly result: { readonly summary: string };
29
30
  readonly completedAt: number;
30
31
  readonly usage: WorkflowRunRecord["usage"];
31
32
  readonly runId: string;
32
33
  readonly resumedFromRunId?: string;
33
- readonly background: true;
34
34
  readonly status: WorkflowRunState;
35
35
  }
36
36
 
37
+ export interface WorkflowLaunchResult {
38
+ readonly content: Array<{ readonly type: "text"; readonly text: string }>;
39
+ readonly details: Readonly<Record<string, unknown>>;
40
+ }
41
+
42
+ export function workflowUnavailableResult(mode: ExtensionContext["mode"]): WorkflowLaunchResult | undefined {
43
+ if (mode !== "print" && mode !== "json") return undefined;
44
+ return {
45
+ content: [{ type: "text", text: `Workflows require TUI/RPC mode; ${mode} mode exits after the prompt.` }],
46
+ details: { error: "workflow_unavailable", mode },
47
+ };
48
+ }
49
+
37
50
  type SessionAvailability = "available" | "missing" | "unknown";
38
51
 
39
- interface BackgroundWorkflowDependencies {
52
+ interface WorkflowLifecycleDependencies {
40
53
  readonly storeForCwd?: (cwd: string) => WorkflowRunStore;
41
54
  readonly sessionAvailability?: (cwd: string, sessionId: string) => Promise<SessionAvailability>;
42
55
  readonly log?: (message: string) => void;
43
56
  readonly shutdownWaitMs?: number;
44
57
  }
45
58
 
46
- interface ActiveBackgroundRun {
59
+ interface ActiveWorkflowRun {
47
60
  readonly controller: AbortController;
48
61
  readonly name: string;
49
62
  readonly sessionId: string;
@@ -51,12 +64,12 @@ interface ActiveBackgroundRun {
51
64
  readonly isSettled: () => boolean;
52
65
  }
53
66
 
54
- type BackgroundWorkflowSettledListener = (ctx: ExtensionContext, runId: string) => void | Promise<void>;
67
+ type WorkflowSettledListener = (ctx: ExtensionContext, runId: string) => void | Promise<void>;
55
68
 
56
- /** Owns background runs for one extension instance and routes their durable completion delivery. */
57
- export class BackgroundWorkflowCoordinator {
58
- private readonly active = new Map<string, ActiveBackgroundRun>();
59
- private readonly settledListeners = new Set<BackgroundWorkflowSettledListener>();
69
+ /** The single workflow lifecycle: launch, cancellation, recovery and durable delivery. */
70
+ export class WorkflowLifecycle {
71
+ private readonly active = new Map<string, ActiveWorkflowRun>();
72
+ private readonly settledListeners = new Set<WorkflowSettledListener>();
60
73
  private readonly pendingDelivery = new Map<string, Set<string>>();
61
74
  private readonly shuttingDown = new Set<string>();
62
75
  private readonly storeForCwd: (cwd: string) => WorkflowRunStore;
@@ -66,7 +79,7 @@ export class BackgroundWorkflowCoordinator {
66
79
 
67
80
  constructor(
68
81
  private readonly pi: Pick<ExtensionAPI, "sendMessage">,
69
- dependencies: BackgroundWorkflowDependencies = {},
82
+ dependencies: WorkflowLifecycleDependencies = {},
70
83
  ) {
71
84
  this.storeForCwd = dependencies.storeForCwd ?? ((cwd) => new ProjectWorkflowRunStore(cwd));
72
85
  this.sessionAvailability = dependencies.sessionAvailability ?? defaultSessionAvailability;
@@ -74,8 +87,42 @@ export class BackgroundWorkflowCoordinator {
74
87
  this.shutdownWaitMs = dependencies.shutdownWaitMs ?? SHUTDOWN_WAIT_MS;
75
88
  }
76
89
 
77
- async start(input: BackgroundWorkflowStartInput): Promise<void> {
78
- if (this.active.has(input.runId)) throw new Error(`Background workflow ${input.runId} is already active.`);
90
+ async launch(input: {
91
+ ctx: ExtensionContext;
92
+ name: string;
93
+ options: ResolvedWorkflowRunOptions;
94
+ execute: (ctx: ExtensionContext, options: ResolvedWorkflowRunOptions) => Promise<void>;
95
+ }): Promise<WorkflowLaunchResult> {
96
+ const unavailable = workflowUnavailableResult(input.ctx.mode);
97
+ if (unavailable) return unavailable;
98
+ const runId = createWorkflowRunId();
99
+ const options: ResolvedWorkflowRunOptions = {
100
+ ...input.options, resultViewer: "skip", runId,
101
+ origin: workflowOrigin(input.ctx),
102
+ };
103
+ try {
104
+ await this.start({
105
+ ctx: input.ctx, runId, name: input.name,
106
+ run: async (signal, onStarted) => input.execute({ ...input.ctx, signal }, {
107
+ ...options, signal,
108
+ onRunMetadata(metadata) {
109
+ onStarted();
110
+ return options.onRunMetadata?.(metadata);
111
+ },
112
+ }),
113
+ });
114
+ } catch (error) {
115
+ const message = unknownErrorMessage(error);
116
+ return { content: [{ type: "text", text: `Workflow did not start: ${message}` }], details: { error: "workflow_start_failed", message, runId } };
117
+ }
118
+ return {
119
+ content: [{ type: "text", text: `Workflow "${input.name}" started.\nRun ID: ${runId}` }],
120
+ details: { state: "running", name: input.name, runId },
121
+ };
122
+ }
123
+
124
+ private async start(input: WorkflowStartInput): Promise<void> {
125
+ if (this.active.has(input.runId)) throw new Error(`Workflow ${input.runId} is already active.`);
79
126
 
80
127
  const sessionId = input.ctx.sessionManager.getSessionId();
81
128
  const controller = new AbortController();
@@ -94,7 +141,7 @@ export class BackgroundWorkflowCoordinator {
94
141
  if (deliveryScheduled || !accepted || !settled || this.shuttingDown.has(sessionId)) return;
95
142
  deliveryScheduled = true;
96
143
  void this.queueOrDeliver(input.ctx, input.runId).catch((error: unknown) => {
97
- this.log(`[workflow:${input.runId}] background delivery failed: ${unknownErrorMessage(error)}`);
144
+ this.log(`[workflow:${input.runId}] delivery failed: ${unknownErrorMessage(error)}`);
98
145
  });
99
146
  };
100
147
 
@@ -105,13 +152,12 @@ export class BackgroundWorkflowCoordinator {
105
152
  startedSignalled = true;
106
153
  resolveStarted?.();
107
154
  });
108
- if (!startedSignalled) rejectStarted?.(new Error("Background workflow ended before publishing run metadata."));
155
+ if (!startedSignalled) rejectStarted?.(new Error("Workflow ended before publishing run metadata."));
109
156
  } catch (error) {
110
157
  if (!startedSignalled) rejectStarted?.(error);
111
158
  } finally {
112
159
  settled = true;
113
160
  this.active.delete(input.runId);
114
- this.updateBackgroundSurface(input.ctx);
115
161
  await this.notifyRunSettled(input.ctx, input.runId);
116
162
  scheduleDelivery();
117
163
  }
@@ -126,13 +172,12 @@ export class BackgroundWorkflowCoordinator {
126
172
 
127
173
  await started;
128
174
  const record = await this.storeForCwd(input.ctx.cwd).load(input.runId);
129
- if (record?.state !== "running" || !record.background || record.background.origin.sessionId !== sessionId) {
130
- controller.abort(new WorkflowAbortError("Background workflow could not persist its origin metadata."));
131
- throw new Error(`Background workflow ${input.runId} did not create a durable run record.`);
175
+ if (!record?.background || record.background.origin.sessionId !== sessionId) {
176
+ controller.abort(new WorkflowAbortError("Workflow could not persist its origin metadata."));
177
+ throw new Error(`Workflow ${input.runId} did not create a durable run record.`);
132
178
  }
133
179
 
134
180
  accepted = true;
135
- this.updateBackgroundSurface(input.ctx);
136
181
  scheduleDelivery();
137
182
  }
138
183
 
@@ -145,7 +190,7 @@ export class BackgroundWorkflowCoordinator {
145
190
  );
146
191
  }
147
192
 
148
- onRunSettled(listener: BackgroundWorkflowSettledListener): () => void {
193
+ onRunSettled(listener: WorkflowSettledListener): () => void {
149
194
  this.settledListeners.add(listener);
150
195
  return () => this.settledListeners.delete(listener);
151
196
  }
@@ -160,7 +205,7 @@ export class BackgroundWorkflowCoordinator {
160
205
  const store = this.storeForCwd(ctx.cwd);
161
206
  if (!active.isSettled()) {
162
207
  await forceStoppedRecord(store, runId);
163
- this.log(`[workflow:${runId}] background workflow did not settle after stop; retained state was forced to stopped.`);
208
+ this.log(`[workflow:${runId}] workflow did not settle after stop; retained state was forced to stopped.`);
164
209
  }
165
210
  const record = await store.load(runId);
166
211
  if (!record) throw new Error(`Workflow run ${runId} was not found after stopping.`);
@@ -182,7 +227,7 @@ export class BackgroundWorkflowCoordinator {
182
227
  try {
183
228
  records = await store.list();
184
229
  } catch (error) {
185
- this.log(`[workflow] background recovery could not load run history: ${unknownErrorMessage(error)}`);
230
+ this.log(`[workflow] recovery could not load run history: ${unknownErrorMessage(error)}`);
186
231
  return;
187
232
  }
188
233
  const sessionId = ctx.sessionManager.getSessionId();
@@ -197,7 +242,7 @@ export class BackgroundWorkflowCoordinator {
197
242
  sessionId,
198
243
  this.active.has(record.runId),
199
244
  );
200
- if (!isPendingBackgroundOutcome(record)) continue;
245
+ if (!isPendingOutcome(record)) continue;
201
246
  const originSessionId = record.background.origin.sessionId;
202
247
  if (originSessionId === sessionId) {
203
248
  await this.queueOrDeliver(ctx, record.runId);
@@ -219,7 +264,7 @@ export class BackgroundWorkflowCoordinator {
219
264
  });
220
265
  this.log(`[workflow:${record.runId}] ${message}`);
221
266
  } catch (error) {
222
- this.log(`[workflow:${record.runId}] background recovery failed: ${unknownErrorMessage(error)}`);
267
+ this.log(`[workflow:${record.runId}] recovery failed: ${unknownErrorMessage(error)}`);
223
268
  }
224
269
  }
225
270
  }
@@ -238,12 +283,11 @@ export class BackgroundWorkflowCoordinator {
238
283
  if (run.isSettled()) continue;
239
284
  try {
240
285
  await forcePausedRecord(store, runId);
241
- this.log(`[workflow:${runId}] background workflow did not settle during shutdown; retained state was forced to paused.`);
286
+ this.log(`[workflow:${runId}] workflow did not settle during shutdown; retained state was forced to paused.`);
242
287
  } catch (error) {
243
288
  this.log(`[workflow:${runId}] failed to force paused state during shutdown: ${unknownErrorMessage(error)}`);
244
289
  }
245
290
  }
246
- if (ctx.hasUI) ctx.ui.setWidget(BACKGROUND_WIDGET_KEY, undefined);
247
291
  }
248
292
 
249
293
  private async queueOrDeliver(ctx: ExtensionContext, runId: string): Promise<void> {
@@ -266,7 +310,7 @@ export class BackgroundWorkflowCoordinator {
266
310
  try {
267
311
  await listener(ctx, runId);
268
312
  } catch (error) {
269
- this.log(`[workflow:${runId}] background settlement listener failed: ${unknownErrorMessage(error)}`);
313
+ this.log(`[workflow:${runId}] settlement listener failed: ${unknownErrorMessage(error)}`);
270
314
  }
271
315
  }
272
316
  }
@@ -280,7 +324,7 @@ export class BackgroundWorkflowCoordinator {
280
324
  try {
281
325
  if (await this.deliver(ctx, runId)) pending.delete(runId);
282
326
  } catch (error) {
283
- this.log(`[workflow:${runId}] background delivery retry failed: ${unknownErrorMessage(error)}`);
327
+ this.log(`[workflow:${runId}] delivery retry failed: ${unknownErrorMessage(error)}`);
284
328
  }
285
329
  }
286
330
  if (pending.size === 0) this.pendingDelivery.delete(sessionId);
@@ -292,23 +336,6 @@ export class BackgroundWorkflowCoordinator {
292
336
  this.pendingDelivery.set(sessionId, pending);
293
337
  }
294
338
 
295
- private updateBackgroundSurface(ctx: ExtensionContext): void {
296
- if (!ctx.hasUI) return;
297
- const sessionId = ctx.sessionManager.getSessionId();
298
- const runs = [...this.active.entries()]
299
- .filter(([, run]) => run.sessionId === sessionId)
300
- .map(([runId, run]) => ({ runId, name: run.name }));
301
- if (runs.length === 0) {
302
- ctx.ui.setWidget(BACKGROUND_WIDGET_KEY, undefined);
303
- return;
304
- }
305
- ctx.ui.setWidget(
306
- BACKGROUND_WIDGET_KEY,
307
- [formatBackgroundActivity(runs, ctx.ui.theme)],
308
- { placement: "aboveEditor" },
309
- );
310
- }
311
-
312
339
  private async deliver(ctx: ExtensionContext, runId: string): Promise<boolean> {
313
340
  const store = this.storeForCwd(ctx.cwd);
314
341
  const record = await store.load(runId);
@@ -316,12 +343,12 @@ export class BackgroundWorkflowCoordinator {
316
343
  if (record.background.origin.sessionId !== ctx.sessionManager.getSessionId()) return false;
317
344
  if (!isDeliverableState(record.state)) return false;
318
345
 
319
- if (!sessionHasBackgroundDelivery(ctx, runId)) {
320
- const details = backgroundResultDetails(record);
346
+ if (!sessionHasDelivery(ctx, runId)) {
347
+ const details = workflowCompletionDetails(record);
321
348
  this.pi.sendMessage(
322
349
  {
323
- customType: BACKGROUND_DELIVERY_CUSTOM_TYPE,
324
- content: formatBackgroundDelivery(details),
350
+ customType: WORKFLOW_DELIVERY_CUSTOM_TYPE,
351
+ content: formatWorkflowDelivery(details),
325
352
  display: true,
326
353
  details,
327
354
  },
@@ -333,35 +360,24 @@ export class BackgroundWorkflowCoordinator {
333
360
  }
334
361
  }
335
362
 
336
- function formatBackgroundActivity(
337
- runs: readonly { readonly runId: string; readonly name: string }[],
338
- theme: ExtensionContext["ui"]["theme"],
339
- ): string {
340
- const visible = runs.slice(0, 2).map((run) => `${run.name} ${run.runId.slice(0, 8)}`);
341
- const hidden = runs.length - visible.length;
342
- const suffix = hidden > 0 ? ` · +${hidden} more` : "";
343
- return `${theme.fg("accent", "●")} ${theme.bold("Background workflows")} ${theme.fg("dim", `· ${visible.join(" · ")}${suffix}`)}`;
344
- }
345
-
346
- export function backgroundOrigin(ctx: Pick<ExtensionContext, "sessionManager">, requestedAt = Date.now()): WorkflowBackgroundOrigin {
363
+ export function workflowOrigin(ctx: Pick<ExtensionContext, "sessionManager">, requestedAt = Date.now()): WorkflowOrigin {
347
364
  return { sessionId: ctx.sessionManager.getSessionId(), requestedAt };
348
365
  }
349
366
 
350
- export function backgroundResultDetails(record: WorkflowRunRecord): BackgroundWorkflowResultDetails {
367
+ export function workflowCompletionDetails(record: WorkflowRunRecord): WorkflowCompletionDetails {
351
368
  if (!isDeliverableState(record.state)) throw new Error(`Workflow run ${record.runId} has not finished or paused.`);
352
369
  return {
353
370
  name: record.workflow.name,
354
- result: { summary: backgroundSummary(record) },
371
+ result: { summary: workflowSummary(record) },
355
372
  completedAt: record.endedAt ?? record.updatedAt,
356
373
  usage: record.usage,
357
374
  runId: record.runId,
358
375
  resumedFromRunId: record.options.resumeFromRunId,
359
- background: true,
360
376
  status: record.state,
361
377
  };
362
378
  }
363
379
 
364
- function backgroundSummary(record: WorkflowRunRecord): string {
380
+ function workflowSummary(record: WorkflowRunRecord): string {
365
381
  if (record.state !== "completed") return boundedSummary(`Workflow ${record.state}: ${record.message}`);
366
382
  if (record.result.kind === "unavailable") {
367
383
  return boundedSummary(`Workflow completed; retained result is unavailable: ${record.result.reason}`);
@@ -372,9 +388,9 @@ function backgroundSummary(record: WorkflowRunRecord): string {
372
388
  return "Workflow completed. Open run history for the retained result.";
373
389
  }
374
390
 
375
- function formatBackgroundDelivery(details: BackgroundWorkflowResultDetails): string {
391
+ function formatWorkflowDelivery(details: WorkflowCompletionDetails): string {
376
392
  return [
377
- `## Background workflow: ${details.name}`,
393
+ `## Workflow: ${details.name}`,
378
394
  "",
379
395
  `Run ID: ${details.runId}`,
380
396
  `State: ${details.status}`,
@@ -383,17 +399,17 @@ function formatBackgroundDelivery(details: BackgroundWorkflowResultDetails): str
383
399
  ].join("\n");
384
400
  }
385
401
 
386
- function sessionHasBackgroundDelivery(ctx: ExtensionContext, runId: string): boolean {
402
+ function sessionHasDelivery(ctx: ExtensionContext, runId: string): boolean {
387
403
  for (const entry of ctx.sessionManager.getEntries()) {
388
404
  if (entry.type !== "message" || entry.message.role !== "custom") continue;
389
- if (entry.message.customType !== BACKGROUND_DELIVERY_CUSTOM_TYPE) continue;
405
+ if (entry.message.customType !== WORKFLOW_DELIVERY_CUSTOM_TYPE) continue;
390
406
  const details = entry.message.details;
391
- if (isRecord(details) && details.background === true && details.runId === runId) return true;
407
+ if (isRecord(details) && details.runId === runId) return true;
392
408
  }
393
409
  return false;
394
410
  }
395
411
 
396
- function isPendingBackgroundOutcome(record: WorkflowRunRecord): record is WorkflowRunRecord & { readonly background: NonNullable<WorkflowRunRecord["background"]> } {
412
+ function isPendingOutcome(record: WorkflowRunRecord): record is WorkflowRunRecord & { readonly background: NonNullable<WorkflowRunRecord["background"]> } {
397
413
  return record.background?.delivery.state === "pending" && isDeliverableState(record.state);
398
414
  }
399
415
 
@@ -0,0 +1,66 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { WorkflowLifecycle } from "./workflow-lifecycle.ts";
3
+ import { validateWorkflowRunId } from "./journal.ts";
4
+ import type { WorkflowProgressSource } from "./types.ts";
5
+ import { ProjectWorkflowRunStore } from "./workflow-run-store.ts";
6
+ import { unknownErrorMessage } from "./unknown-error.ts";
7
+
8
+ /** On-demand telemetry only: never inject live agent output into the parent transcript. */
9
+ export async function manageWorkflow(
10
+ params: { action: "list" | "inspect" | "stop"; runId?: string; agentId?: number; name?: string; script?: string },
11
+ ctx: ExtensionContext,
12
+ coordinator: WorkflowLifecycle,
13
+ sources: ReadonlyMap<string, { source: WorkflowProgressSource }>,
14
+ ) {
15
+ const result = (details: Record<string, unknown>) => ({
16
+ content: [{ type: "text" as const, text: boundedTelemetry(details) }], details,
17
+ });
18
+ try {
19
+ if (params.name !== undefined || params.script !== undefined) throw new Error("Management actions do not accept name or script.");
20
+ const store = new ProjectWorkflowRunStore(ctx.cwd);
21
+ const sessionId = ctx.sessionManager.getSessionId();
22
+ if (params.action === "list") {
23
+ if (params.runId || params.agentId !== undefined) throw new Error("list does not accept runId or agentId.");
24
+ const records = (await store.list()).filter((run) => run.background?.origin.sessionId === sessionId);
25
+ return result({ runs: records.sort((a, b) => b.createdAt - a.createdAt).slice(0, 30).map((run) => ({
26
+ runId: run.runId, name: run.workflow.name, state: run.state,
27
+ agents: (sources.get(run.runId)?.source.snapshot() ?? run.progress).phases.flatMap((phase) => phase.agents).map((agent) => ({ id: agent.id, label: agent.label, status: agent.status })),
28
+ })) });
29
+ }
30
+ if (!params.runId) throw new Error("runId is required.");
31
+ validateWorkflowRunId(params.runId);
32
+ const source = sources.get(params.runId)?.source;
33
+ const record = await store.load(params.runId);
34
+ if (!source && record?.background?.origin.sessionId !== sessionId) throw new Error("Run not found in this session.");
35
+ if (params.action === "stop") {
36
+ if (params.agentId !== undefined) {
37
+ if (!source?.stopAgent) throw new Error("Agent is not active in this session.");
38
+ source.stopAgent(params.agentId);
39
+ return result({ runId: params.runId, agentId: params.agentId, state: "stop_requested" });
40
+ }
41
+ const stopped = await coordinator.stop(ctx, params.runId);
42
+ return result({ runId: stopped.runId, state: stopped.state });
43
+ }
44
+ const snapshot = source?.snapshot() ?? record?.progress;
45
+ if (!snapshot) throw new Error("Run not found.");
46
+ if (params.agentId === undefined) return result({ snapshot });
47
+ const agent = snapshot.phases.flatMap((phase) => phase.agents).find((row) => row.id === params.agentId);
48
+ if (!agent) throw new Error("Agent not found.");
49
+ return result({ runId: params.runId, agent,
50
+ // Bounded untrusted telemetry, not instructions. Never expose thinking blocks here.
51
+ untrustedActivity: source?.conversation(agent.id).slice(-20) ?? [],
52
+ queued: source?.transcript?.(agent.id) ? {
53
+ steering: source.transcript(agent.id)?.steering,
54
+ followUp: source.transcript(agent.id)?.followUp,
55
+ } : undefined,
56
+ });
57
+ } catch (error) {
58
+ return result({ error: unknownErrorMessage(error) });
59
+ }
60
+ }
61
+
62
+ function boundedTelemetry(details: Record<string, unknown>): string {
63
+ const text = JSON.stringify(details, null, 2);
64
+ const limit = 24_000;
65
+ return text.length <= limit ? text : `${text.slice(0, limit)}\n[Truncated. Inspect a specific agentId for details.]`;
66
+ }
@@ -3,8 +3,7 @@ import type {
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import type { AutocompleteItem } from "@earendil-works/pi-tui";
6
- import { BackgroundWorkflowCoordinator } from "./background-workflows.ts";
7
- import { backgroundUnavailableResult, startBackgroundWorkflowTool } from "./background-workflow-tool.ts";
6
+ import { WorkflowLifecycle, workflowUnavailableResult } from "./workflow-lifecycle.ts";
8
7
  import { validateWorkflowRunId } from "./journal.ts";
9
8
  import { resolveWorkflowRunOptions, type ResolvedWorkflowRunOptions } from "./options.ts";
10
9
  import type { LoadedWorkflow } from "./types.ts";
@@ -29,8 +28,7 @@ import {
29
28
  WorkflowUsageLimitScheduler,
30
29
  type WorkflowUsageLimitSchedulerClock,
31
30
  } from "./workflow-usage-limit-scheduler.ts";
32
- import { WorkflowInspector } from "./ui/workflow-inspector.ts";
33
- import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./ui/workflow-viewer-layout.ts";
31
+ import { WorkflowInspector, WORKFLOW_INSPECTOR_OVERLAY_OPTIONS } from "./ui/workflow-inspector.ts";
34
32
  import { completeCurrentArgument, splitArgumentPrefix } from "./command-completions.ts";
35
33
 
36
34
  type WorkflowRunCompletionContext = Pick<ExtensionContext, "cwd" | "sessionManager">;
@@ -55,7 +53,7 @@ export class WorkflowRunController {
55
53
  private completionContext: WorkflowRunCompletionContext | undefined;
56
54
 
57
55
  constructor(
58
- private readonly background: BackgroundWorkflowCoordinator,
56
+ private readonly lifecycle: WorkflowLifecycle,
59
57
  private readonly dependencies: WorkflowRunControllerDependencies,
60
58
  ) {
61
59
  this.storeForCwd = dependencies.storeForCwd ?? ((cwd) => new ProjectWorkflowRunStore(cwd));
@@ -101,7 +99,7 @@ export class WorkflowRunController {
101
99
  }
102
100
  if (!ctx.hasUI) {
103
101
  const records = await this.listRecent(ctx.cwd);
104
- ctx.ui.notify(formatWorkflowRunHistory(records, this.background.activeRunIds(ctx)), "info");
102
+ ctx.ui.notify(formatWorkflowRunHistory(records, this.lifecycle.activeRunIds(ctx)), "info");
105
103
  return;
106
104
  }
107
105
  await this.openRunSelector(ctx);
@@ -117,7 +115,7 @@ export class WorkflowRunController {
117
115
  private async openRunSelector(ctx: ExtensionCommandContext): Promise<void> {
118
116
  while (true) {
119
117
  const records = await this.listRecent(ctx.cwd);
120
- const active = this.background.activeRunIds(ctx);
118
+ const active = this.lifecycle.activeRunIds(ctx);
121
119
  if (records.length === 0) {
122
120
  ctx.ui.notify(formatWorkflowRunHistory(records, active), "info");
123
121
  return;
@@ -149,7 +147,7 @@ export class WorkflowRunController {
149
147
  private async inspect(record: WorkflowRunRecord, ctx: ExtensionContext): Promise<void> {
150
148
  if (!ctx.hasUI || ctx.mode !== "tui") {
151
149
  ctx.ui.notify(
152
- formatWorkflowRunDetails(record, this.background.activeRunIds(ctx).has(record.runId)),
150
+ formatWorkflowRunDetails(record, this.lifecycle.activeRunIds(ctx).has(record.runId)),
153
151
  "info",
154
152
  );
155
153
  return;
@@ -162,7 +160,7 @@ export class WorkflowRunController {
162
160
  () => done(undefined),
163
161
  { label: `${record.state.toUpperCase()} outcome`, text: retainedWorkflowRunOutcome(record) },
164
162
  ),
165
- WORKFLOW_VIEWER_OVERLAY_OPTIONS,
163
+ WORKFLOW_INSPECTOR_OVERLAY_OPTIONS,
166
164
  );
167
165
  }
168
166
 
@@ -182,7 +180,7 @@ export class WorkflowRunController {
182
180
  }
183
181
  const available = availableWorkflowRunActions(
184
182
  record,
185
- this.background.activeRunIds(ctx).has(record.runId),
183
+ this.lifecycle.activeRunIds(ctx).has(record.runId),
186
184
  );
187
185
  if (!available.includes(action)) {
188
186
  ctx.ui.notify(`Action ${action} is not available for ${record.state} run ${runId}.`, "warning");
@@ -204,11 +202,11 @@ export class WorkflowRunController {
204
202
  error: new Error("Workflow stopped by user."),
205
203
  });
206
204
  await this.storeForCwd(ctx.cwd).save(stopped);
207
- await this.background.durableRunSettled(ctx, runId);
205
+ await this.lifecycle.durableRunSettled(ctx, runId);
208
206
  ctx.ui.notify(`Workflow run ${runId} is now stopped.`, "info");
209
207
  return;
210
208
  }
211
- const stopped = await this.background.stop(ctx, runId);
209
+ const stopped = await this.lifecycle.stop(ctx, runId);
212
210
  ctx.ui.notify(`Workflow run ${runId} is now ${stopped.state}.`, "info");
213
211
  return;
214
212
  }
@@ -225,10 +223,10 @@ export class WorkflowRunController {
225
223
  record: WorkflowRunRecord,
226
224
  action: "resume" | "restart",
227
225
  ): Promise<string> {
228
- const unavailable = backgroundUnavailableResult(ctx.mode);
226
+ const unavailable = workflowUnavailableResult(ctx.mode);
229
227
  if (unavailable) {
230
228
  const first = unavailable.content[0];
231
- throw new Error(first?.type === "text" ? first.text : "background workflows are unavailable");
229
+ throw new Error(first?.type === "text" ? first.text : "workflows are unavailable");
232
230
  }
233
231
  const workflow = await this.dependencies.resolveWorkflow(record.workflow.name);
234
232
  if (!workflow) throw new Error(`registered workflow ${record.workflow.name} is unavailable`);
@@ -242,7 +240,6 @@ export class WorkflowRunController {
242
240
  throw new Error("workflow source changed, so journal replay cannot resume safely");
243
241
  }
244
242
  const options = resolveWorkflowRunOptions({
245
- inspect: false,
246
243
  perf: record.options.perf,
247
244
  concurrency: record.options.concurrency ?? undefined,
248
245
  parallelSubmissionLimit: record.options.parallelSubmissionLimit ?? undefined,
@@ -259,13 +256,12 @@ export class WorkflowRunController {
259
256
  resultViewer: "skip",
260
257
  resumeFromRunId: action === "resume" ? record.runId : undefined,
261
258
  });
262
- const result = await startBackgroundWorkflowTool({
263
- coordinator: this.background,
259
+ const result = await this.lifecycle.launch({
264
260
  ctx,
265
261
  name: workflow.meta.name,
266
262
  options,
267
- execute: (backgroundCtx, backgroundOptions) =>
268
- this.dependencies.execute(backgroundCtx, workflow.meta.name, workflow, backgroundOptions),
263
+ execute: (runCtx, runOptions) =>
264
+ this.dependencies.execute(runCtx, workflow.meta.name, workflow, runOptions),
269
265
  });
270
266
  const first = result.content[0];
271
267
  const message = first?.type === "text" ? first.text : `Workflow ${action} started.`;
@@ -314,7 +310,7 @@ export class WorkflowRunController {
314
310
  const action = parts.completed[0];
315
311
  if (!action || !isWorkflowRunLifecycleAction(action)) return null;
316
312
  const records = await this.listRecent(ctx.cwd);
317
- const active = this.background.activeRunIds(ctx);
313
+ const active = this.lifecycle.activeRunIds(ctx);
318
314
  return completeCurrentArgument(
319
315
  argumentPrefix,
320
316
  records
@@ -1,4 +1,4 @@
1
- import type { WorkflowBackgroundOrigin } from "./types.ts";
1
+ import type { WorkflowOrigin } from "./types.ts";
2
2
  import type { WorkflowRunRecord } from "./workflow-run-record.ts";
3
3
 
4
4
  const MAX_BACKGROUND_TEXT = 2_048;
@@ -9,12 +9,12 @@ export type WorkflowRunDelivery =
9
9
  | { readonly state: "unavailable"; readonly attemptedAt: number; readonly message: string };
10
10
 
11
11
  export interface PersistedWorkflowBackground {
12
- readonly origin: WorkflowBackgroundOrigin;
12
+ readonly origin: WorkflowOrigin;
13
13
  readonly delivery: WorkflowRunDelivery;
14
14
  }
15
15
 
16
16
  export function createPersistedWorkflowBackground(
17
- origin: WorkflowBackgroundOrigin | undefined,
17
+ origin: WorkflowOrigin | undefined,
18
18
  ): PersistedWorkflowBackground | undefined {
19
19
  if (!origin) return undefined;
20
20
  return {
@@ -14,7 +14,7 @@ import {
14
14
  createPersistedWorkflowBackground,
15
15
  isPersistedWorkflowBackground,
16
16
  type PersistedWorkflowBackground,
17
- } from "./workflow-run-background.ts";
17
+ } from "./workflow-run-delivery.ts";
18
18
 
19
19
  export const WORKFLOW_RUN_RECORD_VERSION = 1;
20
20
 
@@ -137,7 +137,8 @@ export function createWorkflowRunRecord(input: {
137
137
  createdAt: input.progress.startedAt,
138
138
  updatedAt: input.progress.startedAt,
139
139
  progress: compactWorkflowProgress(input.progress),
140
- background: createPersistedWorkflowBackground(input.options.background),
140
+ // Keep the v1 serialized field name so existing records remain readable.
141
+ background: createPersistedWorkflowBackground(input.options.origin),
141
142
  };
142
143
  }
143
144
 
@@ -242,7 +243,7 @@ function persistedWorkflowRunOptions(
242
243
  argumentsPresent: boolean,
243
244
  ): PersistedWorkflowRunOptions {
244
245
  return {
245
- inspect: options.inspect ?? false,
246
+ inspect: false, // Legacy record field; inspection is now independent of execution.
246
247
  perf: options.perf,
247
248
  concurrency: options.concurrency,
248
249
  parallelSubmissionLimit: options.parallelSubmissionLimit,
@@ -274,6 +275,8 @@ function compactWorkflowProgress(snapshot: WorkflowProgressSnapshot): WorkflowPr
274
275
  id: agent.id,
275
276
  label: boundedText(agent.label),
276
277
  model: agent.model === undefined ? undefined : boundedText(agent.model),
278
+ modelName: agent.modelName === undefined ? undefined : boundedText(agent.modelName),
279
+ thinkingLevel: agent.thinkingLevel === undefined ? undefined : boundedText(agent.thinkingLevel),
277
280
  status: agent.status,
278
281
  startedAt: agent.startedAt,
279
282
  doneAt: agent.doneAt,
@@ -563,7 +566,7 @@ function isPhaseSnapshot(value: unknown): boolean {
563
566
 
564
567
  function isAgentSnapshot(value: unknown): boolean {
565
568
  if (!isRecord(value) || !isFiniteNumber(value.id) || typeof value.label !== "string") return false;
566
- if (value.status !== "queued" && value.status !== "running" && value.status !== "done" && value.status !== "failed") return false;
569
+ if (value.status !== "queued" && value.status !== "running" && value.status !== "stopping" && value.status !== "stopped" && value.status !== "done" && value.status !== "failed") return false;
567
570
  if (!isFiniteNumber(value.toolUses)) return false;
568
571
  if (value.startedAt !== undefined && !isFiniteNumber(value.startedAt)) return false;
569
572
  if (value.doneAt !== undefined && !isFiniteNumber(value.doneAt)) return false;
@@ -119,7 +119,7 @@ export default async function run(api: WorkflowApi, dependencies: CodeReviewDepe
119
119
  "Then: list the changed files, summarize the change in one paragraph (mention the PR if one was found), " +
120
120
  "and read any relevant AGENTS.md or project docs noting conventions a reviewer should know.\n" +
121
121
  "Return diffCommand exactly as a reviewer should run it. Structured output only.",
122
- { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
122
+ { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, ...api.modelProfile("medium"), schema: ScopeSchema },
123
123
  );
124
124
 
125
125
  if (!scope) {
@@ -65,7 +65,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
65
65
  "Inspect relevant files, package/test configuration, and safe diagnostic commands. " +
66
66
  "Safe commands are read-only commands such as status, grep, listing files, typecheck/test commands, or commands explicitly requested by the user. " +
67
67
  "Do not run mutation, install, commit, network, or destructive commands. Return scoped files, observations, and constraints. Structured output only.",
68
- { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
68
+ { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, ...api.modelProfile("medium"), schema: ScopeSchema },
69
69
  );
70
70
 
71
71
  if (!scope) {