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.
- package/.pi/extensions/pi-workflow-engine/index.ts +168 -121
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +145 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +376 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +87 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +84 -529
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +413 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +43 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +81 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +7 -0
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +17 -3
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +164 -61
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +117 -20
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +64 -15
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +14 -3
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +208 -74
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +8 -3
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +148 -64
- package/.pi/extensions/pi-workflow-engine/src/options.ts +9 -6
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +2 -5
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +665 -0
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
- package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +17 -22
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +143 -52
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +295 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +404 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +38 -10
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +5 -69
- package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +28 -3
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +282 -41
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +48 -26
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +8 -1
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -2
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -1
- package/README.md +170 -226
- package/USAGE.md +92 -12
- 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
|
|
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 {
|
|
12
|
-
import {
|
|
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 {
|
|
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:
|
|
108
|
-
|
|
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<
|
|
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
|
-
|
|
219
|
-
|
|
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
|
|
217
|
+
return latestWorkflowInspection;
|
|
223
218
|
}
|
|
224
219
|
|
|
225
220
|
export function getActiveWorkflowInspection(): ActiveWorkflowInspection | undefined {
|
|
226
|
-
return
|
|
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
|
-
|
|
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
|
|
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:
|
|
467
|
+
ctx: ExtensionContext,
|
|
462
468
|
name: string,
|
|
463
|
-
mod:
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
502
|
-
|
|
536
|
+
const completed = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
|
|
537
|
+
inspections.last = completed;
|
|
538
|
+
latestWorkflowInspection = completed;
|
|
503
539
|
},
|
|
504
540
|
});
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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(
|
|
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:
|
|
556
|
+
details: execution.envelope,
|
|
524
557
|
},
|
|
525
558
|
{ triggerTurn: false },
|
|
526
559
|
);
|
|
527
560
|
}
|
|
528
561
|
|
|
529
|
-
|
|
530
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
703
|
-
|
|
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: [{
|
|
734
|
-
|
|
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
|
+
}
|