pi-workflow-engine 0.10.3 → 0.11.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.
Files changed (55) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +168 -121
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -0
  3. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +145 -0
  4. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +376 -0
  5. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +87 -0
  6. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +84 -529
  7. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
  8. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +413 -0
  9. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +43 -1
  10. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +81 -0
  11. package/.pi/extensions/pi-workflow-engine/src/budget.ts +7 -0
  12. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +17 -3
  13. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +164 -61
  14. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +117 -20
  15. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +64 -15
  16. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
  17. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +14 -3
  18. package/.pi/extensions/pi-workflow-engine/src/engine.ts +208 -74
  19. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
  20. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
  21. package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
  22. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +8 -3
  23. package/.pi/extensions/pi-workflow-engine/src/journal.ts +148 -64
  24. package/.pi/extensions/pi-workflow-engine/src/options.ts +9 -6
  25. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
  26. package/.pi/extensions/pi-workflow-engine/src/progress.ts +2 -5
  27. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
  28. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +665 -0
  29. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
  30. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
  31. package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
  32. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
  33. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
  34. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
  35. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
  36. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +17 -22
  37. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +143 -52
  38. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
  39. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +295 -0
  40. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +153 -0
  41. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +404 -0
  42. package/.pi/extensions/pi-workflow-engine/src/types.ts +38 -10
  43. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +5 -69
  44. package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
  45. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
  46. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
  47. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +28 -3
  48. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +282 -41
  49. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +48 -26
  50. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +8 -1
  51. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -2
  52. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -1
  53. package/README.md +170 -226
  54. package/USAGE.md +92 -12
  55. package/package.json +9 -9
@@ -2,23 +2,28 @@ import { fileURLToPath } from "node:url";
2
2
  import { Type } from "typebox";
3
3
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { SelectList, Text, truncateToWidth, type Component, type SelectItem, type SelectListTheme, type TUI } from "@earendil-works/pi-tui";
5
+ import { isAdvisoryReport } from "./src/advisory-schema.ts";
5
6
  import type { WorkflowProgressSnapshot } from "./src/progress.ts";
6
- import type { WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
7
+ import type { LoadedWorkflow, WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
7
8
  import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
8
- import type { PerfSink, PerfSnapshot } from "./src/perf.ts";
9
+ import type { PerfSink } from "./src/perf.ts";
9
10
  import type { WorkflowUsageSnapshot } from "./src/usage.ts";
10
- import { registerDynamax } from "./src/dynamax.ts";
11
- import { handleReviewViewerAction } from "./src/review/review-actions.ts";
12
- import { decideReviewResultsPresentation, extensionContextMode, maybeShowReviewResultsViewer } from "./src/review/review-results-flow.ts";
11
+ import { ADAPTIVE_WORKFLOW_GUIDANCE, dynamaxSessionKey, registerDynamax } from "./src/dynamax.ts";
12
+ import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
13
+ import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
13
14
  import {
14
15
  formatWorkflowDetailLines,
15
- isAdvisoryReport,
16
16
  isWorkflowResult,
17
17
  renderWorkflowResult,
18
- type WorkflowPerfDetails,
19
- type WorkflowResultEnvelope,
20
18
  } from "./src/ui/workflow-result-renderer.ts";
21
- import { parseWorkflowBudgetString, WORKFLOW_BUDGET_MAX, WORKFLOW_BUDGET_MIN } from "./src/options.ts";
19
+ import {
20
+ parseWorkflowBudgetString,
21
+ resolveWorkflowRunOptions,
22
+ type ResolvedWorkflowRunOptions,
23
+ WORKFLOW_BUDGET_MAX,
24
+ WORKFLOW_BUDGET_MIN,
25
+ } from "./src/options.ts";
26
+ import { executeWorkflowInvocation, type WorkflowExecution, type WorkflowPerfDetails } from "./src/workflow-execution.ts";
22
27
 
23
28
  /** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
24
29
  const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
@@ -73,21 +78,6 @@ function formatFindingLocation(location: { readonly file: string; readonly line?
73
78
  return `${location.file}${line}${symbol}`;
74
79
  }
75
80
 
76
- function workflowEnvelope(
77
- name: string,
78
- result: unknown,
79
- usage?: WorkflowUsageSnapshot,
80
- perf?: WorkflowPerfDetails,
81
- metadata?: WorkflowRunMetadata,
82
- ): WorkflowResultEnvelope {
83
- return { name, result, completedAt: Date.now(), usage, perf, runId: metadata?.runId, resumedFromRunId: metadata?.resumedFromRunId };
84
- }
85
-
86
- function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDetails | undefined {
87
- if (!snapshot?.enabled) return undefined;
88
- return { enabled: true, startedAt: snapshot.startedAt, aggregates: snapshot.aggregates };
89
- }
90
-
91
81
  type DiscoveryModule = typeof import("./src/discovery.ts");
92
82
  type EngineModule = typeof import("./src/engine.ts");
93
83
  type InlineWorkflowModule = typeof import("./src/inline-workflow.ts");
@@ -104,9 +94,8 @@ async function loadInlineWorkflow(): Promise<InlineWorkflowModule> {
104
94
  return await import("./src/inline-workflow.ts");
105
95
  }
106
96
 
107
- async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSink | undefined> {
108
- const enabled = options.perf ?? process.env.PI_WORKFLOW_PERF === "1";
109
- if (!enabled) return undefined;
97
+ async function createInvocationPerf(options: ResolvedWorkflowRunOptions): Promise<PerfSink | undefined> {
98
+ if (!options.perf) return undefined;
110
99
  const { createPerfRecorder } = await import("./src/perf.ts");
111
100
  return createPerfRecorder(true);
112
101
  }
@@ -114,7 +103,7 @@ async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSi
114
103
  /**
115
104
  * Resolve an `api.workflow()` reference to a registered workflow module. Throws on an unknown name.
116
105
  */
117
- export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<WorkflowModule> {
106
+ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<LoadedWorkflow> {
118
107
  const { discoverWorkflows } = await loadDiscovery();
119
108
  const workflows = await discoverWorkflows(EXTENSION_DIR, { perf });
120
109
  const mod = workflows.get(ref);
@@ -215,15 +204,21 @@ export interface ActiveWorkflowInspection {
215
204
  readonly snapshot: () => WorkflowProgressSnapshot;
216
205
  }
217
206
 
218
- let lastWorkflowInspection: LastWorkflowInspection | undefined;
219
- let activeWorkflowInspection: ActiveWorkflowInspection | undefined;
207
+ interface SessionWorkflowInspections {
208
+ last?: LastWorkflowInspection;
209
+ active?: ActiveWorkflowInspection;
210
+ }
211
+
212
+ const workflowInspections = new WeakMap<ExtensionAPI, Map<string, SessionWorkflowInspections>>();
213
+ let latestWorkflowInspection: LastWorkflowInspection | undefined;
214
+ let latestActiveWorkflowInspection: ActiveWorkflowInspection | undefined;
220
215
 
221
216
  export function getLastWorkflowInspection(): LastWorkflowInspection | undefined {
222
- return lastWorkflowInspection;
217
+ return latestWorkflowInspection;
223
218
  }
224
219
 
225
220
  export function getActiveWorkflowInspection(): ActiveWorkflowInspection | undefined {
226
- return activeWorkflowInspection;
221
+ return latestActiveWorkflowInspection;
227
222
  }
228
223
 
229
224
  export async function openWorkflowInspector(ctx: ExtensionContext, inspection: LastWorkflowInspection | ActiveWorkflowInspection): Promise<void> {
@@ -233,12 +228,22 @@ export async function openWorkflowInspector(ctx: ExtensionContext, inspection: L
233
228
  );
234
229
  }
235
230
 
236
- async function openAvailableWorkflowInspector(ctx: ExtensionContext): Promise<void> {
231
+ function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
232
+ const sessions = workflowInspections.get(pi) ?? new Map<string, SessionWorkflowInspections>();
233
+ const key = dynamaxSessionKey(ctx);
234
+ const state = sessions.get(key) ?? {};
235
+ sessions.set(key, state);
236
+ workflowInspections.set(pi, sessions);
237
+ return state;
238
+ }
239
+
240
+ async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
237
241
  if (!ctx.hasUI) {
238
242
  ctx.ui.notify("Workflow inspector requires the TUI", "warning");
239
243
  return;
240
244
  }
241
- const inspection = activeWorkflowInspection ?? lastWorkflowInspection;
245
+ const state = workflowInspectionState(pi, ctx);
246
+ const inspection = state.active ?? state.last;
242
247
  if (!inspection) {
243
248
  ctx.ui.notify("No workflow inspector is available yet", "warning");
244
249
  return;
@@ -395,6 +400,7 @@ Always pass a plain string as the first api.agent() argument; build prompts with
395
400
  If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
396
401
  When the run is budgeted, guard expensive loops with \`while (api.budget.total && api.budget.remaining() > N) { ... }\`; api.agent() throws once the budget is spent.
397
402
  Subagents receive no skills by default. When the brief asks for a skill or a stage clearly benefits from one, pass \`skills: ["skill-name"]\` on that agent call only.
403
+ ${ADAPTIVE_WORKFLOW_GUIDANCE}
398
404
  Do not edit files unless the user explicitly requested edits.`;
399
405
  }
400
406
 
@@ -458,76 +464,127 @@ export async function pickWorkflow(
458
464
 
459
465
  export async function sendWorkflowResult(
460
466
  pi: ExtensionAPI,
461
- ctx: ExtensionCommandContext,
467
+ ctx: ExtensionContext,
462
468
  name: string,
463
- mod: WorkflowModule,
469
+ mod: LoadedWorkflow,
464
470
  args: string,
465
471
  options: WorkflowRunOptions,
466
472
  perfRecorder?: PerfSink,
473
+ reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
467
474
  ): Promise<void> {
468
- const { runWorkflow } = await loadEngine();
469
- let perfSnapshot: PerfSnapshot | undefined;
470
- let usageSnapshot: WorkflowUsageSnapshot | undefined;
471
- let runMetadata: WorkflowRunMetadata | undefined;
475
+ await sendResolvedWorkflowResult(
476
+ pi,
477
+ ctx,
478
+ name,
479
+ mod,
480
+ args,
481
+ resolveWorkflowRunOptions(options),
482
+ perfRecorder,
483
+ reviewSessions,
484
+ );
485
+ }
486
+
487
+ async function sendResolvedWorkflowResult(
488
+ pi: ExtensionAPI,
489
+ ctx: ExtensionContext,
490
+ name: string,
491
+ mod: LoadedWorkflow,
492
+ args: string,
493
+ options: ResolvedWorkflowRunOptions,
494
+ perfRecorder: PerfSink | undefined,
495
+ reviewSessions: ReviewSessionCoordinator,
496
+ ): Promise<void> {
497
+ const execution = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
498
+ reviewSessions.remember(ctx, execution, options);
499
+ sendWorkflowExecution(pi, execution);
500
+ await reviewSessions.present(ctx, execution, options);
501
+ }
502
+
503
+ async function executeResolvedWorkflow(
504
+ pi: ExtensionAPI,
505
+ ctx: ExtensionContext,
506
+ name: string,
507
+ mod: LoadedWorkflow,
508
+ args: string,
509
+ options: ResolvedWorkflowRunOptions,
510
+ perfRecorder?: PerfSink,
511
+ ): Promise<WorkflowExecution> {
512
+ const { runResolvedWorkflow } = await loadEngine();
472
513
  let liveInspection: ActiveWorkflowInspection | undefined;
473
- const result = await runWorkflow(ctx, mod, args, {
474
- ...options,
475
- perf: options.perf ?? perfRecorder !== undefined,
514
+ const inspections = workflowInspectionState(pi, ctx);
515
+ return await executeWorkflowInvocation({
516
+ ctx,
517
+ name,
518
+ mod,
519
+ args,
520
+ options,
476
521
  perfRecorder,
522
+ runResolvedWorkflow,
477
523
  resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
478
524
  onProgressSource(source) {
479
525
  if (source) {
480
526
  liveInspection = bindActiveWorkflowInspection(name, args, source);
481
- activeWorkflowInspection = liveInspection;
482
- } else if (activeWorkflowInspection === liveInspection) {
483
- activeWorkflowInspection = undefined;
527
+ inspections.active = liveInspection;
528
+ latestActiveWorkflowInspection = liveInspection;
529
+ } else if (inspections.active === liveInspection) {
530
+ inspections.active = undefined;
531
+ if (latestActiveWorkflowInspection === liveInspection) latestActiveWorkflowInspection = undefined;
484
532
  liveInspection = undefined;
485
533
  }
486
- options.onProgressSource?.(source);
487
- },
488
- onPerfSnapshot(snapshot) {
489
- perfSnapshot = snapshot;
490
- options.onPerfSnapshot?.(snapshot);
491
- },
492
- onUsageSnapshot(snapshot) {
493
- usageSnapshot = snapshot;
494
- options.onUsageSnapshot?.(snapshot);
495
- },
496
- onRunMetadata(metadata) {
497
- runMetadata = metadata;
498
- options.onRunMetadata?.(metadata);
499
534
  },
500
535
  onProgressSnapshot(snapshot) {
501
- lastWorkflowInspection = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
502
- options.onProgressSnapshot?.(snapshot);
536
+ const completed = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
537
+ inspections.last = completed;
538
+ latestWorkflowInspection = completed;
503
539
  },
504
540
  });
505
- const perf = compactPerfSnapshot(perfSnapshot);
506
- const reviewDecision = decideReviewResultsPresentation({
507
- workflowName: name,
508
- result,
509
- mode: extensionContextMode(ctx),
510
- hasUI: ctx.hasUI,
511
- resultViewer: options.resultViewer,
512
- invocationKind: "command",
513
- });
514
- const reviewAction = await maybeShowReviewResultsViewer(ctx, reviewDecision);
515
- if (reviewDecision.kind !== "send") {
516
- await handleReviewViewerAction(pi, ctx, reviewAction, reviewDecision.issues, reviewDecision.report.reviewContext);
517
- }
541
+ }
542
+
543
+ function sendWorkflowExecution(pi: ExtensionAPI, execution: WorkflowExecution): void {
544
+ const { name } = execution.envelope;
518
545
  pi.sendMessage(
519
546
  {
520
547
  customType: "workflow-result",
521
- content: formatMessageContent(name, result, usageSnapshot, perf, runMetadata),
548
+ content: formatMessageContent(
549
+ name,
550
+ execution.envelope.result,
551
+ execution.envelope.usage,
552
+ execution.envelope.perf,
553
+ execution.metadata,
554
+ ),
522
555
  display: true,
523
- details: workflowEnvelope(name, result, usageSnapshot, perf, runMetadata),
556
+ details: execution.envelope,
524
557
  },
525
558
  { triggerTurn: false },
526
559
  );
527
560
  }
528
561
 
529
- export default function workflowEngine(pi: ExtensionAPI): void {
530
- registerDynamax(pi, undefined, { openInspector: openAvailableWorkflowInspector });
562
+ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordinator {
563
+ return new ReviewSessionCoordinator(pi, {
564
+ async runFollowUp(ctx, workflow, options) {
565
+ const perfRecorder = await createInvocationPerf(options);
566
+ return await executeResolvedWorkflow(pi, ctx, workflow.meta.name, workflow, "", options, perfRecorder);
567
+ },
568
+ publish: (execution) => sendWorkflowExecution(pi, execution),
569
+ });
570
+ }
571
+
572
+ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts()): void {
573
+ const reviewSessions = createReviewSessionCoordinator(pi);
574
+ registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
575
+ pi.on("session_shutdown", (_event, ctx) => {
576
+ const key = dynamaxSessionKey(ctx);
577
+ workflowInspections.get(pi)?.delete(key);
578
+ reviewSessions.dispose(ctx);
579
+ });
580
+ if (shortcuts.results) {
581
+ pi.registerShortcut(shortcuts.results, {
582
+ description: "Open last code-review results",
583
+ handler: async (ctx) => {
584
+ await reviewSessions.reopen(ctx);
585
+ },
586
+ });
587
+ }
531
588
 
532
589
  pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
533
590
  const details = message.details;
@@ -548,7 +605,18 @@ export default function workflowEngine(pi: ExtensionAPI): void {
548
605
  ctx.ui.notify("Usage: /workflow:inspector [last]", "warning");
549
606
  return;
550
607
  }
551
- await openAvailableWorkflowInspector(ctx);
608
+ await openAvailableWorkflowInspector(pi, ctx);
609
+ },
610
+ });
611
+
612
+ pi.registerCommand("workflow:results", {
613
+ description: "Reopen the last code-review findings viewer",
614
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
615
+ if (args.trim()) {
616
+ ctx.ui.notify("Usage: /workflow:results", "warning");
617
+ return;
618
+ }
619
+ await reviewSessions.reopen(ctx);
552
620
  },
553
621
  });
554
622
 
@@ -561,7 +629,8 @@ export default function workflowEngine(pi: ExtensionAPI): void {
561
629
  ctx.ui.notify(`Invalid workflow option: ${direct.optionErrors.join("; ")}`, "warning");
562
630
  return;
563
631
  }
564
- const perfRecorder = await createInvocationPerf(direct.options);
632
+ const directOptions = resolveWorkflowRunOptions(direct.options);
633
+ const perfRecorder = await createInvocationPerf(directOptions);
565
634
  const { discoverWorkflows } = await loadDiscovery();
566
635
  const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: direct.refreshDiscovery, perf: perfRecorder });
567
636
  const available = [...workflows.keys()].join(", ") || "(none)";
@@ -583,7 +652,8 @@ export default function workflowEngine(pi: ExtensionAPI): void {
583
652
  return;
584
653
  }
585
654
 
586
- await sendWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, invocation.options, perfRecorder);
655
+ const effectiveOptions = invocation === direct ? directOptions : resolveWorkflowRunOptions(invocation.options);
656
+ await sendResolvedWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, effectiveOptions, perfRecorder, reviewSessions);
587
657
  },
588
658
  });
589
659
 
@@ -605,6 +675,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
605
675
  "When using `isolation: \"worktree\"`, `api.agent()` returns `{ result, patch, changed }`; use `.result` for the answer and `.patch` for the isolated diff.",
606
676
  "If an inline subagent needs grep/find/code-search helpers, use `tools: [\"read\", \"bash\", \"grep\", \"find\", \"ls\"]` plus `toolHints: [\"search\"]` so installed tools such as ast-grep, mgrep, ffgrep, or fffind are discovered dynamically.",
607
677
  "`api.budget` exposes `{ total, spent(), remaining() }` (output tokens). When the run is budgeted, scale fleets from `budget.total` and guard loops with `while (budget.total && budget.remaining() > N) { await api.agent(...) }`; `api.agent()` throws once the ceiling is reached.",
678
+ ADAPTIVE_WORKFLOW_GUIDANCE,
608
679
  "Every workflow tool call must provide exactly one of `name` or `script`, never both.",
609
680
  ],
610
681
  parameters: Type.Object({
@@ -656,7 +727,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
656
727
  };
657
728
  }
658
729
 
659
- const runOptions: WorkflowRunOptions = {
730
+ const runOptions = resolveWorkflowRunOptions({
660
731
  inspect: ctx.hasUI,
661
732
  concurrency: params.concurrency,
662
733
  parallelSubmissionLimit: params.parallelSubmissionLimit,
@@ -664,9 +735,9 @@ export default function workflowEngine(pi: ExtensionAPI): void {
664
735
  perf: params.perf,
665
736
  resumeFromRunId,
666
737
  signal,
667
- };
738
+ });
668
739
  const perfRecorder = await createInvocationPerf(runOptions);
669
- let mod: WorkflowModule;
740
+ let mod: LoadedWorkflow;
670
741
  let resultName: string;
671
742
 
672
743
  if (request.kind === "named") {
@@ -693,45 +764,21 @@ export default function workflowEngine(pi: ExtensionAPI): void {
693
764
  resultName = mod.meta.name;
694
765
  }
695
766
 
696
- const { runWorkflow } = await loadEngine();
697
- let perfSnapshot: PerfSnapshot | undefined;
698
- let usageSnapshot: WorkflowUsageSnapshot | undefined;
699
- let runMetadata: WorkflowRunMetadata | undefined;
700
- let liveInspection: ActiveWorkflowInspection | undefined;
701
767
  const resultArgs = params.args ?? "";
702
- const result = await runWorkflow(ctx, mod, resultArgs, {
703
- ...runOptions,
704
- perf: runOptions.perf ?? perfRecorder !== undefined,
705
- perfRecorder,
706
- resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
707
- onProgressSource(source) {
708
- if (source) {
709
- liveInspection = bindActiveWorkflowInspection(resultName, resultArgs, source);
710
- activeWorkflowInspection = liveInspection;
711
- } else if (activeWorkflowInspection === liveInspection) {
712
- activeWorkflowInspection = undefined;
713
- liveInspection = undefined;
714
- }
715
- },
716
- onPerfSnapshot: (snapshot) => {
717
- perfSnapshot = snapshot;
718
- },
719
- onUsageSnapshot: (snapshot) => {
720
- usageSnapshot = snapshot;
721
- },
722
- onRunMetadata: (metadata) => {
723
- runMetadata = metadata;
724
- },
725
- onProgressSnapshot: (snapshot) => {
726
- // Record the run so /workflow:inspector can reopen it — tool-invoked (dynamax) runs were
727
- // previously uninspectable, unlike the /workflow command path.
728
- lastWorkflowInspection = { name: resultName, args: resultArgs, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
729
- },
730
- });
731
- const perf = compactPerfSnapshot(perfSnapshot);
768
+ const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
769
+ reviewSessions.remember(ctx, execution, runOptions);
732
770
  return {
733
- content: [{ type: "text", text: formatMessageContent(resultName, result, usageSnapshot, perf, runMetadata) }],
734
- details: workflowEnvelope(resultName, result, usageSnapshot, perf, runMetadata),
771
+ content: [{
772
+ type: "text",
773
+ text: formatMessageContent(
774
+ resultName,
775
+ execution.envelope.result,
776
+ execution.envelope.usage,
777
+ execution.envelope.perf,
778
+ execution.metadata,
779
+ ),
780
+ }],
781
+ details: execution.envelope,
735
782
  };
736
783
  },
737
784
  });
@@ -1,4 +1,5 @@
1
1
  import { Type, type Static } from "typebox";
2
+ import { Value } from "typebox/value";
2
3
 
3
4
  export const AdvisorySeveritySchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
4
5
  description: "Impact level of an advisory finding.",
@@ -53,6 +54,11 @@ export const AdvisoryReportSchema = Type.Object({
53
54
  nextSteps: Type.Array(Type.String({ description: "Concrete follow-up commands, inspections, or decisions." })),
54
55
  });
55
56
 
57
+ export const AdvisoryReportWithStatsSchema = Type.Object({
58
+ ...AdvisoryReportSchema.properties,
59
+ stats: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()]))),
60
+ });
61
+
56
62
  export type AdvisorySeverity = Static<typeof AdvisorySeveritySchema>;
57
63
  export type AdvisoryConfidence = Static<typeof AdvisoryConfidenceSchema>;
58
64
  export type AdvisoryVerifierVerdict = Static<typeof AdvisoryVerifierVerdictSchema>;
@@ -62,3 +68,8 @@ export type AdvisoryCandidates = Static<typeof AdvisoryCandidatesSchema>;
62
68
  export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
63
69
  export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
64
70
  export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
71
+ export type AdvisoryReportWithStats = Static<typeof AdvisoryReportWithStatsSchema>;
72
+
73
+ export function isAdvisoryReport(value: unknown): value is AdvisoryReportWithStats {
74
+ return Value.Check(AdvisoryReportWithStatsSchema, value);
75
+ }
@@ -0,0 +1,145 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { assertWorkflowBudgetAvailable } from "./budget.ts";
3
+ import {
4
+ type AgentExecutionOptions,
5
+ type AgentRunTags,
6
+ type RunContext,
7
+ } from "./agent-runner-types.ts";
8
+ import {
9
+ captureReplayIdentity,
10
+ captureIsolatedRepositoryAfterSetup,
11
+ captureSharedRepositoryBeforeSetup,
12
+ isReplayEnabled,
13
+ lookupReplayResult,
14
+ validateReplayEvidence,
15
+ validateReplayIdentity,
16
+ type AgentAttemptResult,
17
+ type AgentReplayEvidence,
18
+ type AgentReplayPlan,
19
+ } from "./agent-replay.ts";
20
+ import { openAgentSession, promptAgentSession, type AgentSessionHandle } from "./agent-session.ts";
21
+ import {
22
+ createAgentWorkspace,
23
+ disposeAgentWorkspace,
24
+ type AgentWorkspace,
25
+ } from "./agent-workspace.ts";
26
+ import type { AgentResumeBaseContext, AgentResumeContext, RepositoryResumeContext } from "./resume-context.ts";
27
+ import { captureRepositoryMutationGuard } from "./resume-context.ts";
28
+
29
+ /** Execute one fully bracketed workspace/session attempt. Cleanup always precedes settlement. */
30
+ export async function executeAgentAttempt(input: {
31
+ readonly rc: RunContext;
32
+ readonly prompt: string;
33
+ readonly opts: AgentExecutionOptions;
34
+ readonly resumeBaseContext: AgentResumeBaseContext;
35
+ readonly model: Model<Api> | undefined;
36
+ readonly replay: AgentReplayPlan;
37
+ readonly label: string;
38
+ readonly rowId: number;
39
+ readonly tags: AgentRunTags;
40
+ }): Promise<AgentAttemptResult> {
41
+ const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags } = input;
42
+ let repositoryBefore: RepositoryResumeContext | undefined;
43
+ let evidence: AgentReplayEvidence | undefined;
44
+ if (isReplayEnabled(replay)) {
45
+ if (replay.kind === "isolated") {
46
+ const mutationGuard = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
47
+ if (mutationGuard.kind === "unverifiable") {
48
+ repositoryBefore = mutationGuard;
49
+ } else {
50
+ evidence = { kind: "isolated", mutationGuard: mutationGuard.fingerprint };
51
+ }
52
+ } else {
53
+ repositoryBefore = await captureSharedRepositoryBeforeSetup(rc, prompt, opts, replay);
54
+ evidence = { kind: "shared" };
55
+ }
56
+ }
57
+ let workspace: AgentWorkspace | undefined;
58
+ let handle: AgentSessionHandle | undefined;
59
+
60
+ try {
61
+ workspace = await createAgentWorkspace(rc, opts, label);
62
+ if (replay.kind === "isolated" && evidence?.kind === "isolated") {
63
+ if (workspace.kind !== "isolated") throw new Error("Isolated replay created a shared workspace.");
64
+ repositoryBefore = await captureIsolatedRepositoryAfterSetup(rc, prompt, opts, workspace);
65
+ }
66
+ handle = await openAgentSession({ rc, prompt, opts, cwd: workspace.cwd, model, label, tags });
67
+
68
+ let identity: AgentResumeContext | undefined;
69
+ if (isReplayEnabled(replay) && repositoryBefore && evidence) {
70
+ const capture = await captureReplayIdentity({
71
+ rc,
72
+ base: resumeBaseContext,
73
+ repository: repositoryBefore,
74
+ selectedSkills: handle.selectedSkills,
75
+ session: handle.session,
76
+ sessionCwd: workspace.cwd,
77
+ });
78
+ if (capture.kind === "unverifiable") {
79
+ rc.progress.log(`${label}: resume disabled for this call (${capture.reason})`);
80
+ } else {
81
+ identity = capture.identity;
82
+ const cached = await lookupReplayResult({ rc, key: replay.key, identity, opts, workspace });
83
+ if (cached.hit) {
84
+ const contract = await validateReplayIdentity({
85
+ rc,
86
+ identity,
87
+ selectedSkills: handle.selectedSkills,
88
+ session: handle.session,
89
+ sessionCwd: workspace.cwd,
90
+ replay,
91
+ workspace,
92
+ });
93
+ const validation = contract.ok
94
+ ? await validateReplayEvidence(rc, evidence)
95
+ : contract;
96
+ if (validation.ok) {
97
+ return { kind: "cache-hit", result: cached.result, identity, evidence };
98
+ }
99
+ rc.progress.log(`${label}: cached result invalidated (${validation.reason})`);
100
+ } else if (cached.reason) {
101
+ rc.progress.log(`${label}: cached result invalidated (${cached.reason})`);
102
+ }
103
+ }
104
+ }
105
+
106
+ assertWorkflowBudgetAvailable(rc.budget);
107
+ const rawResult = await promptAgentSession({
108
+ rc,
109
+ handle,
110
+ prompt,
111
+ opts,
112
+ label,
113
+ rowId,
114
+ tags,
115
+ });
116
+ const result = await workspace.wrapResult(rawResult);
117
+ if (!identity) return { kind: "live-unrecordable", result };
118
+ if (!isReplayEnabled(replay) || !evidence) throw new Error("Replay identity produced without complete replay evidence.");
119
+
120
+ const contract = await validateReplayIdentity({
121
+ rc,
122
+ identity,
123
+ selectedSkills: handle.selectedSkills,
124
+ session: handle.session,
125
+ sessionCwd: workspace.cwd,
126
+ replay,
127
+ workspace,
128
+ });
129
+ const validation = contract.ok
130
+ ? await validateReplayEvidence(rc, evidence)
131
+ : contract;
132
+ if (validation.ok) {
133
+ return { kind: "live-recordable", result, identity, evidence };
134
+ }
135
+ rc.progress.log(`${label}: read-only resume contract was not recorded (${validation.reason})`);
136
+ return { kind: "live-unrecordable", result };
137
+ } finally {
138
+ try {
139
+ const session = handle?.session;
140
+ if (session) rc.perf.timeSync("agent.dispose_ms", () => session.dispose(), tags);
141
+ } finally {
142
+ await disposeAgentWorkspace(rc, label, workspace);
143
+ }
144
+ }
145
+ }