pi-workflow-engine 0.13.0 → 1.0.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 +106 -391
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +8 -8
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +0 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +7 -9
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -129
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +56 -19
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +48 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/USAGE.md +31 -17
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -24
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, type AdvisoryCandidate, type IdentifiedAdvisoryCandidate, type AdvisoryLocation, type AdvisoryReport, type AdvisoryVerdict } from "./advisory-schema.ts";
|
|
1
|
+
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, formatAdvisoryLocation, type AdvisoryCandidate, type AdvisoryFinding, type IdentifiedAdvisoryCandidate, type AdvisoryLocation, type AdvisoryReport, type AdvisoryVerdict } from "./advisory-schema.ts";
|
|
2
2
|
import { AdvisorySynthesisSchema, SYNTHESIS_ID_INSTRUCTIONS, withAdvisoryCoverage, collectAdvisoryStage, dedupeCandidates, identifyCandidates, uniqueLocations, type AdvisoryStageCoverage, type AdvisorySynthesis } from "./advisory-evidence.ts";
|
|
3
3
|
import type { AgentOptions, WorkflowApi, WorkflowProgressEvent, WorkflowRunStats } from "./types.ts";
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
export interface AdvisoryLens {
|
|
6
7
|
label: string;
|
|
7
8
|
category: string;
|
|
@@ -11,7 +12,6 @@ export interface AdvisoryLens {
|
|
|
11
12
|
export type AdvisoryVerified = IdentifiedAdvisoryCandidate & {
|
|
12
13
|
verdict: AdvisoryVerdict["verdict"];
|
|
13
14
|
evidence: string[];
|
|
14
|
-
confidence?: AdvisoryVerdict["confidence"];
|
|
15
15
|
challenge?: import("./advisory-challenge.ts").ChallengeRecord;
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -19,7 +19,6 @@ export interface LensVerificationPipelineResult {
|
|
|
19
19
|
verified: AdvisoryVerified[];
|
|
20
20
|
rawCandidates: number;
|
|
21
21
|
dropped: number;
|
|
22
|
-
refuted: number;
|
|
23
22
|
coverage: AdvisoryStageCoverage[];
|
|
24
23
|
}
|
|
25
24
|
|
|
@@ -71,7 +70,6 @@ export async function runLensVerificationPipeline(
|
|
|
71
70
|
const { api, lenses, perLens, finderPhase = "Find", finderPrompt, verifierPrompt, boundCandidate } = options;
|
|
72
71
|
const coverage: AdvisoryStageCoverage[] = [];
|
|
73
72
|
let rawCandidates = 0;
|
|
74
|
-
let refuted = 0;
|
|
75
73
|
api.phase(finderPhase);
|
|
76
74
|
const found = await collectAdvisoryStage(api, "Find", lenses.map((lens) => ({ id: lens.label, run: async () => {
|
|
77
75
|
const result = await api.agent(finderPrompt(lens), {
|
|
@@ -107,11 +105,59 @@ export async function runLensVerificationPipeline(
|
|
|
107
105
|
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
108
106
|
profile: "small", schema: AdvisoryVerdictSchema,
|
|
109
107
|
});
|
|
110
|
-
recordVerdictProgress(api.progress, candidate, judged
|
|
111
|
-
return { ...candidate, verdict: judged.verdict, evidence: judged.evidence
|
|
108
|
+
recordVerdictProgress(api.progress, candidate, judged);
|
|
109
|
+
return { ...candidate, verdict: judged.verdict, evidence: judged.evidence };
|
|
112
110
|
},
|
|
113
111
|
})), coverage);
|
|
114
|
-
return { verified, rawCandidates, dropped,
|
|
112
|
+
return { verified, rawCandidates, dropped, coverage };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Stats for a lens review that stopped before discovery. */
|
|
116
|
+
export const EMPTY_LENS_REVIEW_STATS: Readonly<WorkflowRunStats> = Object.freeze({ files: 0, candidates: 0, verified: 0, kept: 0, dropped: 0, refuted: 0 });
|
|
117
|
+
|
|
118
|
+
export interface LensReviewConclusion {
|
|
119
|
+
/** Scoped file count reported in stats. */
|
|
120
|
+
files: number;
|
|
121
|
+
rank(finding: AdvisoryVerified): number;
|
|
122
|
+
/** One ranked finding's synthesis-prompt block, headed by the IDs synthesis selects; defaults to {@link formatRankedFinding}. */
|
|
123
|
+
formatFinding?(finding: AdvisoryVerified, index: number): string;
|
|
124
|
+
/** Report used when no finding survives verification and challenge. */
|
|
125
|
+
empty: { summary: string; nextSteps: string[] };
|
|
126
|
+
synthesisPrompt(block: string, ranked: readonly AdvisoryVerified[], refuted: readonly AdvisoryVerified[]): string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatRankedFinding(finding: AdvisoryVerified, index: number, recommendationLabel = "Recommendation"): string {
|
|
130
|
+
return `### [${index}] IDs: ${finding.sourceCandidateIds.join(", ")} ${formatLocation(finding)} (${finding.verdict}, ${finding.category})\n` +
|
|
131
|
+
`${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}\n${recommendationLabel}: ${finding.recommendation ?? "(none supplied)"}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Rank and synthesize the findings that survived verification and challenge into the final report. */
|
|
135
|
+
export async function concludeLensReview(
|
|
136
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase" | "progress" | "log">,
|
|
137
|
+
pipeline: LensVerificationPipelineResult,
|
|
138
|
+
verified: AdvisoryVerified[],
|
|
139
|
+
conclusion: LensReviewConclusion,
|
|
140
|
+
) {
|
|
141
|
+
const { coverage } = pipeline;
|
|
142
|
+
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
143
|
+
const stats: WorkflowRunStats = {
|
|
144
|
+
files: conclusion.files,
|
|
145
|
+
candidates: pipeline.rawCandidates,
|
|
146
|
+
verified: verified.length,
|
|
147
|
+
kept: surviving.length,
|
|
148
|
+
dropped: pipeline.dropped,
|
|
149
|
+
refuted: verified.length - surviving.length,
|
|
150
|
+
};
|
|
151
|
+
publishVerifiedKeptProgress(api, verified.length, surviving.length);
|
|
152
|
+
if (surviving.length === 0) {
|
|
153
|
+
return finishAdvisoryReport(emptyAdvisoryReport(conclusion.empty.summary, conclusion.empty.nextSteps, stats), coverage, verified);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const ranked = [...surviving].sort((a, b) => conclusion.rank(a) - conclusion.rank(b));
|
|
157
|
+
const block = ranked.map((finding, index) => (conclusion.formatFinding ?? formatRankedFinding)(finding, index)).join("\n\n");
|
|
158
|
+
const refuted = verified.filter((finding) => finding.verdict === "REFUTED");
|
|
159
|
+
const resolved = await synthesizeAdvisoryReport(api, conclusion.synthesisPrompt(block, ranked, refuted), ranked, coverage);
|
|
160
|
+
return finishAdvisoryReport({ ...resolved, stats: { ...stats, kept: resolved.findings.length } }, coverage, verified);
|
|
115
161
|
}
|
|
116
162
|
|
|
117
163
|
export async function synthesizeAdvisoryReport(
|
|
@@ -123,7 +169,7 @@ export async function synthesizeAdvisoryReport(
|
|
|
123
169
|
api.phase("Synthesize");
|
|
124
170
|
const [report] = await collectAdvisoryStage(api, "Synthesize", [{ id: "synthesize", run: () => api.agent(
|
|
125
171
|
SYNTHESIS_ID_INSTRUCTIONS + prompt,
|
|
126
|
-
{ phase: "Synthesize", label: "synthesize", tools: [], profile: "medium", resume: "read-only", schema: AdvisorySynthesisSchema },
|
|
172
|
+
{ phase: "Synthesize", label: "synthesize", tools: [], skills: [], profile: "medium", resume: "read-only", schema: AdvisorySynthesisSchema },
|
|
127
173
|
) }], coverage);
|
|
128
174
|
return resolveAdvisorySynthesis(report, ranked, {
|
|
129
175
|
impact: "Impact not restated by verification.",
|
|
@@ -142,27 +188,20 @@ export function primaryLocation(candidate: Pick<AdvisoryCandidate, "locations">)
|
|
|
142
188
|
}
|
|
143
189
|
|
|
144
190
|
export function formatLocation(candidate: Pick<AdvisoryCandidate, "locations">): string {
|
|
145
|
-
|
|
146
|
-
const line = location.line != null ? `:${location.line}` : "";
|
|
147
|
-
const symbol = location.symbol ? ` (${location.symbol})` : "";
|
|
148
|
-
return `${location.file}${line}${symbol}`;
|
|
191
|
+
return formatAdvisoryLocation(primaryLocation(candidate));
|
|
149
192
|
}
|
|
150
193
|
|
|
151
194
|
export function formatEvidence(evidence: readonly string[]): string {
|
|
152
195
|
return evidence.join("; ");
|
|
153
196
|
}
|
|
154
197
|
|
|
155
|
-
export function normalizePath(path: string): string {
|
|
156
|
-
return path.replace(/^\.\//, "").replace(/^[ab]\//, "");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
198
|
const VERDICT_PRESENTATION = {
|
|
160
199
|
CONFIRMED: { lane: "Confirmed", status: "success", confidence: "high" },
|
|
161
200
|
PLAUSIBLE: { lane: "Plausible", status: "warning", confidence: "medium" },
|
|
162
201
|
NOT_SUBSTANTIATED: { lane: "Unresolved", status: "warning", confidence: "medium" },
|
|
163
202
|
REFUTED: { lane: "Refuted", status: "error", confidence: "low" },
|
|
164
203
|
} satisfies Record<AdvisoryVerdict["verdict"], {
|
|
165
|
-
lane: string; status: "success" | "warning" | "error"; confidence:
|
|
204
|
+
lane: string; status: "success" | "warning" | "error"; confidence: AdvisoryFinding["confidence"];
|
|
166
205
|
}>;
|
|
167
206
|
|
|
168
207
|
export function verdictConfidence(verdict: AdvisoryVerdict["verdict"]): "high" | "medium" | "low" {
|
|
@@ -173,11 +212,9 @@ export function recordVerdictProgress(
|
|
|
173
212
|
progress: (event: WorkflowProgressEvent) => void,
|
|
174
213
|
candidate: Pick<AdvisoryCandidate, "locations" | "summary">,
|
|
175
214
|
verdict: Pick<AdvisoryVerdict, "verdict" | "evidence">,
|
|
176
|
-
onRefuted?: () => void,
|
|
177
215
|
): void {
|
|
178
216
|
progress({ type: "counter_delta", key: `verdict.${verdict.verdict.toLowerCase()}`, label: verdict.verdict, delta: 1 });
|
|
179
217
|
if (verdict.verdict === "REFUTED") {
|
|
180
|
-
onRefuted?.();
|
|
181
218
|
progress({ type: "counter_delta", key: "refuted", label: "refuted", delta: 1 });
|
|
182
219
|
}
|
|
183
220
|
progress({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isRecord } from "./guards.ts";
|
|
2
3
|
import type { ResolvedWorkflowRunOptions } from "./options.ts";
|
|
3
4
|
import type { PerfAggregate, PerfSink, PerfSnapshot } from "./perf.ts";
|
|
4
5
|
import type { WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
@@ -11,14 +12,14 @@ import type {
|
|
|
11
12
|
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
12
13
|
|
|
13
14
|
export interface WorkflowPerfDetails {
|
|
14
|
-
readonly enabled: boolean;
|
|
15
|
-
readonly startedAt: number;
|
|
16
15
|
readonly aggregates: readonly PerfAggregate[];
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export interface WorkflowResultEnvelope {
|
|
20
19
|
readonly name: string;
|
|
21
20
|
readonly result: unknown;
|
|
21
|
+
/** Optional so envelopes persisted before it existed still render. */
|
|
22
|
+
readonly startedAt?: number;
|
|
22
23
|
readonly completedAt: number;
|
|
23
24
|
readonly usage?: WorkflowUsageSnapshot;
|
|
24
25
|
readonly perf?: WorkflowPerfDetails;
|
|
@@ -26,6 +27,12 @@ export interface WorkflowResultEnvelope {
|
|
|
26
27
|
readonly resumedFromRunId?: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/** A workflow result's own summary: the result itself when it is a string, else its string `summary` field. */
|
|
31
|
+
export function workflowResultSummary(result: unknown): string | undefined {
|
|
32
|
+
if (typeof result === "string") return result;
|
|
33
|
+
return isRecord(result) && typeof result.summary === "string" ? result.summary : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
export type ResolvedWorkflowRunner = (
|
|
30
37
|
ctx: ExtensionContext,
|
|
31
38
|
mod: LoadedWorkflow,
|
|
@@ -46,13 +53,8 @@ export interface WorkflowExecutionInput {
|
|
|
46
53
|
readonly onProgressSnapshot?: (snapshot: WorkflowProgressSnapshot) => void | Promise<void>;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
export interface WorkflowExecution {
|
|
50
|
-
readonly metadata?: WorkflowRunMetadata;
|
|
51
|
-
readonly envelope: WorkflowResultEnvelope;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
56
|
/** Shared command/tool execution path. The engine remains lazy-loaded by the caller. */
|
|
55
|
-
export async function executeWorkflowInvocation(input: WorkflowExecutionInput): Promise<
|
|
57
|
+
export async function executeWorkflowInvocation(input: WorkflowExecutionInput): Promise<WorkflowResultEnvelope> {
|
|
56
58
|
let perfSnapshot: PerfSnapshot | undefined;
|
|
57
59
|
let usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
58
60
|
let runMetadata: WorkflowRunMetadata | undefined;
|
|
@@ -86,27 +88,20 @@ export async function executeWorkflowInvocation(input: WorkflowExecutionInput):
|
|
|
86
88
|
);
|
|
87
89
|
},
|
|
88
90
|
};
|
|
91
|
+
const startedAt = Date.now();
|
|
89
92
|
const result = await input.runResolvedWorkflow(input.ctx, input.mod, input.args, runOptions);
|
|
90
|
-
const perf = compactPerfSnapshot(perfSnapshot);
|
|
91
93
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
resumedFromRunId: runMetadata?.resumedFromRunId,
|
|
101
|
-
},
|
|
94
|
+
name: input.name,
|
|
95
|
+
result,
|
|
96
|
+
startedAt,
|
|
97
|
+
completedAt: Date.now(),
|
|
98
|
+
usage: usageSnapshot,
|
|
99
|
+
perf: perfSnapshot?.enabled ? { aggregates: perfSnapshot.aggregates } : undefined,
|
|
100
|
+
runId: runMetadata?.runId,
|
|
101
|
+
resumedFromRunId: runMetadata?.resumedFromRunId,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
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
105
|
async function notifyLifecycleObservers(...observers: ReadonlyArray<() => void | Promise<void>>): Promise<void> {
|
|
111
106
|
const failures: unknown[] = [];
|
|
112
107
|
for (const observer of observers) {
|
|
@@ -5,35 +5,33 @@ import type {
|
|
|
5
5
|
WorkflowSourceIdentity,
|
|
6
6
|
} from "./types.ts";
|
|
7
7
|
import type { WorktreeBaseline } from "./worktree.ts";
|
|
8
|
+
import { isRecord } from "./guards.ts";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
10
|
+
/** Validate and normalize workflow metadata without executing workflow code. */
|
|
11
|
+
export function parseWorkflowMeta(meta: unknown): { meta: WorkflowMeta } | { reason: string } {
|
|
12
|
+
if (!isRecord(meta)) return { reason: "meta export must be an object" };
|
|
13
|
+
if (typeof meta.name !== "string") return { reason: "meta.name must be a string" };
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
readonly default?: unknown;
|
|
18
|
-
};
|
|
15
|
+
const description = meta.description;
|
|
16
|
+
if (description !== undefined && typeof description !== "string") return { reason: "meta.description must be a string when provided" };
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
const phases = meta.phases;
|
|
19
|
+
if (phases !== undefined && !isWorkflowPhases(phases)) return { reason: "meta.phases must be an array of { title: string }" };
|
|
20
|
+
|
|
21
|
+
const workflowMeta: WorkflowMeta = { name: meta.name, description: description ?? "" };
|
|
22
|
+
if (phases !== undefined) workflowMeta.phases = phases;
|
|
23
|
+
return { meta: workflowMeta };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function parseWorkflowModule(value: unknown): { module: WorkflowModule } | { reason: string } {
|
|
27
27
|
if (!isRecord(value)) return { reason: "module export is not an object" };
|
|
28
|
-
|
|
29
|
-
const meta = candidate.meta;
|
|
30
|
-
if (!isRecord(meta)) return { reason: "missing meta export" };
|
|
28
|
+
if (value.meta === undefined) return { reason: "missing meta export" };
|
|
31
29
|
|
|
32
|
-
const parsedMeta =
|
|
30
|
+
const parsedMeta = parseWorkflowMeta(value.meta);
|
|
33
31
|
if ("reason" in parsedMeta) return parsedMeta;
|
|
34
|
-
if (!isWorkflowRun(
|
|
32
|
+
if (!isWorkflowRun(value.default)) return { reason: "default export must be a function" };
|
|
35
33
|
|
|
36
|
-
return { module: { meta: parsedMeta.meta, default:
|
|
34
|
+
return { module: { meta: parsedMeta.meta, default: value.default } };
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
/** Attach engine-owned source provenance after an authored module has been validated. */
|
|
@@ -48,20 +46,6 @@ export function loadWorkflow(
|
|
|
48
46
|
: { ...loaded, isolatedWorktreeBaseline };
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
function parseWorkflowMetaObject(meta: WorkflowMetaCandidate): { meta: WorkflowMeta } | { reason: string } {
|
|
52
|
-
if (typeof meta.name !== "string") return { reason: "meta.name must be a string" };
|
|
53
|
-
|
|
54
|
-
const description = meta.description;
|
|
55
|
-
if (description !== undefined && typeof description !== "string") return { reason: "meta.description must be a string when provided" };
|
|
56
|
-
|
|
57
|
-
const phases = meta.phases;
|
|
58
|
-
if (phases !== undefined && !isWorkflowPhases(phases)) return { reason: "meta.phases must be an array of { title: string }" };
|
|
59
|
-
|
|
60
|
-
const workflowMeta: WorkflowMeta = { name: meta.name, description: description ?? "" };
|
|
61
|
-
if (phases !== undefined) workflowMeta.phases = phases;
|
|
62
|
-
return { meta: workflowMeta };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
49
|
function isWorkflowRun(value: unknown): value is WorkflowModule["default"] {
|
|
66
50
|
return typeof value === "function";
|
|
67
51
|
}
|
|
@@ -69,7 +53,3 @@ function isWorkflowRun(value: unknown): value is WorkflowModule["default"] {
|
|
|
69
53
|
function isWorkflowPhases(value: unknown): value is Array<{ title: string }> {
|
|
70
54
|
return Array.isArray(value) && value.every((phase) => isRecord(phase) && typeof phase.title === "string");
|
|
71
55
|
}
|
|
72
|
-
|
|
73
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
74
|
-
return typeof value === "object" && value !== null;
|
|
75
|
-
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { formatAdvisoryLocation, isAdvisoryReport, type AdvisoryFinding, type AdvisoryReport } from "./advisory-schema.ts";
|
|
2
|
+
import { isRecord } from "./guards.ts";
|
|
3
|
+
import { toReviewIssues } from "./review/review-issues.ts";
|
|
4
|
+
import { formatPerfSummary } from "./perf.ts";
|
|
5
|
+
import { prettyJson } from "./text.ts";
|
|
6
|
+
import { formatWorkflowUsageLine } from "./usage.ts";
|
|
7
|
+
import { workflowResultSummary, type WorkflowResultEnvelope } from "./workflow-execution.ts";
|
|
8
|
+
|
|
9
|
+
/** Cap on the result JSON copied into the host agent's context; the run record keeps the full value. */
|
|
10
|
+
const MAX_CONTEXT_RESULT_JSON_CHARS = 20_000;
|
|
11
|
+
|
|
12
|
+
/** The host model only sees this text, never the envelope `details`, so it carries every result field. */
|
|
13
|
+
export function formatWorkflowResultForContext(envelope: WorkflowResultEnvelope): string {
|
|
14
|
+
const details = formatWorkflowDetailLines(envelope);
|
|
15
|
+
return `## Workflow: ${envelope.name}\n\n${formatResultForContext(envelope.result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Run id, usage and perf lines appended for the host model. */
|
|
19
|
+
function formatWorkflowDetailLines(envelope: WorkflowResultEnvelope): string[] {
|
|
20
|
+
const run = envelope.runId
|
|
21
|
+
? envelope.resumedFromRunId ? `Run: ${envelope.runId} (resumed from ${envelope.resumedFromRunId})` : `Run: ${envelope.runId}`
|
|
22
|
+
: undefined;
|
|
23
|
+
return [
|
|
24
|
+
run,
|
|
25
|
+
formatWorkflowUsageLine(envelope.usage),
|
|
26
|
+
envelope.perf ? formatPerfSummary(envelope.perf.aggregates) : undefined,
|
|
27
|
+
].filter((line): line is string => line !== undefined);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatResultForContext(result: unknown): string {
|
|
31
|
+
if (typeof result === "string") return result;
|
|
32
|
+
if (isAdvisoryReport(result)) return formatAdvisoryReportForContext(result);
|
|
33
|
+
const summary = workflowResultSummary(result);
|
|
34
|
+
if (summary !== undefined && isRecord(result) && Object.keys(result).length === 1) return summary;
|
|
35
|
+
const json = formatResultJson(result);
|
|
36
|
+
if (json === undefined) return summary ?? "Workflow finished.";
|
|
37
|
+
return summary === undefined ? json : `${summary}\n\n${json}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatAdvisoryReportForContext(report: AdvisoryReport): string {
|
|
41
|
+
const lines = [report.summary];
|
|
42
|
+
const issues = toReviewIssues(report);
|
|
43
|
+
if (issues.length > 0) {
|
|
44
|
+
lines.push("", "Findings:");
|
|
45
|
+
for (const { id, finding } of issues) {
|
|
46
|
+
lines.push(
|
|
47
|
+
`\n### ${id}: ${finding.summary}`,
|
|
48
|
+
`- Severity: ${finding.severity}`,
|
|
49
|
+
`- Confidence: ${finding.confidence}`,
|
|
50
|
+
`- Category: ${finding.category}`,
|
|
51
|
+
`- Location: ${formatFindingLocations(finding)}`,
|
|
52
|
+
`- Impact: ${finding.impact}`,
|
|
53
|
+
`- Evidence: ${finding.evidence.length > 0 ? finding.evidence.join("; ") : "(none cited)"}`,
|
|
54
|
+
`- Recommendation: ${finding.recommendation}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (report.gaps && report.gaps.length > 0) {
|
|
59
|
+
lines.push("", "Coverage gaps:", ...report.gaps.map((gap) => `- ${gap}`));
|
|
60
|
+
}
|
|
61
|
+
if (report.nextSteps.length > 0) {
|
|
62
|
+
lines.push("", "Next steps:", ...report.nextSteps.map((step) => `- ${step}`));
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatResultJson(result: unknown): string | undefined {
|
|
68
|
+
if (result === null || result === undefined) return undefined;
|
|
69
|
+
const json = prettyJson(result);
|
|
70
|
+
if (json === undefined) return undefined;
|
|
71
|
+
if (json.length <= MAX_CONTEXT_RESULT_JSON_CHARS) return `Result:\n\`\`\`json\n${json}\n\`\`\``;
|
|
72
|
+
return `Result (first ${MAX_CONTEXT_RESULT_JSON_CHARS} of ${json.length} characters; truncated, the run record holds the full value):\n\`\`\`json\n${json.slice(0, MAX_CONTEXT_RESULT_JSON_CHARS)}\n\`\`\``;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The review anchor that the findings viewer and PR comments cite comes first, then every other cited location. */
|
|
76
|
+
function formatFindingLocations(finding: AdvisoryFinding): string {
|
|
77
|
+
const locations = finding.reviewAnchor ? [finding.reviewAnchor, ...finding.locations] : finding.locations;
|
|
78
|
+
return [...new Set(locations.map(formatAdvisoryLocation))].join(", ");
|
|
79
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isFiniteNumber, isRecord } from "./guards.ts";
|
|
2
|
+
import { truncateText } from "./text.ts";
|
|
1
3
|
import type { WorkflowBackgroundOrigin } from "./types.ts";
|
|
2
4
|
import type { WorkflowRunRecord } from "./workflow-run-record.ts";
|
|
3
5
|
|
|
@@ -19,7 +21,7 @@ export function createPersistedWorkflowBackground(
|
|
|
19
21
|
if (!origin) return undefined;
|
|
20
22
|
return {
|
|
21
23
|
origin: {
|
|
22
|
-
sessionId:
|
|
24
|
+
sessionId: truncateText(origin.sessionId, MAX_BACKGROUND_TEXT),
|
|
23
25
|
requestedAt: origin.requestedAt,
|
|
24
26
|
},
|
|
25
27
|
delivery: { state: "pending" },
|
|
@@ -38,7 +40,7 @@ export function updateWorkflowRunDelivery(
|
|
|
38
40
|
background: {
|
|
39
41
|
origin: record.background.origin,
|
|
40
42
|
delivery: delivery.state === "unavailable"
|
|
41
|
-
? { ...delivery, message:
|
|
43
|
+
? { ...delivery, message: truncateText(delivery.message, MAX_BACKGROUND_TEXT) }
|
|
42
44
|
: delivery,
|
|
43
45
|
},
|
|
44
46
|
};
|
|
@@ -58,15 +60,3 @@ export function isPersistedWorkflowBackground(value: unknown): value is Persiste
|
|
|
58
60
|
return false;
|
|
59
61
|
}
|
|
60
62
|
}
|
|
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
|
-
}
|