pi-workflow-engine 0.10.3 → 0.12.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 +418 -213
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +147 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +399 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +154 -530
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +368 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +87 -39
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +10 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +55 -3
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +230 -61
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +38 -55
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +70 -17
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -63
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +357 -75
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- 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 +10 -5
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +156 -66
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +96 -6
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +74 -122
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +701 -0
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -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 +18 -22
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +128 -56
- 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 +294 -0
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +150 -0
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +401 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +87 -25
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -70
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
- 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/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +30 -3
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +321 -43
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +62 -64
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +17 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +15 -16
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +15 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +82 -207
- package/USAGE.md +295 -21
- package/package.json +9 -9
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress.ts";
|
|
3
|
-
import {
|
|
2
|
+
import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
3
|
+
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
4
|
+
import { agentDetailParts, agentLabelColor, formatCount, formatDuration, statusIcon } from "./workflow-format.ts";
|
|
4
5
|
|
|
5
|
-
const MAX_WIDGET_LINES =
|
|
6
|
-
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
-
|
|
8
|
-
export interface WorkflowWidget {
|
|
9
|
-
nextFrame(): void;
|
|
10
|
-
render(width: number, theme: Theme): string[];
|
|
11
|
-
invalidate(): void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function createWorkflowWidget(snapshotProvider: () => WorkflowProgressSnapshot): WorkflowWidget {
|
|
15
|
-
return new LiveWorkflowWidget(snapshotProvider);
|
|
16
|
-
}
|
|
6
|
+
const MAX_WIDGET_LINES = 10;
|
|
17
7
|
|
|
18
8
|
export function renderWorkflowWidgetLines(
|
|
19
9
|
snapshot: WorkflowProgressSnapshot,
|
|
20
|
-
frame: number,
|
|
21
|
-
width: number,
|
|
22
10
|
theme: Theme,
|
|
23
11
|
): string[] {
|
|
24
|
-
const safeWidth = Math.max(1, width);
|
|
25
12
|
const counts = countAgents(snapshot.phases);
|
|
26
13
|
const active = counts.running + counts.queued;
|
|
27
14
|
const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
|
|
28
|
-
const
|
|
15
|
+
const activity = active > 0 ? theme.fg("accent", "●") : theme.fg("success", "✓");
|
|
29
16
|
|
|
30
17
|
const headingParts = [`${counts.done}/${counts.total} done`, elapsed];
|
|
31
18
|
if (counts.running > 0) headingParts.unshift(`${counts.running} running`);
|
|
@@ -33,10 +20,7 @@ export function renderWorkflowWidgetLines(
|
|
|
33
20
|
if (counts.failed > 0) headingParts.unshift(theme.fg("error", `${counts.failed} failed`));
|
|
34
21
|
|
|
35
22
|
const lines: string[] = [
|
|
36
|
-
|
|
37
|
-
`${spinner} ${theme.bold(snapshot.title)} ${theme.fg("dim", "·")} ${theme.fg("muted", snapshot.currentPhase)} ${theme.fg("dim", "·")} ${headingParts.join(` ${theme.fg("dim", "·")} `)}`,
|
|
38
|
-
safeWidth,
|
|
39
|
-
),
|
|
23
|
+
`${activity} ${theme.bold(snapshot.title)} ${theme.fg("dim", "·")} ${theme.fg("muted", snapshot.currentPhase)} ${theme.fg("dim", "·")} ${headingParts.join(` ${theme.fg("dim", "·")} `)}`,
|
|
40
24
|
];
|
|
41
25
|
|
|
42
26
|
const footer = footerLine(snapshot, theme);
|
|
@@ -46,53 +30,13 @@ export function renderWorkflowWidgetLines(
|
|
|
46
30
|
const visibleBodyLinesToRender = reserveHiddenLine ? body.lines.slice(0, Math.max(0, bodyBudget - 1)) : body.lines;
|
|
47
31
|
const hidden = body.hidden + (body.lines.length - visibleBodyLinesToRender.length);
|
|
48
32
|
|
|
49
|
-
|
|
33
|
+
lines.push(...visibleBodyLinesToRender);
|
|
50
34
|
if (hidden > 0 && lines.length < MAX_WIDGET_LINES) {
|
|
51
|
-
lines.push(
|
|
35
|
+
lines.push(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hidden} more`)}`);
|
|
52
36
|
}
|
|
53
|
-
if (footer && lines.length < MAX_WIDGET_LINES) lines.push(
|
|
54
|
-
|
|
55
|
-
return lines.slice(0, MAX_WIDGET_LINES).map((line) => truncateDisplay(line, safeWidth));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
class LiveWorkflowWidget implements WorkflowWidget {
|
|
59
|
-
private frame = 0;
|
|
60
|
-
private cachedWidth: number | undefined;
|
|
61
|
-
private cachedFrame: number | undefined;
|
|
62
|
-
private cachedDoneAt: number | undefined;
|
|
63
|
-
private cachedLines: string[] | undefined;
|
|
64
|
-
|
|
65
|
-
constructor(private readonly snapshotProvider: () => WorkflowProgressSnapshot) {}
|
|
37
|
+
if (footer && lines.length < MAX_WIDGET_LINES) lines.push(footer);
|
|
66
38
|
|
|
67
|
-
|
|
68
|
-
this.frame = (this.frame + 1) % SPINNER.length;
|
|
69
|
-
this.invalidate();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
render(width: number, theme: Theme): string[] {
|
|
73
|
-
const snapshot = this.snapshotProvider();
|
|
74
|
-
if (
|
|
75
|
-
this.cachedLines &&
|
|
76
|
-
this.cachedWidth === width &&
|
|
77
|
-
this.cachedFrame === this.frame &&
|
|
78
|
-
this.cachedDoneAt === snapshot.doneAt
|
|
79
|
-
) {
|
|
80
|
-
return this.cachedLines;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.cachedLines = renderWorkflowWidgetLines(snapshot, this.frame, width, theme);
|
|
84
|
-
this.cachedWidth = width;
|
|
85
|
-
this.cachedFrame = this.frame;
|
|
86
|
-
this.cachedDoneAt = snapshot.doneAt;
|
|
87
|
-
return this.cachedLines;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
invalidate(): void {
|
|
91
|
-
this.cachedWidth = undefined;
|
|
92
|
-
this.cachedFrame = undefined;
|
|
93
|
-
this.cachedDoneAt = undefined;
|
|
94
|
-
this.cachedLines = undefined;
|
|
95
|
-
}
|
|
39
|
+
return lines.slice(0, MAX_WIDGET_LINES);
|
|
96
40
|
}
|
|
97
41
|
|
|
98
42
|
interface AgentCounts {
|
|
@@ -173,9 +117,10 @@ function agentLine(agent: AgentRowSnapshot, theme: Theme): string {
|
|
|
173
117
|
}
|
|
174
118
|
|
|
175
119
|
function footerLine(snapshot: WorkflowProgressSnapshot, theme: Theme): string | undefined {
|
|
120
|
+
const usage = formatWorkflowUsageLine(snapshot.usage);
|
|
176
121
|
const counters = snapshot.counters.slice(0, 4).map((counter) => `${counter.label} ${formatCount(counter.value)}`);
|
|
177
122
|
const latestLog = snapshot.logs[snapshot.logs.length - 1];
|
|
178
|
-
const parts =
|
|
123
|
+
const parts = usage ? [usage, ...counters] : counters;
|
|
179
124
|
if (latestLog) parts.push(latestLog);
|
|
180
125
|
if (parts.length === 0) return undefined;
|
|
181
126
|
return `${theme.fg("dim", "└─")} ${theme.fg("dim", parts.join(" · "))}`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Read an unknown thrown value without allowing hostile coercion to mask the original failure. */
|
|
2
|
+
export function unknownErrorMessage(error: unknown): string {
|
|
3
|
+
try {
|
|
4
|
+
return error instanceof Error ? error.message : String(error);
|
|
5
|
+
} catch {
|
|
6
|
+
return "unknown error";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -6,12 +6,22 @@ export interface WorkflowUsageCost {
|
|
|
6
6
|
readonly total: number;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export type WorkflowUsageCoverage = "none" | "partial" | "complete";
|
|
10
|
+
|
|
11
|
+
export interface WorkflowUsageComponentCoverage {
|
|
12
|
+
readonly input: WorkflowUsageCoverage;
|
|
13
|
+
readonly output: WorkflowUsageCoverage;
|
|
14
|
+
readonly cacheRead: WorkflowUsageCoverage;
|
|
15
|
+
readonly cacheWrite: WorkflowUsageCoverage;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
export interface WorkflowUsageTotals {
|
|
10
19
|
readonly input: number;
|
|
11
20
|
readonly output: number;
|
|
12
21
|
readonly cacheRead: number;
|
|
13
22
|
readonly cacheWrite: number;
|
|
14
23
|
readonly totalTokens: number;
|
|
24
|
+
readonly coverage: WorkflowUsageComponentCoverage;
|
|
15
25
|
readonly cost: WorkflowUsageCost;
|
|
16
26
|
}
|
|
17
27
|
|
|
@@ -36,7 +46,21 @@ export interface WorkflowUsageSink {
|
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
const ZERO_COST: WorkflowUsageCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
39
|
-
const
|
|
49
|
+
const ZERO_COVERAGE: WorkflowUsageComponentCoverage = {
|
|
50
|
+
input: "none",
|
|
51
|
+
output: "none",
|
|
52
|
+
cacheRead: "none",
|
|
53
|
+
cacheWrite: "none",
|
|
54
|
+
};
|
|
55
|
+
const ZERO_TOTALS: WorkflowUsageTotals = {
|
|
56
|
+
input: 0,
|
|
57
|
+
output: 0,
|
|
58
|
+
cacheRead: 0,
|
|
59
|
+
cacheWrite: 0,
|
|
60
|
+
totalTokens: 0,
|
|
61
|
+
coverage: ZERO_COVERAGE,
|
|
62
|
+
cost: ZERO_COST,
|
|
63
|
+
};
|
|
40
64
|
|
|
41
65
|
interface AssistantUsageMessage {
|
|
42
66
|
readonly provider?: string;
|
|
@@ -47,6 +71,8 @@ interface AssistantUsageMessage {
|
|
|
47
71
|
export class WorkflowUsageRecorder implements WorkflowUsageSink {
|
|
48
72
|
private readonly agents: WorkflowAgentUsage[] = [];
|
|
49
73
|
|
|
74
|
+
constructor(private readonly onSnapshot?: (snapshot: WorkflowUsageSnapshot) => void) {}
|
|
75
|
+
|
|
50
76
|
recordAgentSession(input: { label: string; phase?: string; messages: readonly unknown[] }): void {
|
|
51
77
|
const assistantMessages = input.messages.flatMap((message) => {
|
|
52
78
|
const parsed = parseAssistantUsageMessage(message);
|
|
@@ -64,6 +90,7 @@ export class WorkflowUsageRecorder implements WorkflowUsageSink {
|
|
|
64
90
|
assistantMessages: assistantMessages.length,
|
|
65
91
|
usage,
|
|
66
92
|
});
|
|
93
|
+
this.onSnapshot?.(this.snapshot());
|
|
67
94
|
}
|
|
68
95
|
|
|
69
96
|
snapshot(): WorkflowUsageSnapshot {
|
|
@@ -79,8 +106,8 @@ export class WorkflowUsageRecorder implements WorkflowUsageSink {
|
|
|
79
106
|
}
|
|
80
107
|
}
|
|
81
108
|
|
|
82
|
-
export function createWorkflowUsageRecorder(): WorkflowUsageSink {
|
|
83
|
-
return new WorkflowUsageRecorder();
|
|
109
|
+
export function createWorkflowUsageRecorder(onSnapshot?: (snapshot: WorkflowUsageSnapshot) => void): WorkflowUsageSink {
|
|
110
|
+
return new WorkflowUsageRecorder(onSnapshot);
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
export function emptyWorkflowUsageTotals(): WorkflowUsageTotals {
|
|
@@ -96,19 +123,45 @@ export function isWorkflowUsageSnapshot(value: unknown): value is WorkflowUsageS
|
|
|
96
123
|
|
|
97
124
|
export function hasWorkflowUsage(snapshot: unknown): snapshot is WorkflowUsageSnapshot {
|
|
98
125
|
if (!isWorkflowUsageSnapshot(snapshot)) return false;
|
|
99
|
-
return snapshot.
|
|
126
|
+
return snapshot.totals.totalTokens > 0 || snapshot.totals.cost.total > 0;
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
export function formatWorkflowUsageLine(snapshot: unknown): string | undefined {
|
|
103
130
|
if (!hasWorkflowUsage(snapshot)) return undefined;
|
|
104
|
-
const parts =
|
|
105
|
-
if (snapshot.totals.cacheRead > 0) parts.push(`R${formatUsageCount(snapshot.totals.cacheRead)}`);
|
|
106
|
-
if (snapshot.totals.cacheWrite > 0) parts.push(`W${formatUsageCount(snapshot.totals.cacheWrite)}`);
|
|
131
|
+
const parts = formatWorkflowUsageComponents(snapshot.totals);
|
|
107
132
|
parts.push(`cost $${snapshot.totals.cost.total.toFixed(3)}`);
|
|
108
133
|
parts.push(`agents ${snapshot.agents.length}`);
|
|
109
134
|
return `Usage: ${parts.join(" · ")}`;
|
|
110
135
|
}
|
|
111
136
|
|
|
137
|
+
function formatWorkflowUsageComponents(totals: WorkflowUsageTotals): string[] {
|
|
138
|
+
const parts: string[] = [];
|
|
139
|
+
const completeCacheBreakdown =
|
|
140
|
+
totals.coverage.cacheRead === "complete" && totals.coverage.cacheWrite === "complete";
|
|
141
|
+
appendUsageComponent(parts, completeCacheBreakdown ? "fresh" : "input", totals.input, totals.coverage.input);
|
|
142
|
+
appendUsageComponent(parts, "cache read", totals.cacheRead, totals.coverage.cacheRead);
|
|
143
|
+
appendUsageComponent(parts, "cache write", totals.cacheWrite, totals.coverage.cacheWrite);
|
|
144
|
+
appendUsageComponent(parts, "output", totals.output, totals.coverage.output);
|
|
145
|
+
|
|
146
|
+
const knownComponents = totals.input + totals.output + totals.cacheRead + totals.cacheWrite;
|
|
147
|
+
if (parts.length === 0 && totals.totalTokens > 0) parts.push(`tokens ${formatUsageCount(totals.totalTokens)}`);
|
|
148
|
+
else if (!hasCompleteCoverage(totals.coverage) && totals.totalTokens !== knownComponents) {
|
|
149
|
+
parts.push(`total ${formatUsageCount(totals.totalTokens)}`);
|
|
150
|
+
}
|
|
151
|
+
return parts;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function appendUsageComponent(
|
|
155
|
+
parts: string[],
|
|
156
|
+
label: string,
|
|
157
|
+
value: number,
|
|
158
|
+
coverage: WorkflowUsageCoverage,
|
|
159
|
+
): void {
|
|
160
|
+
if (value <= 0 || coverage === "none") return;
|
|
161
|
+
const count = formatUsageCount(value);
|
|
162
|
+
parts.push(`${label} ${coverage === "partial" ? `≥${count}` : count}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
112
165
|
function isWorkflowAgentUsage(value: unknown): value is WorkflowAgentUsage {
|
|
113
166
|
if (!isRecord(value)) return false;
|
|
114
167
|
if (typeof value.label !== "string") return false;
|
|
@@ -126,9 +179,20 @@ function isWorkflowUsageTotals(value: unknown): value is WorkflowUsageTotals {
|
|
|
126
179
|
if (finiteNumber(value.cacheRead) === undefined) return false;
|
|
127
180
|
if (finiteNumber(value.cacheWrite) === undefined) return false;
|
|
128
181
|
if (finiteNumber(value.totalTokens) === undefined) return false;
|
|
182
|
+
if (!isWorkflowUsageCoverage(value.coverage)) return false;
|
|
129
183
|
return isWorkflowUsageCost(value.cost);
|
|
130
184
|
}
|
|
131
185
|
|
|
186
|
+
function isWorkflowUsageCoverage(value: unknown): value is WorkflowUsageComponentCoverage {
|
|
187
|
+
if (!isRecord(value)) return false;
|
|
188
|
+
return (
|
|
189
|
+
isCoverageValue(value.input) &&
|
|
190
|
+
isCoverageValue(value.output) &&
|
|
191
|
+
isCoverageValue(value.cacheRead) &&
|
|
192
|
+
isCoverageValue(value.cacheWrite)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
132
196
|
function isWorkflowUsageCost(value: unknown): value is WorkflowUsageCost {
|
|
133
197
|
if (!isRecord(value)) return false;
|
|
134
198
|
return (
|
|
@@ -158,14 +222,26 @@ function parseUsageTotals(value: unknown): WorkflowUsageTotals | undefined {
|
|
|
158
222
|
const output = finiteNumber(value.output);
|
|
159
223
|
const cacheRead = finiteNumber(value.cacheRead);
|
|
160
224
|
const cacheWrite = finiteNumber(value.cacheWrite);
|
|
225
|
+
const reportedTotal = finiteNumber(value.totalTokens);
|
|
161
226
|
const cost = parseUsageCost(value.cost);
|
|
162
|
-
if (
|
|
227
|
+
if (!cost) return undefined;
|
|
228
|
+
if (input === undefined && output === undefined && cacheRead === undefined && cacheWrite === undefined && reportedTotal === undefined) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const coverage: WorkflowUsageComponentCoverage = {
|
|
232
|
+
input: input === undefined ? "none" : "complete",
|
|
233
|
+
output: output === undefined ? "none" : "complete",
|
|
234
|
+
cacheRead: cacheRead === undefined ? "none" : "complete",
|
|
235
|
+
cacheWrite: cacheWrite === undefined ? "none" : "complete",
|
|
236
|
+
};
|
|
237
|
+
const componentTotal = (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
|
|
163
238
|
return {
|
|
164
|
-
input,
|
|
165
|
-
output,
|
|
166
|
-
cacheRead,
|
|
167
|
-
cacheWrite,
|
|
168
|
-
totalTokens:
|
|
239
|
+
input: input ?? 0,
|
|
240
|
+
output: output ?? 0,
|
|
241
|
+
cacheRead: cacheRead ?? 0,
|
|
242
|
+
cacheWrite: cacheWrite ?? 0,
|
|
243
|
+
totalTokens: hasCompleteCoverage(coverage) ? componentTotal : (reportedTotal ?? componentTotal),
|
|
244
|
+
coverage,
|
|
169
245
|
cost,
|
|
170
246
|
};
|
|
171
247
|
}
|
|
@@ -177,12 +253,20 @@ function parseUsageCost(value: unknown): WorkflowUsageCost | undefined {
|
|
|
177
253
|
const cacheRead = finiteNumber(value.cacheRead);
|
|
178
254
|
const cacheWrite = finiteNumber(value.cacheWrite);
|
|
179
255
|
const total = finiteNumber(value.total);
|
|
180
|
-
if (
|
|
181
|
-
return {
|
|
256
|
+
if (total === undefined) return undefined;
|
|
257
|
+
return {
|
|
258
|
+
input: input ?? 0,
|
|
259
|
+
output: output ?? 0,
|
|
260
|
+
cacheRead: cacheRead ?? 0,
|
|
261
|
+
cacheWrite: cacheWrite ?? 0,
|
|
262
|
+
total,
|
|
263
|
+
};
|
|
182
264
|
}
|
|
183
265
|
|
|
184
266
|
function sumTotals(values: readonly WorkflowUsageTotals[]): WorkflowUsageTotals {
|
|
185
|
-
|
|
267
|
+
const [first, ...rest] = values;
|
|
268
|
+
if (!first) return emptyWorkflowUsageTotals();
|
|
269
|
+
return rest.reduce(addTotals, cloneTotals(first));
|
|
186
270
|
}
|
|
187
271
|
|
|
188
272
|
function addTotals(left: WorkflowUsageTotals, right: WorkflowUsageTotals): WorkflowUsageTotals {
|
|
@@ -192,6 +276,12 @@ function addTotals(left: WorkflowUsageTotals, right: WorkflowUsageTotals): Workf
|
|
|
192
276
|
cacheRead: left.cacheRead + right.cacheRead,
|
|
193
277
|
cacheWrite: left.cacheWrite + right.cacheWrite,
|
|
194
278
|
totalTokens: left.totalTokens + right.totalTokens,
|
|
279
|
+
coverage: {
|
|
280
|
+
input: combineCoverage(left.coverage.input, right.coverage.input),
|
|
281
|
+
output: combineCoverage(left.coverage.output, right.coverage.output),
|
|
282
|
+
cacheRead: combineCoverage(left.coverage.cacheRead, right.coverage.cacheRead),
|
|
283
|
+
cacheWrite: combineCoverage(left.coverage.cacheWrite, right.coverage.cacheWrite),
|
|
284
|
+
},
|
|
195
285
|
cost: {
|
|
196
286
|
input: left.cost.input + right.cost.input,
|
|
197
287
|
output: left.cost.output + right.cost.output,
|
|
@@ -209,10 +299,28 @@ function cloneTotals(value: WorkflowUsageTotals): WorkflowUsageTotals {
|
|
|
209
299
|
cacheRead: value.cacheRead,
|
|
210
300
|
cacheWrite: value.cacheWrite,
|
|
211
301
|
totalTokens: value.totalTokens,
|
|
302
|
+
coverage: { ...value.coverage },
|
|
212
303
|
cost: { ...value.cost },
|
|
213
304
|
};
|
|
214
305
|
}
|
|
215
306
|
|
|
307
|
+
function combineCoverage(left: WorkflowUsageCoverage, right: WorkflowUsageCoverage): WorkflowUsageCoverage {
|
|
308
|
+
return left === right ? left : "partial";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function hasCompleteCoverage(coverage: WorkflowUsageComponentCoverage): boolean {
|
|
312
|
+
return (
|
|
313
|
+
coverage.input === "complete" &&
|
|
314
|
+
coverage.output === "complete" &&
|
|
315
|
+
coverage.cacheRead === "complete" &&
|
|
316
|
+
coverage.cacheWrite === "complete"
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isCoverageValue(value: unknown): value is WorkflowUsageCoverage {
|
|
321
|
+
return value === "none" || value === "partial" || value === "complete";
|
|
322
|
+
}
|
|
323
|
+
|
|
216
324
|
function formatUsageCount(count: number): string {
|
|
217
325
|
if (count < 1000) return count.toString();
|
|
218
326
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, type AdvisoryCandidate, type AdvisoryFinding, type AdvisoryLocation, type AdvisoryReport, type AdvisoryVerdict } from "./advisory-schema.ts";
|
|
2
2
|
import { compactResults } from "./concurrency.ts";
|
|
3
|
-
import type { AgentOptions, WorkflowApi, WorkflowProgressEvent } from "./types.ts";
|
|
3
|
+
import type { AgentOptions, WorkflowApi, WorkflowProgressEvent, WorkflowRunStats } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export interface AdvisoryLens {
|
|
6
6
|
label: string;
|
|
@@ -27,6 +27,26 @@ export const DEFAULT_ADVISORY_TOOLS: NonNullable<AgentOptions["tools"]> = ["read
|
|
|
27
27
|
/** Dynamically include installed grep/find/search-like extension tools. */
|
|
28
28
|
export const DEFAULT_ADVISORY_TOOL_HINTS: NonNullable<AgentOptions["toolHints"]> = ["search"];
|
|
29
29
|
|
|
30
|
+
export function emptyAdvisoryReport<Stats extends WorkflowRunStats>(
|
|
31
|
+
summary: string,
|
|
32
|
+
nextSteps: string[],
|
|
33
|
+
stats: Stats,
|
|
34
|
+
): AdvisoryReport & { stats: Stats } {
|
|
35
|
+
return { summary, findings: [], nextSteps, stats };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function publishVerifiedKeptProgress(
|
|
39
|
+
api: Pick<WorkflowApi, "progress" | "log">,
|
|
40
|
+
verified: number,
|
|
41
|
+
kept: number,
|
|
42
|
+
): void {
|
|
43
|
+
api.progress({ type: "counter", key: "verified", label: "verified", value: verified });
|
|
44
|
+
api.progress({ type: "counter", key: "kept", label: "kept", value: kept });
|
|
45
|
+
api.progress({ type: "summary", key: "verified", value: verified });
|
|
46
|
+
api.progress({ type: "summary", key: "kept", value: kept });
|
|
47
|
+
api.log(`${verified} verified → ${kept} kept`);
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
export type AdvisorySchedulingMode = "pipeline" | "finder-barrier";
|
|
31
51
|
|
|
32
52
|
export interface LensVerificationPipelineOptions<Lens extends AdvisoryLens, Verified extends AdvisoryVerified> {
|
|
@@ -85,7 +105,7 @@ export async function runLensVerificationPipeline<Lens extends AdvisoryLens, Ver
|
|
|
85
105
|
label: `find:${lens.label}`,
|
|
86
106
|
tools,
|
|
87
107
|
toolHints,
|
|
88
|
-
|
|
108
|
+
profile: "small",
|
|
89
109
|
schema: AdvisoryCandidatesSchema,
|
|
90
110
|
});
|
|
91
111
|
const candidates = (found?.candidates ?? []).slice(0, perLens);
|
|
@@ -122,7 +142,7 @@ export async function runLensVerificationPipeline<Lens extends AdvisoryLens, Ver
|
|
|
122
142
|
label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
123
143
|
tools,
|
|
124
144
|
toolHints,
|
|
125
|
-
|
|
145
|
+
profile: "small",
|
|
126
146
|
schema: AdvisoryVerdictSchema,
|
|
127
147
|
});
|
|
128
148
|
if (!judged) return null;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ResolvedWorkflowRunOptions } from "./options.ts";
|
|
3
|
+
import type { PerfAggregate, PerfSink, PerfSnapshot } from "./perf.ts";
|
|
4
|
+
import type { WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
5
|
+
import type {
|
|
6
|
+
LoadedWorkflow,
|
|
7
|
+
WorkflowProgressSource,
|
|
8
|
+
WorkflowRef,
|
|
9
|
+
WorkflowRunMetadata,
|
|
10
|
+
} from "./types.ts";
|
|
11
|
+
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
12
|
+
|
|
13
|
+
export interface WorkflowPerfDetails {
|
|
14
|
+
readonly enabled: boolean;
|
|
15
|
+
readonly startedAt: number;
|
|
16
|
+
readonly aggregates: readonly PerfAggregate[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkflowResultEnvelope {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly result: unknown;
|
|
22
|
+
readonly completedAt: number;
|
|
23
|
+
readonly usage?: WorkflowUsageSnapshot;
|
|
24
|
+
readonly perf?: WorkflowPerfDetails;
|
|
25
|
+
readonly runId?: string;
|
|
26
|
+
readonly resumedFromRunId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ResolvedWorkflowRunner = (
|
|
30
|
+
ctx: ExtensionContext,
|
|
31
|
+
mod: LoadedWorkflow,
|
|
32
|
+
args: string,
|
|
33
|
+
options: ResolvedWorkflowRunOptions,
|
|
34
|
+
) => Promise<unknown>;
|
|
35
|
+
|
|
36
|
+
export interface WorkflowExecutionInput {
|
|
37
|
+
readonly ctx: ExtensionContext;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly mod: LoadedWorkflow;
|
|
40
|
+
readonly args: string;
|
|
41
|
+
readonly options: ResolvedWorkflowRunOptions;
|
|
42
|
+
readonly perfRecorder?: PerfSink;
|
|
43
|
+
readonly runResolvedWorkflow: ResolvedWorkflowRunner;
|
|
44
|
+
readonly resolveWorkflow: (ref: WorkflowRef) => Promise<LoadedWorkflow>;
|
|
45
|
+
readonly onProgressSource?: (source: WorkflowProgressSource | undefined) => void | Promise<void>;
|
|
46
|
+
readonly onProgressSnapshot?: (snapshot: WorkflowProgressSnapshot) => void | Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WorkflowExecution {
|
|
50
|
+
readonly metadata?: WorkflowRunMetadata;
|
|
51
|
+
readonly envelope: WorkflowResultEnvelope;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Shared command/tool execution path. The engine remains lazy-loaded by the caller. */
|
|
55
|
+
export async function executeWorkflowInvocation(input: WorkflowExecutionInput): Promise<WorkflowExecution> {
|
|
56
|
+
let perfSnapshot: PerfSnapshot | undefined;
|
|
57
|
+
let usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
58
|
+
let runMetadata: WorkflowRunMetadata | undefined;
|
|
59
|
+
const { options } = input;
|
|
60
|
+
const runOptions: ResolvedWorkflowRunOptions = {
|
|
61
|
+
...options,
|
|
62
|
+
perfRecorder: input.perfRecorder,
|
|
63
|
+
resolveWorkflow: input.resolveWorkflow,
|
|
64
|
+
onProgressSource(source) {
|
|
65
|
+
return notifyLifecycleObservers(
|
|
66
|
+
() => input.onProgressSource?.(source),
|
|
67
|
+
() => options.onProgressSource?.(source),
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
onPerfSnapshot(snapshot) {
|
|
71
|
+
perfSnapshot = snapshot;
|
|
72
|
+
return notifyLifecycleObservers(() => options.onPerfSnapshot?.(snapshot));
|
|
73
|
+
},
|
|
74
|
+
onUsageSnapshot(snapshot) {
|
|
75
|
+
usageSnapshot = snapshot;
|
|
76
|
+
return notifyLifecycleObservers(() => options.onUsageSnapshot?.(snapshot));
|
|
77
|
+
},
|
|
78
|
+
onRunMetadata(metadata) {
|
|
79
|
+
runMetadata = metadata;
|
|
80
|
+
return notifyLifecycleObservers(() => options.onRunMetadata?.(metadata));
|
|
81
|
+
},
|
|
82
|
+
onProgressSnapshot(snapshot) {
|
|
83
|
+
return notifyLifecycleObservers(
|
|
84
|
+
() => input.onProgressSnapshot?.(snapshot),
|
|
85
|
+
() => options.onProgressSnapshot?.(snapshot),
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
const result = await input.runResolvedWorkflow(input.ctx, input.mod, input.args, runOptions);
|
|
90
|
+
const perf = compactPerfSnapshot(perfSnapshot);
|
|
91
|
+
return {
|
|
92
|
+
metadata: runMetadata,
|
|
93
|
+
envelope: {
|
|
94
|
+
name: input.name,
|
|
95
|
+
result,
|
|
96
|
+
completedAt: Date.now(),
|
|
97
|
+
usage: usageSnapshot,
|
|
98
|
+
perf,
|
|
99
|
+
runId: runMetadata?.runId,
|
|
100
|
+
resumedFromRunId: runMetadata?.resumedFromRunId,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDetails | undefined {
|
|
106
|
+
if (!snapshot?.enabled) return undefined;
|
|
107
|
+
return { enabled: true, startedAt: snapshot.startedAt, aggregates: snapshot.aggregates };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function notifyLifecycleObservers(...observers: ReadonlyArray<() => void | Promise<void>>): Promise<void> {
|
|
111
|
+
const failures: unknown[] = [];
|
|
112
|
+
for (const observer of observers) {
|
|
113
|
+
try {
|
|
114
|
+
await observer();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
failures.push(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (failures.length === 1) throw failures[0];
|
|
120
|
+
if (failures.length > 1) throw new AggregateError(failures, "Workflow lifecycle observers failed");
|
|
121
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
LoadedWorkflow,
|
|
3
|
+
WorkflowMeta,
|
|
4
|
+
WorkflowModule,
|
|
5
|
+
WorkflowSourceIdentity,
|
|
6
|
+
} from "./types.ts";
|
|
7
|
+
import type { WorktreeBaseline } from "./worktree.ts";
|
|
2
8
|
|
|
3
9
|
type WorkflowMetaCandidate = {
|
|
4
10
|
readonly name?: unknown;
|
|
@@ -30,6 +36,18 @@ export function parseWorkflowModule(value: unknown): { module: WorkflowModule }
|
|
|
30
36
|
return { module: { meta: parsedMeta.meta, default: candidate.default } };
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
/** Attach engine-owned source provenance after an authored module has been validated. */
|
|
40
|
+
export function loadWorkflow(
|
|
41
|
+
module: WorkflowModule,
|
|
42
|
+
source: WorkflowSourceIdentity,
|
|
43
|
+
isolatedWorktreeBaseline?: WorktreeBaseline,
|
|
44
|
+
): LoadedWorkflow {
|
|
45
|
+
const loaded = { meta: module.meta, default: module.default, source };
|
|
46
|
+
return isolatedWorktreeBaseline === undefined
|
|
47
|
+
? loaded
|
|
48
|
+
: { ...loaded, isolatedWorktreeBaseline };
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
function parseWorkflowMetaObject(meta: WorkflowMetaCandidate): { meta: WorkflowMeta } | { reason: string } {
|
|
34
52
|
if (typeof meta.name !== "string") return { reason: "meta.name must be a string" };
|
|
35
53
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { WorkflowBackgroundOrigin } from "./types.ts";
|
|
2
|
+
import type { WorkflowRunRecord } from "./workflow-run-record.ts";
|
|
3
|
+
|
|
4
|
+
const MAX_BACKGROUND_TEXT = 2_048;
|
|
5
|
+
|
|
6
|
+
export type WorkflowRunDelivery =
|
|
7
|
+
| { readonly state: "pending" }
|
|
8
|
+
| { readonly state: "delivered"; readonly deliveredAt: number }
|
|
9
|
+
| { readonly state: "unavailable"; readonly attemptedAt: number; readonly message: string };
|
|
10
|
+
|
|
11
|
+
export interface PersistedWorkflowBackground {
|
|
12
|
+
readonly origin: WorkflowBackgroundOrigin;
|
|
13
|
+
readonly delivery: WorkflowRunDelivery;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createPersistedWorkflowBackground(
|
|
17
|
+
origin: WorkflowBackgroundOrigin | undefined,
|
|
18
|
+
): PersistedWorkflowBackground | undefined {
|
|
19
|
+
if (!origin) return undefined;
|
|
20
|
+
return {
|
|
21
|
+
origin: {
|
|
22
|
+
sessionId: boundedText(origin.sessionId),
|
|
23
|
+
requestedAt: origin.requestedAt,
|
|
24
|
+
},
|
|
25
|
+
delivery: { state: "pending" },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function updateWorkflowRunDelivery(
|
|
30
|
+
record: WorkflowRunRecord,
|
|
31
|
+
delivery: WorkflowRunDelivery,
|
|
32
|
+
at = Date.now(),
|
|
33
|
+
): WorkflowRunRecord {
|
|
34
|
+
if (!record.background) throw new Error(`Workflow run ${record.runId} is not a background run.`);
|
|
35
|
+
return {
|
|
36
|
+
...record,
|
|
37
|
+
updatedAt: at,
|
|
38
|
+
background: {
|
|
39
|
+
origin: record.background.origin,
|
|
40
|
+
delivery: delivery.state === "unavailable"
|
|
41
|
+
? { ...delivery, message: boundedText(delivery.message) }
|
|
42
|
+
: delivery,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isPersistedWorkflowBackground(value: unknown): value is PersistedWorkflowBackground {
|
|
48
|
+
if (!isRecord(value) || !isRecord(value.origin) || !isRecord(value.delivery)) return false;
|
|
49
|
+
if (typeof value.origin.sessionId !== "string" || !isFiniteNumber(value.origin.requestedAt)) return false;
|
|
50
|
+
switch (value.delivery.state) {
|
|
51
|
+
case "pending":
|
|
52
|
+
return true;
|
|
53
|
+
case "delivered":
|
|
54
|
+
return isFiniteNumber(value.delivery.deliveredAt);
|
|
55
|
+
case "unavailable":
|
|
56
|
+
return isFiniteNumber(value.delivery.attemptedAt) && typeof value.delivery.message === "string";
|
|
57
|
+
default:
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function boundedText(value: string): string {
|
|
63
|
+
return value.length <= MAX_BACKGROUND_TEXT ? value : `${value.slice(0, MAX_BACKGROUND_TEXT - 1)}…`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
67
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
71
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
72
|
+
}
|