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
|
@@ -12,14 +12,17 @@ import {
|
|
|
12
12
|
formatLocation,
|
|
13
13
|
normalizePath,
|
|
14
14
|
primaryLocation,
|
|
15
|
+
publishVerifiedKeptProgress,
|
|
15
16
|
recordVerdictProgress,
|
|
16
17
|
verdictConfidence,
|
|
17
18
|
DEFAULT_ADVISORY_TOOL_HINTS,
|
|
18
19
|
DEFAULT_ADVISORY_TOOLS,
|
|
19
20
|
} from "../src/workflow-advisory-utils.ts";
|
|
20
21
|
import { compactResults } from "../src/concurrency.ts";
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
22
|
+
import { formatReviewDiffTarget, parseAllowedDiffCommand } from "../src/review-diff-target.ts";
|
|
23
|
+
import { buildCodeReviewScopeBlock, dedupeCodeReviewCandidates } from "../src/review/code-review-orchestration.ts";
|
|
24
|
+
import type { ReviewContext } from "../src/review/review-report.ts";
|
|
25
|
+
import { captureReviewMaterial, type ReviewMaterialCaptureResult } from "../src/review/review-snapshot.ts";
|
|
23
26
|
import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
|
|
24
27
|
|
|
25
28
|
export const meta: WorkflowMeta = {
|
|
@@ -61,7 +64,6 @@ const ANGLES: Angle[] = [
|
|
|
61
64
|
const TOOLS = DEFAULT_ADVISORY_TOOLS;
|
|
62
65
|
const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
|
|
63
66
|
const PER_ANGLE = 6;
|
|
64
|
-
const DIFF_EMBED_CAP = 60_000;
|
|
65
67
|
|
|
66
68
|
/** Parse a unified diff into the set of added/changed new-file line numbers per file. */
|
|
67
69
|
export function changedLines(diff: string): Map<string, Set<number>> {
|
|
@@ -95,7 +97,15 @@ export function inDiff(changed: Map<string, Set<number>>, file: string, line?: n
|
|
|
95
97
|
return set.has(line) || set.has(line - 1) || set.has(line + 1);
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
export
|
|
100
|
+
export interface CodeReviewDependencies {
|
|
101
|
+
readonly captureReviewMaterial?: (
|
|
102
|
+
target: Parameters<typeof captureReviewMaterial>[0],
|
|
103
|
+
cwd: string,
|
|
104
|
+
signal?: AbortSignal,
|
|
105
|
+
) => Promise<ReviewMaterialCaptureResult>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default async function run(api: WorkflowApi, dependencies: CodeReviewDependencies = {}): Promise<unknown> {
|
|
99
109
|
const { agent, parallel, phase, log, progress, args, cwd, signal } = api;
|
|
100
110
|
const target = args.trim();
|
|
101
111
|
let fileCount = 0;
|
|
@@ -116,6 +126,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
116
126
|
(target
|
|
117
127
|
? `Target / instructions (verbatim): "${target}". If it names a PR number, branch, ref range, or files, build the matching diff command (use 'gh pr diff <number>' for a PR). Otherwise use the default selection below.\n`
|
|
118
128
|
: "No explicit target — select the diff to review using the default below.\n") +
|
|
129
|
+
"Canonical Git syntax: use `git diff -- <path> [<path>...]` for file paths; use one `A..B` or `A...B` range operand for two revisions. Never emit ambiguous two-operand forms such as `git diff A B`.\n" +
|
|
119
130
|
"Default selection — run commands to decide, falling through until you get a NON-EMPTY diff:\n" +
|
|
120
131
|
"1. Get the current branch: `git branch --show-current`.\n" +
|
|
121
132
|
"2. Check for an OPEN GitHub PR for this branch: `gh pr list --head <branch> --state open --json number,title`. " +
|
|
@@ -127,7 +138,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
127
138
|
"Then: list the changed files, summarize the change in one paragraph (mention the PR if one was found), " +
|
|
128
139
|
"and read any relevant AGENTS.md or project docs noting conventions a reviewer should know.\n" +
|
|
129
140
|
"Return diffCommand exactly as a reviewer should run it. Structured output only.",
|
|
130
|
-
{ phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS,
|
|
141
|
+
{ phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, profile: "medium", schema: ScopeSchema },
|
|
131
142
|
);
|
|
132
143
|
|
|
133
144
|
if (!scope) {
|
|
@@ -136,56 +147,51 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
136
147
|
|
|
137
148
|
fileCount = scope.files.length;
|
|
138
149
|
progress({ type: "summary", key: "files", value: scope.files.join(", ") || "(none)" });
|
|
139
|
-
|
|
150
|
+
const diffTarget = parseAllowedDiffCommand(scope.diffCommand);
|
|
151
|
+
if ("error" in diffTarget) {
|
|
152
|
+
throw new Error(`Code-review target rejected: ${diffTarget.error}`);
|
|
153
|
+
}
|
|
154
|
+
const diffCommand = formatReviewDiffTarget(diffTarget);
|
|
155
|
+
progress({ type: "summary", key: "diffCommand", value: diffCommand });
|
|
140
156
|
progress({ type: "counter", key: "files", label: "files", value: fileCount });
|
|
141
157
|
|
|
142
158
|
if (scope.files.length === 0) {
|
|
143
159
|
return { summary: "No changes found to review.", findings: [], nextSteps: ["Provide a PR, ref range, or changed files to review."], stats: makeStats(0, 0) };
|
|
144
160
|
}
|
|
145
161
|
|
|
162
|
+
log(`${scope.files.length} changed files`);
|
|
163
|
+
|
|
164
|
+
// Capture the diff once, deterministically, so findings can be bounded to changed lines in code.
|
|
165
|
+
const reviewMaterial = await (dependencies.captureReviewMaterial ?? captureReviewMaterial)(diffTarget, cwd, signal);
|
|
166
|
+
if (!reviewMaterial.ok) {
|
|
167
|
+
throw new Error(`Code-review diff capture failed: ${reviewMaterial.error}`);
|
|
168
|
+
}
|
|
169
|
+
const diffText = reviewMaterial.diff;
|
|
170
|
+
const changed = changedLines(diffText);
|
|
171
|
+
progress({ type: "summary", key: "diffBytes", value: Buffer.byteLength(diffText) });
|
|
172
|
+
if (reviewMaterial.snapshot.status === "unavailable") {
|
|
173
|
+
log(`review snapshot unavailable (${reviewMaterial.snapshot.reason}) — patch previews will be unavailable`);
|
|
174
|
+
}
|
|
175
|
+
|
|
146
176
|
const reviewContext: ReviewContext = {
|
|
147
177
|
workflowName: "code-review",
|
|
148
178
|
target,
|
|
149
|
-
|
|
179
|
+
diffTarget,
|
|
150
180
|
files: scope.files,
|
|
151
181
|
summary: scope.summary,
|
|
182
|
+
...(reviewMaterial.snapshot.status === "verified"
|
|
183
|
+
? { snapshot: reviewMaterial.snapshot.identity }
|
|
184
|
+
: {}),
|
|
152
185
|
};
|
|
153
186
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
changed = changedLines(diffText);
|
|
163
|
-
progress({ type: "summary", key: "diffBytes", value: capturedDiff.bytes });
|
|
164
|
-
} else {
|
|
165
|
-
log(`diff capture failed (${capturedDiff.error ?? "unknown error"}) — reviewing without the line gate`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const diffBlock = diffText
|
|
169
|
-
? `\n## Diff (review is bounded to these changed lines)\n\`\`\`diff\n${
|
|
170
|
-
diffText.length > DIFF_EMBED_CAP
|
|
171
|
-
? `${diffText.slice(0, DIFF_EMBED_CAP)}\n... (truncated — run \`${scope.diffCommand}\` for the full diff)`
|
|
172
|
-
: diffText
|
|
173
|
-
}\n\`\`\`\n`
|
|
174
|
-
: "";
|
|
175
|
-
|
|
176
|
-
const scopeBlock =
|
|
177
|
-
`## Diff command\n${scope.diffCommand}\n\n## Changed files\n${scope.files.map((file) => `- ${file}`).join("\n")}\n\n` +
|
|
178
|
-
`## Summary\n${scope.summary}\n\n## Conventions\n${scope.conventions ?? "(none noted)"}\n` +
|
|
179
|
-
diffBlock +
|
|
180
|
-
(target ? `\n## User instructions (verbatim)\n${target}\n` : "");
|
|
181
|
-
|
|
182
|
-
// Dedup after all finders complete so verifier agents cannot consume the global cap before full candidate discovery.
|
|
183
|
-
const seen = new Set<string>();
|
|
184
|
-
const dedupKey = (candidate: Candidate): string => {
|
|
185
|
-
const location = primaryLocation(candidate);
|
|
186
|
-
const lineKey = location.line != null ? Math.round(location.line / 5) * 5 : candidate.summary.slice(0, 40).toLowerCase();
|
|
187
|
-
return `${normalizePath(location.file)}:${lineKey}`;
|
|
188
|
-
};
|
|
187
|
+
const scopeBlock = buildCodeReviewScopeBlock({
|
|
188
|
+
diffCommand,
|
|
189
|
+
files: scope.files,
|
|
190
|
+
summary: scope.summary,
|
|
191
|
+
conventions: scope.conventions,
|
|
192
|
+
diffText,
|
|
193
|
+
target,
|
|
194
|
+
});
|
|
189
195
|
|
|
190
196
|
// ─── Find barrier → dedup → Verify ───
|
|
191
197
|
phase("Find");
|
|
@@ -199,19 +205,16 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
199
205
|
`Surface up to ${PER_ANGLE} candidates. Use category exactly "${angle.kind}". Each candidate must include a one-line summary, ` +
|
|
200
206
|
"locations with the changed file and a line that appears in the diff, and impact describing the concrete failure or maintenance scenario. " +
|
|
201
207
|
"Pass through anything with a nameable impact — a separate verifier judges them next. Structured output only.",
|
|
202
|
-
{ phase: "Find", label: `find:${angle.label}`, tools: TOOLS, toolHints: TOOL_HINTS,
|
|
208
|
+
{ phase: "Find", label: `find:${angle.label}`, tools: TOOLS, toolHints: TOOL_HINTS, profile: "small", schema: AdvisoryCandidatesSchema },
|
|
203
209
|
);
|
|
204
210
|
const raw = (found?.candidates ?? []).slice(0, PER_ANGLE);
|
|
205
211
|
rawCandidateCount += raw.length;
|
|
206
212
|
progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
|
|
207
|
-
const
|
|
208
|
-
const bounded = gate ? raw.filter((candidate) => inDiff(gate, primaryLocation(candidate).file, primaryLocation(candidate).line)) : raw;
|
|
213
|
+
const bounded = raw.filter((candidate) => inDiff(changed, primaryLocation(candidate).file, primaryLocation(candidate).line));
|
|
209
214
|
const dropped = raw.length - bounded.length;
|
|
210
215
|
if (dropped > 0) {
|
|
211
216
|
droppedCandidateCount += dropped;
|
|
212
217
|
progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
|
|
213
|
-
}
|
|
214
|
-
if (gate && dropped > 0) {
|
|
215
218
|
log(`find:${angle.label}: dropped ${dropped} out-of-diff candidate(s)`);
|
|
216
219
|
}
|
|
217
220
|
for (const candidate of bounded) {
|
|
@@ -228,16 +231,8 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
228
231
|
}),
|
|
229
232
|
);
|
|
230
233
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
.filter((candidate) => {
|
|
234
|
-
const key = dedupKey(candidate);
|
|
235
|
-
if (seen.has(key)) return false;
|
|
236
|
-
seen.add(key);
|
|
237
|
-
return true;
|
|
238
|
-
})
|
|
239
|
-
.map((candidate) => ({ angle, candidate })),
|
|
240
|
-
);
|
|
234
|
+
// Dedup after all finders complete so verifier agents cannot consume the global cap before full candidate discovery.
|
|
235
|
+
const novel = dedupeCodeReviewCandidates(compactResults(perAngle));
|
|
241
236
|
|
|
242
237
|
phase("Verify");
|
|
243
238
|
const verdicts = await parallel(
|
|
@@ -254,7 +249,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
254
249
|
label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
255
250
|
tools: TOOLS,
|
|
256
251
|
toolHints: TOOL_HINTS,
|
|
257
|
-
|
|
252
|
+
profile: "small",
|
|
258
253
|
schema: AdvisoryVerdictSchema,
|
|
259
254
|
},
|
|
260
255
|
);
|
|
@@ -267,11 +262,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
267
262
|
const verified = compactResults(verdicts);
|
|
268
263
|
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
269
264
|
const stats = makeStats(verified.length, surviving.length);
|
|
270
|
-
|
|
271
|
-
progress({ type: "counter", key: "kept", label: "kept", value: surviving.length });
|
|
272
|
-
progress({ type: "summary", key: "verified", value: verified.length });
|
|
273
|
-
progress({ type: "summary", key: "kept", value: surviving.length });
|
|
274
|
-
log(`${verified.length} verified → ${surviving.length} kept`);
|
|
265
|
+
publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
|
|
275
266
|
|
|
276
267
|
if (surviving.length === 0) {
|
|
277
268
|
return { summary: "No findings survived verification.", findings: [], nextSteps: ["No code-review action is recommended from this workflow run."], stats, reviewContext };
|
|
@@ -295,7 +286,14 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
295
286
|
"Merge findings with the same root cause, rank most-severe first (correctness bugs above cleanups), and produce the final advisory report. " +
|
|
296
287
|
"Return summary, findings, and nextSteps. For each finding: category must be bug or cleanup; severity is impact level (low/medium/high), not category; " +
|
|
297
288
|
"confidence must be high for CONFIRMED and medium for PLAUSIBLE; copy locations and evidence arrays from the verified finding; impact is the concrete failure or maintenance scenario; recommendation is an advisory fix direction, not an edit. Structured output only.",
|
|
298
|
-
{
|
|
289
|
+
{
|
|
290
|
+
phase: "Synthesize",
|
|
291
|
+
label: "synthesize",
|
|
292
|
+
tools: [],
|
|
293
|
+
profile: "medium",
|
|
294
|
+
resume: "read-only",
|
|
295
|
+
schema: AdvisoryReportSchema,
|
|
296
|
+
},
|
|
299
297
|
);
|
|
300
298
|
|
|
301
299
|
if (!report) return { summary: "Synthesis produced no output.", findings: [], nextSteps: ["Re-run the workflow or inspect verifier evidence manually."], stats, reviewContext };
|
|
@@ -4,15 +4,16 @@ import {
|
|
|
4
4
|
AdvisoryReportSchema,
|
|
5
5
|
AdvisoryVerdictSchema,
|
|
6
6
|
type AdvisoryCandidate,
|
|
7
|
-
type AdvisoryReport,
|
|
8
7
|
type AdvisoryVerdict,
|
|
9
8
|
} from "../src/advisory-schema.ts";
|
|
10
9
|
import {
|
|
11
10
|
advisoryDedupKey as dedupKey,
|
|
12
11
|
backfillAdvisoryFindings,
|
|
12
|
+
emptyAdvisoryReport,
|
|
13
13
|
formatEvidence,
|
|
14
14
|
formatLocation,
|
|
15
15
|
primaryLocation,
|
|
16
|
+
publishVerifiedKeptProgress,
|
|
16
17
|
recordVerdictProgress,
|
|
17
18
|
DEFAULT_ADVISORY_TOOL_HINTS,
|
|
18
19
|
DEFAULT_ADVISORY_TOOLS,
|
|
@@ -89,11 +90,11 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
89
90
|
"Inspect relevant files, package/test configuration, and safe diagnostic commands. " +
|
|
90
91
|
"Safe commands are read-only commands such as status, grep, listing files, typecheck/test commands, or commands explicitly requested by the user. " +
|
|
91
92
|
"Do not run mutation, install, commit, network, or destructive commands. Return scoped files, observations, and constraints. Structured output only.",
|
|
92
|
-
{ phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS,
|
|
93
|
+
{ phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, profile: "medium", schema: ScopeSchema },
|
|
93
94
|
);
|
|
94
95
|
|
|
95
96
|
if (!scope) {
|
|
96
|
-
return
|
|
97
|
+
return emptyAdvisoryReport(
|
|
97
98
|
"Diagnosis could not establish a scope.",
|
|
98
99
|
["Provide the failing command, error message, or regression description and rerun diagnose."],
|
|
99
100
|
makeStats(0, 0),
|
|
@@ -121,7 +122,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
121
122
|
`Consider ONLY this hypothesis lens:\n${lens.text}\n\n` +
|
|
122
123
|
`Surface up to ${PER_LENS} root-cause hypotheses. Use category exactly "${lens.category}". ` +
|
|
123
124
|
"Each hypothesis must include a one-line summary, locations, impact explaining how it produces the symptom, and an optional recommendation for the next validation step. Structured output only.",
|
|
124
|
-
{ phase: "Hypothesize", label: `hypothesize:${lens.label}`, tools: TOOLS, toolHints: TOOL_HINTS,
|
|
125
|
+
{ phase: "Hypothesize", label: `hypothesize:${lens.label}`, tools: TOOLS, toolHints: TOOL_HINTS, profile: "small", schema: AdvisoryCandidatesSchema },
|
|
125
126
|
);
|
|
126
127
|
const candidates = (found?.candidates ?? []).slice(0, PER_LENS).map((candidate) => ({ ...candidate, lens }));
|
|
127
128
|
rawCandidateCount += candidates.length;
|
|
@@ -162,7 +163,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
162
163
|
label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
163
164
|
tools: TOOLS,
|
|
164
165
|
toolHints: TOOL_HINTS,
|
|
165
|
-
|
|
166
|
+
profile: "small",
|
|
166
167
|
schema: AdvisoryVerdictSchema,
|
|
167
168
|
},
|
|
168
169
|
);
|
|
@@ -178,14 +179,10 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
178
179
|
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
179
180
|
const refuted = verified.filter((finding) => finding.verdict === "REFUTED");
|
|
180
181
|
const stats = makeStats(verified.length, surviving.length);
|
|
181
|
-
|
|
182
|
-
progress({ type: "counter", key: "kept", label: "kept", value: surviving.length });
|
|
183
|
-
progress({ type: "summary", key: "verified", value: verified.length });
|
|
184
|
-
progress({ type: "summary", key: "kept", value: surviving.length });
|
|
185
|
-
log(`${verified.length} verified → ${surviving.length} kept`);
|
|
182
|
+
publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
|
|
186
183
|
|
|
187
184
|
if (surviving.length === 0) {
|
|
188
|
-
return
|
|
185
|
+
return emptyAdvisoryReport(
|
|
189
186
|
"No root-cause hypothesis survived verification.",
|
|
190
187
|
["Capture the exact failing command and error output.", "Rerun diagnose with a narrower symptom or more evidence."],
|
|
191
188
|
stats,
|
|
@@ -212,10 +209,17 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
212
209
|
"Produce the shared advisory report shape. Only include confirmed or plausible root causes in findings. " +
|
|
213
210
|
"Use categories such as root-cause, regression, configuration, dependency, or test-fixture. " +
|
|
214
211
|
"Recommendation must be a validation/fix plan, not a patch. nextSteps must be the minimum commands or code inspections needed to confirm the top diagnosis. Structured output only.",
|
|
215
|
-
{
|
|
212
|
+
{
|
|
213
|
+
phase: "Synthesize",
|
|
214
|
+
label: "synthesize",
|
|
215
|
+
tools: [],
|
|
216
|
+
profile: "medium",
|
|
217
|
+
resume: "read-only",
|
|
218
|
+
schema: AdvisoryReportSchema,
|
|
219
|
+
},
|
|
216
220
|
);
|
|
217
221
|
|
|
218
|
-
if (!report) return
|
|
222
|
+
if (!report) return emptyAdvisoryReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun diagnose with a narrower symptom."], stats);
|
|
219
223
|
|
|
220
224
|
const findings = backfillAdvisoryFindings(report.findings, ranked, {
|
|
221
225
|
impact: "Impact not restated by synthesis.",
|
|
@@ -224,10 +228,6 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
224
228
|
return { ...report, findings, stats };
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
function emptyReport(summary: string, nextSteps: string[], stats: WorkflowRunStats): AdvisoryReport & { stats: WorkflowRunStats } {
|
|
228
|
-
return { summary, findings: [], nextSteps, stats };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
231
|
function dedupe(candidates: Hypothesis[], onDropped: (dropped: number) => void): Hypothesis[] {
|
|
232
232
|
const seen = new Set<string>();
|
|
233
233
|
const novel: Hypothesis[] = [];
|
|
@@ -2,13 +2,14 @@ import { Type } from "typebox";
|
|
|
2
2
|
import {
|
|
3
3
|
AdvisoryReportSchema,
|
|
4
4
|
type AdvisoryCandidate,
|
|
5
|
-
type AdvisoryReport,
|
|
6
5
|
type AdvisoryVerdict,
|
|
7
6
|
} from "../src/advisory-schema.ts";
|
|
8
7
|
import {
|
|
9
8
|
backfillAdvisoryFindings,
|
|
9
|
+
emptyAdvisoryReport,
|
|
10
10
|
formatEvidence,
|
|
11
11
|
formatLocation,
|
|
12
|
+
publishVerifiedKeptProgress,
|
|
12
13
|
runLensVerificationPipeline,
|
|
13
14
|
DEFAULT_ADVISORY_TOOL_HINTS,
|
|
14
15
|
DEFAULT_ADVISORY_TOOLS,
|
|
@@ -80,11 +81,11 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
80
81
|
"Inspect repository structure, scripts, likely hot-path files, and any existing benchmark or measurement commands. " +
|
|
81
82
|
"Prefer identifying what to measure before claiming bottlenecks. Return files, commands, summary, and known measurements or the lack of them. " +
|
|
82
83
|
`This workflow will fan out across ${PERF_LENSES.length} lenses with up to ${PER_LENS} candidates per lens. Structured output only.`,
|
|
83
|
-
{ phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS,
|
|
84
|
+
{ phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, profile: "medium", schema: ScopeSchema },
|
|
84
85
|
);
|
|
85
86
|
|
|
86
87
|
if (!scope || scope.files.length === 0) {
|
|
87
|
-
return
|
|
88
|
+
return emptyAdvisoryReport(
|
|
88
89
|
"No performance-relevant files were identified.",
|
|
89
90
|
["Provide a slow command, workload, file path, or user-visible latency concern to review."],
|
|
90
91
|
makeStats(0, 0),
|
|
@@ -139,14 +140,10 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
139
140
|
const verified = pipelineResult.verified;
|
|
140
141
|
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
141
142
|
const stats = makeStats(verified.length, surviving.length);
|
|
142
|
-
|
|
143
|
-
progress({ type: "counter", key: "kept", label: "kept", value: surviving.length });
|
|
144
|
-
progress({ type: "summary", key: "verified", value: verified.length });
|
|
145
|
-
progress({ type: "summary", key: "kept", value: surviving.length });
|
|
146
|
-
log(`${verified.length} verified → ${surviving.length} kept`);
|
|
143
|
+
publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
|
|
147
144
|
|
|
148
145
|
if (surviving.length === 0) {
|
|
149
|
-
return
|
|
146
|
+
return emptyAdvisoryReport(
|
|
150
147
|
"No performance finding survived verification.",
|
|
151
148
|
["Add or run a focused measurement for the target workload before optimizing.", "Rerun perf-review with benchmark output or a narrower slow path."],
|
|
152
149
|
stats,
|
|
@@ -168,10 +165,17 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
168
165
|
"Produce the shared advisory report shape. Categories should be algorithmic, io, concurrency, startup, allocation, or measurement when applicable. " +
|
|
169
166
|
"Severity is expected performance impact for the target workload. Prefer measurement recommendations before optimization recommendations when evidence is weak. " +
|
|
170
167
|
"Recommendations must be safe advisory next actions, not patches. Include risky optimizations to avoid in recommendations or nextSteps when relevant. Structured output only.",
|
|
171
|
-
{
|
|
168
|
+
{
|
|
169
|
+
phase: "Synthesize",
|
|
170
|
+
label: "synthesize",
|
|
171
|
+
tools: [],
|
|
172
|
+
profile: "medium",
|
|
173
|
+
resume: "read-only",
|
|
174
|
+
schema: AdvisoryReportSchema,
|
|
175
|
+
},
|
|
172
176
|
);
|
|
173
177
|
|
|
174
|
-
if (!report) return
|
|
178
|
+
if (!report) return emptyAdvisoryReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun perf-review with a narrower target."], stats);
|
|
175
179
|
|
|
176
180
|
const findings = backfillAdvisoryFindings(report.findings, ranked, {
|
|
177
181
|
impact: "Performance impact not restated by synthesis.",
|
|
@@ -180,13 +184,8 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
180
184
|
return { ...report, findings, stats };
|
|
181
185
|
}
|
|
182
186
|
|
|
183
|
-
function emptyReport(summary: string, nextSteps: string[], stats: WorkflowRunStats): AdvisoryReport & { stats: WorkflowRunStats } {
|
|
184
|
-
return { summary, findings: [], nextSteps, stats };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
187
|
function rank(finding: Verified): number {
|
|
188
188
|
const verdictRank = finding.verdict === "CONFIRMED" ? 0 : 1;
|
|
189
189
|
const measurementPenalty = finding.category === "measurement" ? 1 : 0;
|
|
190
190
|
return verdictRank + measurementPenalty;
|
|
191
191
|
}
|
|
192
|
-
|
|
@@ -2,13 +2,14 @@ import { Type } from "typebox";
|
|
|
2
2
|
import {
|
|
3
3
|
AdvisoryReportSchema,
|
|
4
4
|
type AdvisoryCandidate,
|
|
5
|
-
type AdvisoryReport,
|
|
6
5
|
type AdvisoryVerdict,
|
|
7
6
|
} from "../src/advisory-schema.ts";
|
|
8
7
|
import {
|
|
9
8
|
backfillAdvisoryFindings,
|
|
9
|
+
emptyAdvisoryReport,
|
|
10
10
|
formatEvidence,
|
|
11
11
|
formatLocation,
|
|
12
|
+
publishVerifiedKeptProgress,
|
|
12
13
|
runLensVerificationPipeline,
|
|
13
14
|
DEFAULT_ADVISORY_TOOL_HINTS,
|
|
14
15
|
DEFAULT_ADVISORY_TOOLS,
|
|
@@ -79,11 +80,11 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
79
80
|
"Inspect repository structure, the target path or module, and relevant AGENTS.md / project docs conventions. " +
|
|
80
81
|
"Return the concrete files that should be considered, a short summary, and any conventions that affect refactor advice. " +
|
|
81
82
|
`This workflow will fan out across ${REFACTOR_LENSES.length} lenses with up to ${PER_LENS} candidates per lens. Structured output only.`,
|
|
82
|
-
{ phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS,
|
|
83
|
+
{ phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, profile: "medium", schema: ScopeSchema },
|
|
83
84
|
);
|
|
84
85
|
|
|
85
86
|
if (!scope || scope.files.length === 0) {
|
|
86
|
-
return
|
|
87
|
+
return emptyAdvisoryReport(
|
|
87
88
|
"No files were identified for refactor scouting.",
|
|
88
89
|
["Provide a target path, module, or subsystem to scout for refactor opportunities."],
|
|
89
90
|
makeStats(0, 0),
|
|
@@ -136,14 +137,10 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
136
137
|
const verified = pipelineResult.verified;
|
|
137
138
|
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
138
139
|
const stats = makeStats(verified.length, surviving.length);
|
|
139
|
-
|
|
140
|
-
progress({ type: "counter", key: "kept", label: "kept", value: surviving.length });
|
|
141
|
-
progress({ type: "summary", key: "verified", value: verified.length });
|
|
142
|
-
progress({ type: "summary", key: "kept", value: surviving.length });
|
|
143
|
-
log(`${verified.length} verified → ${surviving.length} kept`);
|
|
140
|
+
publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
|
|
144
141
|
|
|
145
142
|
if (surviving.length === 0) {
|
|
146
|
-
return
|
|
143
|
+
return emptyAdvisoryReport("No refactor opportunities survived verification.", ["Leave the scoped code unchanged unless a human reviewer has additional context."], stats);
|
|
147
144
|
}
|
|
148
145
|
|
|
149
146
|
phase("Synthesize");
|
|
@@ -162,10 +159,17 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
162
159
|
"Return the shared advisory report shape. Categories should come from the verified candidates. " +
|
|
163
160
|
"Severity is maintenance or future-correctness impact. Confidence is high for CONFIRMED, medium for PLAUSIBLE unless verifier confidence says otherwise. " +
|
|
164
161
|
"Recommendations must be safe first refactor steps, not rewrites. Include concrete nextSteps for the host developer. Structured output only.",
|
|
165
|
-
{
|
|
162
|
+
{
|
|
163
|
+
phase: "Synthesize",
|
|
164
|
+
label: "synthesize",
|
|
165
|
+
tools: [],
|
|
166
|
+
profile: "medium",
|
|
167
|
+
resume: "read-only",
|
|
168
|
+
schema: AdvisoryReportSchema,
|
|
169
|
+
},
|
|
166
170
|
);
|
|
167
171
|
|
|
168
|
-
if (!report) return
|
|
172
|
+
if (!report) return emptyAdvisoryReport("Synthesis produced no output.", ["Inspect verifier evidence manually or rerun the workflow with a narrower target."], stats);
|
|
169
173
|
|
|
170
174
|
const findings = backfillAdvisoryFindings(report.findings, ranked, {
|
|
171
175
|
impact: "Impact not restated by synthesis.",
|
|
@@ -174,10 +178,6 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
|
174
178
|
return { ...report, findings, stats };
|
|
175
179
|
}
|
|
176
180
|
|
|
177
|
-
function emptyReport(summary: string, nextSteps: string[], stats: WorkflowRunStats): AdvisoryReport & { stats: WorkflowRunStats } {
|
|
178
|
-
return { summary, findings: [], nextSteps, stats };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
181
|
function rank(finding: Verified): number {
|
|
182
182
|
const verdictRank = finding.verdict === "CONFIRMED" ? 0 : 1;
|
|
183
183
|
const categoryRank = finding.category === "dead-code" || finding.category === "conventions" ? 2 : 0;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { compactResults } from "../src/concurrency.ts";
|
|
2
|
+
import {
|
|
3
|
+
MAX_RESEARCH_LANES,
|
|
4
|
+
ResearchLaneResultSchema,
|
|
5
|
+
ResearchPlanSchema,
|
|
6
|
+
ResearchReportSchema,
|
|
7
|
+
ResearchVerificationSchema,
|
|
8
|
+
type ResearchClaimCandidate,
|
|
9
|
+
type ResearchReport,
|
|
10
|
+
type ResearchVerification,
|
|
11
|
+
} from "../src/research-contract.ts";
|
|
12
|
+
import {
|
|
13
|
+
buildClaimCandidates,
|
|
14
|
+
fallbackResearchReport,
|
|
15
|
+
normalizeResearchLanes,
|
|
16
|
+
sanitizeLaneResults,
|
|
17
|
+
sanitizeResearchReport,
|
|
18
|
+
sanitizeVerification,
|
|
19
|
+
unavailableResearchReport,
|
|
20
|
+
unavailableVerification,
|
|
21
|
+
} from "../src/research-evidence.ts";
|
|
22
|
+
import { WorkflowToolHintUnavailableError } from "../src/tool-capabilities.ts";
|
|
23
|
+
import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
24
|
+
|
|
25
|
+
export const meta: WorkflowMeta = {
|
|
26
|
+
name: "research",
|
|
27
|
+
description: "Source-grounded external research: decompose → gather direct-page evidence → independently verify claims → cited synthesis.",
|
|
28
|
+
phases: [{ title: "Plan" }, { title: "Gather" }, { title: "Verify" }, { title: "Synthesize" }],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const EXTERNAL_TOOLS: string[] = [];
|
|
32
|
+
const EXTERNAL_TOOL_HINTS = ["external-search"] as const;
|
|
33
|
+
|
|
34
|
+
export default async function run(api: WorkflowApi): Promise<ResearchReport> {
|
|
35
|
+
const { agent, parallel, phase, log, progress, args } = api;
|
|
36
|
+
const question = args.trim();
|
|
37
|
+
if (!question) return unavailableResearchReport("empty-question");
|
|
38
|
+
|
|
39
|
+
phase("Plan");
|
|
40
|
+
let plan;
|
|
41
|
+
try {
|
|
42
|
+
plan = await agent(
|
|
43
|
+
`Plan bounded, source-grounded research for the user's question and any scope constraints embedded in it.\n\n` +
|
|
44
|
+
`Question and constraints (verbatim):\n${question}\n\n` +
|
|
45
|
+
`Create at most ${MAX_RESEARCH_LANES} non-overlapping lanes. Each lane needs a stable short id, title, objective, and 1-4 concrete search queries. ` +
|
|
46
|
+
"Separate primary-source discovery, current status, counterevidence, or jurisdiction/timeframe only when relevant. " +
|
|
47
|
+
"Do not claim exhaustive coverage. Structured output only.",
|
|
48
|
+
{
|
|
49
|
+
phase: "Plan",
|
|
50
|
+
label: "plan",
|
|
51
|
+
tools: EXTERNAL_TOOLS,
|
|
52
|
+
toolHints: EXTERNAL_TOOL_HINTS,
|
|
53
|
+
requireToolHints: true,
|
|
54
|
+
profile: "medium",
|
|
55
|
+
resume: "off",
|
|
56
|
+
schema: ResearchPlanSchema,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof WorkflowToolHintUnavailableError) return unavailableResearchReport("missing-capability");
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!plan) return unavailableResearchReport("no-evidence");
|
|
65
|
+
const lanes = normalizeResearchLanes(plan);
|
|
66
|
+
if (lanes.length === 0) return unavailableResearchReport("no-evidence");
|
|
67
|
+
progress({ type: "counter", key: "research.lanes", label: "research lanes", value: lanes.length });
|
|
68
|
+
progress({ type: "summary", key: "research.question", value: question });
|
|
69
|
+
|
|
70
|
+
phase("Gather");
|
|
71
|
+
const gathered = compactResults(
|
|
72
|
+
await parallel(
|
|
73
|
+
lanes.map((lane) => async () => {
|
|
74
|
+
const result = await agent(
|
|
75
|
+
`Research one bounded lane using only installed external web-search, browsing, or URL-extraction tools.\n\n` +
|
|
76
|
+
`Question: ${question}\n` +
|
|
77
|
+
`Scope constraints: ${plan.scopeConstraints.join("; ") || "(none)"}\n` +
|
|
78
|
+
`Lane id: ${lane.id}\nLane: ${lane.title}\nObjective: ${lane.objective}\n` +
|
|
79
|
+
`Queries:\n${lane.queries.map((query) => `- ${query}`).join("\n")}\n\n` +
|
|
80
|
+
"Open the supporting pages instead of citing a search-results page. Prefer primary and authoritative sources; use independent sources when useful. " +
|
|
81
|
+
"Return concrete claims with importance, whether each page supports or conflicts with the claim, a short passage or precise paraphrase, and the exact page title and URL. " +
|
|
82
|
+
"State evidence gaps. Never invent a URL or claim that the opened page does not support. Structured output only.",
|
|
83
|
+
{
|
|
84
|
+
phase: "Gather",
|
|
85
|
+
label: `gather:${lane.id}`,
|
|
86
|
+
tools: EXTERNAL_TOOLS,
|
|
87
|
+
toolHints: EXTERNAL_TOOL_HINTS,
|
|
88
|
+
requireToolHints: true,
|
|
89
|
+
profile: "small",
|
|
90
|
+
resume: "off",
|
|
91
|
+
schema: ResearchLaneResultSchema,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
if (!result) return null;
|
|
95
|
+
progress({ type: "counter_delta", key: "research.evidence", label: "evidence items", delta: result.evidence.length });
|
|
96
|
+
progress({
|
|
97
|
+
type: "lane_item",
|
|
98
|
+
lane: "Research lanes",
|
|
99
|
+
title: lane.title,
|
|
100
|
+
subtitle: `${result.evidence.length} evidence item(s)`,
|
|
101
|
+
status: result.evidence.length > 0 ? "success" : "warning",
|
|
102
|
+
details: result.gaps.join("; ") || lane.objective,
|
|
103
|
+
});
|
|
104
|
+
return result;
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const laneResults = sanitizeLaneResults(gathered);
|
|
110
|
+
const candidates = buildClaimCandidates(laneResults);
|
|
111
|
+
if (candidates.length === 0) return unavailableResearchReport("no-evidence");
|
|
112
|
+
progress({ type: "counter", key: "research.claims", label: "claims to verify", value: candidates.length });
|
|
113
|
+
log(`${candidates.length} bounded claim(s) selected for independent verification`);
|
|
114
|
+
|
|
115
|
+
phase("Verify");
|
|
116
|
+
const verificationResults = await parallel(
|
|
117
|
+
candidates.map((candidate, index) => async () => {
|
|
118
|
+
const result = await agent(verificationPrompt(question, candidate), {
|
|
119
|
+
phase: "Verify",
|
|
120
|
+
label: `verify:${index + 1}`,
|
|
121
|
+
tools: EXTERNAL_TOOLS,
|
|
122
|
+
toolHints: EXTERNAL_TOOL_HINTS,
|
|
123
|
+
requireToolHints: true,
|
|
124
|
+
profile: "medium",
|
|
125
|
+
resume: "off",
|
|
126
|
+
schema: ResearchVerificationSchema,
|
|
127
|
+
});
|
|
128
|
+
return result ? sanitizeVerification(result, candidate) : null;
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
const verifications = verificationResults.map((result, index) => result ?? unavailableVerification(candidates[index]!));
|
|
132
|
+
for (const verification of verifications) {
|
|
133
|
+
progress({ type: "counter_delta", key: `research.${verification.verdict.toLowerCase()}`, label: verification.verdict.toLowerCase(), delta: 1 });
|
|
134
|
+
}
|
|
135
|
+
const synthesisInputs = verifications.filter((verification) => verification.verdict !== "REJECTED");
|
|
136
|
+
|
|
137
|
+
phase("Synthesize");
|
|
138
|
+
const synthesis = await agent(
|
|
139
|
+
`Answer the research question using only the independently verified handoff below.\n\n` +
|
|
140
|
+
`Question: ${question}\nScope constraints: ${plan.scopeConstraints.join("; ") || "(none)"}\n\n` +
|
|
141
|
+
`Verified claims JSON:\n${JSON.stringify(synthesisInputs)}\n\n` +
|
|
142
|
+
"Keep SUPPORTED claims, CONFLICTED evidence, UNCERTAIN claims, and model INFERENCE in their separate fields. Exclude REJECTED claims. " +
|
|
143
|
+
"Copy each verified claim string exactly so its citations remain bound to that claim during validation. " +
|
|
144
|
+
"Every supported or conflicting claim must cite exact title/URL objects from its verification; do not add URLs, cite search-results pages, or turn inference into fact. " +
|
|
145
|
+
"Answer concisely, disclose limited coverage, and provide useful next steps. Structured output only.",
|
|
146
|
+
{
|
|
147
|
+
phase: "Synthesize",
|
|
148
|
+
label: "synthesize",
|
|
149
|
+
tools: [],
|
|
150
|
+
profile: "medium",
|
|
151
|
+
resume: "off",
|
|
152
|
+
schema: ResearchReportSchema,
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return sanitizeResearchReport(
|
|
157
|
+
synthesis ?? fallbackResearchReport(synthesisInputs, "The model did not return a structured synthesis."),
|
|
158
|
+
synthesisInputs,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function verificationPrompt(question: string, candidate: ResearchClaimCandidate): string {
|
|
163
|
+
return `Independently verify one important research claim using installed external web-search, browsing, or URL-extraction tools.\n\n` +
|
|
164
|
+
`Question: ${question}\nClaim: ${candidate.claim}\nImportance: ${candidate.importance}\n\n` +
|
|
165
|
+
`Gather-stage evidence (context only; do not treat it as verified):\n${JSON.stringify(candidate.evidence)}\n\n` +
|
|
166
|
+
"Search independently and open direct supporting pages. Prefer primary/authoritative sources and actively look for credible counterevidence. " +
|
|
167
|
+
"Return SUPPORTED only when direct pages substantiate the claim, CONFLICTED when credible sources disagree, UNCERTAIN when evidence is insufficient, " +
|
|
168
|
+
"INFERENCE when the conclusion is reasoned rather than directly stated, or REJECTED when evidence refutes it. " +
|
|
169
|
+
"Include exact page titles and direct HTTP(S) URLs, never search-results URLs. Structured output only.";
|
|
170
|
+
}
|