pi-workflow-engine 0.10.2 → 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 +199 -120
- 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,22 +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
16
|
isWorkflowResult,
|
|
16
17
|
renderWorkflowResult,
|
|
17
|
-
type WorkflowPerfDetails,
|
|
18
|
-
type WorkflowResultEnvelope,
|
|
19
18
|
} from "./src/ui/workflow-result-renderer.ts";
|
|
20
|
-
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";
|
|
21
27
|
|
|
22
28
|
/** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
|
|
23
29
|
const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
@@ -37,22 +43,39 @@ function formatMessageContent(
|
|
|
37
43
|
metadata?: WorkflowRunMetadata,
|
|
38
44
|
): string {
|
|
39
45
|
const details = formatWorkflowDetailLines({ usage, perf, metadata });
|
|
40
|
-
return `## Workflow: ${name}\n\n${
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
|
|
46
|
+
return `## Workflow: ${name}\n\n${formatResultForContext(result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatResultForContext(result: unknown): string {
|
|
50
|
+
if (!isAdvisoryReport(result)) return summarize(result);
|
|
51
|
+
|
|
52
|
+
const lines = [result.summary];
|
|
53
|
+
if (result.findings.length > 0) {
|
|
54
|
+
lines.push("", "Findings:");
|
|
55
|
+
result.findings.forEach((finding, index) => {
|
|
56
|
+
const id = `R${String(index + 1).padStart(3, "0")}`;
|
|
57
|
+
lines.push(
|
|
58
|
+
`\n### ${id}: ${finding.summary}`,
|
|
59
|
+
`- Severity: ${finding.severity}`,
|
|
60
|
+
`- Confidence: ${finding.confidence}`,
|
|
61
|
+
`- Category: ${finding.category}`,
|
|
62
|
+
`- Location: ${finding.locations.map(formatFindingLocation).join(", ")}`,
|
|
63
|
+
`- Impact: ${finding.impact}`,
|
|
64
|
+
`- Evidence: ${finding.evidence.length > 0 ? finding.evidence.join("; ") : "(none cited)"}`,
|
|
65
|
+
`- Recommendation: ${finding.recommendation}`,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (result.nextSteps.length > 0) {
|
|
70
|
+
lines.push("", "Next steps:", ...result.nextSteps.map((step) => `- ${step}`));
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
51
73
|
}
|
|
52
74
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
function formatFindingLocation(location: { readonly file: string; readonly line?: number; readonly symbol?: string }): string {
|
|
76
|
+
const line = location.line === undefined ? "" : `:${location.line}`;
|
|
77
|
+
const symbol = location.symbol === undefined ? "" : ` (${location.symbol})`;
|
|
78
|
+
return `${location.file}${line}${symbol}`;
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
type DiscoveryModule = typeof import("./src/discovery.ts");
|
|
@@ -71,9 +94,8 @@ async function loadInlineWorkflow(): Promise<InlineWorkflowModule> {
|
|
|
71
94
|
return await import("./src/inline-workflow.ts");
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
async function createInvocationPerf(options:
|
|
75
|
-
|
|
76
|
-
if (!enabled) return undefined;
|
|
97
|
+
async function createInvocationPerf(options: ResolvedWorkflowRunOptions): Promise<PerfSink | undefined> {
|
|
98
|
+
if (!options.perf) return undefined;
|
|
77
99
|
const { createPerfRecorder } = await import("./src/perf.ts");
|
|
78
100
|
return createPerfRecorder(true);
|
|
79
101
|
}
|
|
@@ -81,7 +103,7 @@ async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSi
|
|
|
81
103
|
/**
|
|
82
104
|
* Resolve an `api.workflow()` reference to a registered workflow module. Throws on an unknown name.
|
|
83
105
|
*/
|
|
84
|
-
export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<
|
|
106
|
+
export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<LoadedWorkflow> {
|
|
85
107
|
const { discoverWorkflows } = await loadDiscovery();
|
|
86
108
|
const workflows = await discoverWorkflows(EXTENSION_DIR, { perf });
|
|
87
109
|
const mod = workflows.get(ref);
|
|
@@ -182,15 +204,21 @@ export interface ActiveWorkflowInspection {
|
|
|
182
204
|
readonly snapshot: () => WorkflowProgressSnapshot;
|
|
183
205
|
}
|
|
184
206
|
|
|
185
|
-
|
|
186
|
-
|
|
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;
|
|
187
215
|
|
|
188
216
|
export function getLastWorkflowInspection(): LastWorkflowInspection | undefined {
|
|
189
|
-
return
|
|
217
|
+
return latestWorkflowInspection;
|
|
190
218
|
}
|
|
191
219
|
|
|
192
220
|
export function getActiveWorkflowInspection(): ActiveWorkflowInspection | undefined {
|
|
193
|
-
return
|
|
221
|
+
return latestActiveWorkflowInspection;
|
|
194
222
|
}
|
|
195
223
|
|
|
196
224
|
export async function openWorkflowInspector(ctx: ExtensionContext, inspection: LastWorkflowInspection | ActiveWorkflowInspection): Promise<void> {
|
|
@@ -200,12 +228,22 @@ export async function openWorkflowInspector(ctx: ExtensionContext, inspection: L
|
|
|
200
228
|
);
|
|
201
229
|
}
|
|
202
230
|
|
|
203
|
-
|
|
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> {
|
|
204
241
|
if (!ctx.hasUI) {
|
|
205
242
|
ctx.ui.notify("Workflow inspector requires the TUI", "warning");
|
|
206
243
|
return;
|
|
207
244
|
}
|
|
208
|
-
const
|
|
245
|
+
const state = workflowInspectionState(pi, ctx);
|
|
246
|
+
const inspection = state.active ?? state.last;
|
|
209
247
|
if (!inspection) {
|
|
210
248
|
ctx.ui.notify("No workflow inspector is available yet", "warning");
|
|
211
249
|
return;
|
|
@@ -362,6 +400,7 @@ Always pass a plain string as the first api.agent() argument; build prompts with
|
|
|
362
400
|
If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
|
|
363
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.
|
|
364
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}
|
|
365
404
|
Do not edit files unless the user explicitly requested edits.`;
|
|
366
405
|
}
|
|
367
406
|
|
|
@@ -425,76 +464,127 @@ export async function pickWorkflow(
|
|
|
425
464
|
|
|
426
465
|
export async function sendWorkflowResult(
|
|
427
466
|
pi: ExtensionAPI,
|
|
428
|
-
ctx:
|
|
467
|
+
ctx: ExtensionContext,
|
|
429
468
|
name: string,
|
|
430
|
-
mod:
|
|
469
|
+
mod: LoadedWorkflow,
|
|
431
470
|
args: string,
|
|
432
471
|
options: WorkflowRunOptions,
|
|
433
472
|
perfRecorder?: PerfSink,
|
|
473
|
+
reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
|
|
434
474
|
): Promise<void> {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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();
|
|
439
513
|
let liveInspection: ActiveWorkflowInspection | undefined;
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
514
|
+
const inspections = workflowInspectionState(pi, ctx);
|
|
515
|
+
return await executeWorkflowInvocation({
|
|
516
|
+
ctx,
|
|
517
|
+
name,
|
|
518
|
+
mod,
|
|
519
|
+
args,
|
|
520
|
+
options,
|
|
443
521
|
perfRecorder,
|
|
522
|
+
runResolvedWorkflow,
|
|
444
523
|
resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
|
|
445
524
|
onProgressSource(source) {
|
|
446
525
|
if (source) {
|
|
447
526
|
liveInspection = bindActiveWorkflowInspection(name, args, source);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
527
|
+
inspections.active = liveInspection;
|
|
528
|
+
latestActiveWorkflowInspection = liveInspection;
|
|
529
|
+
} else if (inspections.active === liveInspection) {
|
|
530
|
+
inspections.active = undefined;
|
|
531
|
+
if (latestActiveWorkflowInspection === liveInspection) latestActiveWorkflowInspection = undefined;
|
|
451
532
|
liveInspection = undefined;
|
|
452
533
|
}
|
|
453
|
-
options.onProgressSource?.(source);
|
|
454
|
-
},
|
|
455
|
-
onPerfSnapshot(snapshot) {
|
|
456
|
-
perfSnapshot = snapshot;
|
|
457
|
-
options.onPerfSnapshot?.(snapshot);
|
|
458
|
-
},
|
|
459
|
-
onUsageSnapshot(snapshot) {
|
|
460
|
-
usageSnapshot = snapshot;
|
|
461
|
-
options.onUsageSnapshot?.(snapshot);
|
|
462
|
-
},
|
|
463
|
-
onRunMetadata(metadata) {
|
|
464
|
-
runMetadata = metadata;
|
|
465
|
-
options.onRunMetadata?.(metadata);
|
|
466
534
|
},
|
|
467
535
|
onProgressSnapshot(snapshot) {
|
|
468
|
-
|
|
469
|
-
|
|
536
|
+
const completed = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
|
|
537
|
+
inspections.last = completed;
|
|
538
|
+
latestWorkflowInspection = completed;
|
|
470
539
|
},
|
|
471
540
|
});
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
mode: extensionContextMode(ctx),
|
|
477
|
-
hasUI: ctx.hasUI,
|
|
478
|
-
resultViewer: options.resultViewer,
|
|
479
|
-
invocationKind: "command",
|
|
480
|
-
});
|
|
481
|
-
const reviewAction = await maybeShowReviewResultsViewer(ctx, reviewDecision);
|
|
482
|
-
if (reviewDecision.kind !== "send") {
|
|
483
|
-
await handleReviewViewerAction(pi, ctx, reviewAction, reviewDecision.issues, reviewDecision.report.reviewContext);
|
|
484
|
-
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function sendWorkflowExecution(pi: ExtensionAPI, execution: WorkflowExecution): void {
|
|
544
|
+
const { name } = execution.envelope;
|
|
485
545
|
pi.sendMessage(
|
|
486
546
|
{
|
|
487
547
|
customType: "workflow-result",
|
|
488
|
-
content: formatMessageContent(
|
|
548
|
+
content: formatMessageContent(
|
|
549
|
+
name,
|
|
550
|
+
execution.envelope.result,
|
|
551
|
+
execution.envelope.usage,
|
|
552
|
+
execution.envelope.perf,
|
|
553
|
+
execution.metadata,
|
|
554
|
+
),
|
|
489
555
|
display: true,
|
|
490
|
-
details:
|
|
556
|
+
details: execution.envelope,
|
|
491
557
|
},
|
|
492
558
|
{ triggerTurn: false },
|
|
493
559
|
);
|
|
494
560
|
}
|
|
495
561
|
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|
|
498
588
|
|
|
499
589
|
pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
|
|
500
590
|
const details = message.details;
|
|
@@ -515,7 +605,18 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
515
605
|
ctx.ui.notify("Usage: /workflow:inspector [last]", "warning");
|
|
516
606
|
return;
|
|
517
607
|
}
|
|
518
|
-
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);
|
|
519
620
|
},
|
|
520
621
|
});
|
|
521
622
|
|
|
@@ -528,7 +629,8 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
528
629
|
ctx.ui.notify(`Invalid workflow option: ${direct.optionErrors.join("; ")}`, "warning");
|
|
529
630
|
return;
|
|
530
631
|
}
|
|
531
|
-
const
|
|
632
|
+
const directOptions = resolveWorkflowRunOptions(direct.options);
|
|
633
|
+
const perfRecorder = await createInvocationPerf(directOptions);
|
|
532
634
|
const { discoverWorkflows } = await loadDiscovery();
|
|
533
635
|
const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: direct.refreshDiscovery, perf: perfRecorder });
|
|
534
636
|
const available = [...workflows.keys()].join(", ") || "(none)";
|
|
@@ -550,7 +652,8 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
550
652
|
return;
|
|
551
653
|
}
|
|
552
654
|
|
|
553
|
-
|
|
655
|
+
const effectiveOptions = invocation === direct ? directOptions : resolveWorkflowRunOptions(invocation.options);
|
|
656
|
+
await sendResolvedWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, effectiveOptions, perfRecorder, reviewSessions);
|
|
554
657
|
},
|
|
555
658
|
});
|
|
556
659
|
|
|
@@ -572,6 +675,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
572
675
|
"When using `isolation: \"worktree\"`, `api.agent()` returns `{ result, patch, changed }`; use `.result` for the answer and `.patch` for the isolated diff.",
|
|
573
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.",
|
|
574
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,
|
|
575
679
|
"Every workflow tool call must provide exactly one of `name` or `script`, never both.",
|
|
576
680
|
],
|
|
577
681
|
parameters: Type.Object({
|
|
@@ -623,7 +727,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
623
727
|
};
|
|
624
728
|
}
|
|
625
729
|
|
|
626
|
-
const runOptions
|
|
730
|
+
const runOptions = resolveWorkflowRunOptions({
|
|
627
731
|
inspect: ctx.hasUI,
|
|
628
732
|
concurrency: params.concurrency,
|
|
629
733
|
parallelSubmissionLimit: params.parallelSubmissionLimit,
|
|
@@ -631,9 +735,9 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
631
735
|
perf: params.perf,
|
|
632
736
|
resumeFromRunId,
|
|
633
737
|
signal,
|
|
634
|
-
};
|
|
738
|
+
});
|
|
635
739
|
const perfRecorder = await createInvocationPerf(runOptions);
|
|
636
|
-
let mod:
|
|
740
|
+
let mod: LoadedWorkflow;
|
|
637
741
|
let resultName: string;
|
|
638
742
|
|
|
639
743
|
if (request.kind === "named") {
|
|
@@ -660,46 +764,21 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
660
764
|
resultName = mod.meta.name;
|
|
661
765
|
}
|
|
662
766
|
|
|
663
|
-
const { runWorkflow } = await loadEngine();
|
|
664
|
-
let perfSnapshot: PerfSnapshot | undefined;
|
|
665
|
-
let usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
666
|
-
let runMetadata: WorkflowRunMetadata | undefined;
|
|
667
|
-
let liveInspection: ActiveWorkflowInspection | undefined;
|
|
668
767
|
const resultArgs = params.args ?? "";
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
perf: runOptions.perf ?? perfRecorder !== undefined,
|
|
672
|
-
perfRecorder,
|
|
673
|
-
resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
|
|
674
|
-
onProgressSource(source) {
|
|
675
|
-
if (source) {
|
|
676
|
-
liveInspection = bindActiveWorkflowInspection(resultName, resultArgs, source);
|
|
677
|
-
activeWorkflowInspection = liveInspection;
|
|
678
|
-
} else if (activeWorkflowInspection === liveInspection) {
|
|
679
|
-
activeWorkflowInspection = undefined;
|
|
680
|
-
liveInspection = undefined;
|
|
681
|
-
}
|
|
682
|
-
},
|
|
683
|
-
onPerfSnapshot: (snapshot) => {
|
|
684
|
-
perfSnapshot = snapshot;
|
|
685
|
-
},
|
|
686
|
-
onUsageSnapshot: (snapshot) => {
|
|
687
|
-
usageSnapshot = snapshot;
|
|
688
|
-
},
|
|
689
|
-
onRunMetadata: (metadata) => {
|
|
690
|
-
runMetadata = metadata;
|
|
691
|
-
},
|
|
692
|
-
onProgressSnapshot: (snapshot) => {
|
|
693
|
-
// Record the run so /workflow:inspector can reopen it — tool-invoked (dynamax) runs were
|
|
694
|
-
// previously uninspectable, unlike the /workflow command path.
|
|
695
|
-
lastWorkflowInspection = { name: resultName, args: resultArgs, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
const perf = compactPerfSnapshot(perfSnapshot);
|
|
699
|
-
const detailLines = formatWorkflowDetailLines({ usage: usageSnapshot, perf, metadata: runMetadata });
|
|
768
|
+
const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
|
|
769
|
+
reviewSessions.remember(ctx, execution, runOptions);
|
|
700
770
|
return {
|
|
701
|
-
content: [{
|
|
702
|
-
|
|
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,
|
|
703
782
|
};
|
|
704
783
|
},
|
|
705
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
|
+
}
|