@mrclrchtr/supi-review 2.6.0 → 2.7.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/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/package.json +2 -2
- package/src/history/synthesize.ts +1 -5
- package/src/review.ts +43 -31
- package/src/tool/agent-review-tools.ts +8 -10
- package/src/tool/agent-review-workflow.ts +41 -28
- package/src/tool/brief-runner.ts +25 -25
- package/src/tool/child-failure-diagnostics.ts +145 -0
- package/src/tool/child-lifecycle-trace.ts +329 -0
- package/src/tool/review-handlers.ts +9 -30
- package/src/tool/review-runner.ts +25 -44
- package/src/tool/runner-helpers.ts +0 -14
- package/src/tool/session-lifecycle.ts +105 -10
- package/src/types.ts +51 -40
- package/src/ui/format-content.ts +47 -38
- package/src/ui/renderer.ts +67 -45
- package/src/ui/review-tool-renderer.ts +32 -1
- package/src/tool/review-debug.ts +0 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-review",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "SuPi Review extension — session-aware review command and agent tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@mrclrchtr/supi-core": "2.
|
|
34
|
+
"@mrclrchtr/supi-core": "2.7.0"
|
|
35
35
|
},
|
|
36
36
|
"bundledDependencies": [
|
|
37
37
|
"@mrclrchtr/supi-core"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { listReviewInstructionBlocks } from "../target/packet.ts";
|
|
3
2
|
import { runBriefSynthesis } from "../tool/brief-runner.ts";
|
|
4
3
|
import type {
|
|
@@ -15,7 +14,6 @@ export const BRIEF_SYNTHESIS_PROMPT_VERSION = "1";
|
|
|
15
14
|
|
|
16
15
|
export interface SynthesizeReviewBriefOptions {
|
|
17
16
|
model: ReviewModelSelection;
|
|
18
|
-
modelRegistry: ModelRegistry;
|
|
19
17
|
cwd: string;
|
|
20
18
|
snapshot: ReviewSnapshot;
|
|
21
19
|
serializedContext: string;
|
|
@@ -28,13 +26,11 @@ export interface SynthesizeReviewBriefOptions {
|
|
|
28
26
|
export function synthesizeReviewBrief(
|
|
29
27
|
options: SynthesizeReviewBriefOptions,
|
|
30
28
|
): Promise<BriefSynthesisRunResult> {
|
|
31
|
-
const { model,
|
|
32
|
-
options;
|
|
29
|
+
const { model, cwd, snapshot, serializedContext, note, signal, onProgress } = options;
|
|
33
30
|
|
|
34
31
|
return runBriefSynthesis({
|
|
35
32
|
prompt: buildBriefSynthesisPrompt(snapshot, serializedContext, note),
|
|
36
33
|
model: model.model,
|
|
37
|
-
modelRegistry,
|
|
38
34
|
cwd,
|
|
39
35
|
signal,
|
|
40
36
|
onProgress,
|
package/src/review.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { synthesizeReviewBrief } from "./history/synthesize.ts";
|
|
|
7
7
|
import { normalizeReviewResult } from "./review-result.ts";
|
|
8
8
|
import { buildReviewPacket } from "./target/packet.ts";
|
|
9
9
|
import { registerAgentReviewTools } from "./tool/agent-review-tools.ts";
|
|
10
|
+
import { createUnobservedChildFailureDiagnostics } from "./tool/child-failure-diagnostics.ts";
|
|
10
11
|
import { runReviewer } from "./tool/review-runner.ts";
|
|
11
12
|
import type {
|
|
12
13
|
BriefSynthesisRunResult,
|
|
@@ -17,7 +18,11 @@ import type {
|
|
|
17
18
|
ReviewTargetSpec,
|
|
18
19
|
} from "./types.ts";
|
|
19
20
|
import { collectReviewNote, previewReviewPlan, selectModel, selectTarget } from "./ui/flow.ts";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
formatBriefSynthesisFailureContent,
|
|
23
|
+
formatBriefSynthesisFailureCopy,
|
|
24
|
+
formatReviewContent,
|
|
25
|
+
} from "./ui/format-content.ts";
|
|
21
26
|
import { registerReviewRenderer } from "./ui/renderer.ts";
|
|
22
27
|
|
|
23
28
|
type CommandContext = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1];
|
|
@@ -64,7 +69,6 @@ async function handleInteractive(ctx: CommandContext, pi: ExtensionAPI): Promise
|
|
|
64
69
|
(signal: AbortSignal, onProgress: (p: WidgetProgress) => void) =>
|
|
65
70
|
synthesizeReviewBrief({
|
|
66
71
|
model,
|
|
67
|
-
modelRegistry: ctx.modelRegistry,
|
|
68
72
|
cwd: ctx.cwd,
|
|
69
73
|
snapshot,
|
|
70
74
|
serializedContext,
|
|
@@ -75,22 +79,21 @@ async function handleInteractive(ctx: CommandContext, pi: ExtensionAPI): Promise
|
|
|
75
79
|
);
|
|
76
80
|
|
|
77
81
|
if (!synthesis) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
ctx.ui.notify("Brief synthesis was canceled or failed", "warning");
|
|
82
|
+
const failure: BriefSynthesisRunResult = {
|
|
83
|
+
kind: "failed",
|
|
84
|
+
failureCode: "unexpected-runner-failure",
|
|
85
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
86
|
+
};
|
|
87
|
+
notifyBriefDone(pi, failure, snapshot, model.canonicalId);
|
|
88
|
+
injectBriefSynthesisFailureMessage(pi, failure, snapshot, model.canonicalId);
|
|
89
|
+
ctx.ui.notify(formatBriefSynthesisFailureCopy(failure), "warning");
|
|
88
90
|
return;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
notifyBriefDone(pi, synthesis, snapshot, model.canonicalId);
|
|
92
94
|
|
|
93
95
|
if (synthesis.kind !== "success") {
|
|
96
|
+
injectBriefSynthesisFailureMessage(pi, synthesis, snapshot, model.canonicalId);
|
|
94
97
|
notifySynthesisFailure(synthesis, ctx);
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
@@ -114,7 +117,6 @@ async function handleInteractive(ctx: CommandContext, pi: ExtensionAPI): Promise
|
|
|
114
117
|
runReviewer({
|
|
115
118
|
prompt: plan.packet.prompt,
|
|
116
119
|
model: plan.model,
|
|
117
|
-
modelRegistry: ctx.modelRegistry,
|
|
118
120
|
cwd: ctx.cwd,
|
|
119
121
|
signal,
|
|
120
122
|
snapshot: plan.snapshot,
|
|
@@ -124,14 +126,17 @@ async function handleInteractive(ctx: CommandContext, pi: ExtensionAPI): Promise
|
|
|
124
126
|
);
|
|
125
127
|
|
|
126
128
|
if (!rawResult) {
|
|
127
|
-
|
|
129
|
+
const failure: ReviewResult = {
|
|
128
130
|
kind: "failed",
|
|
129
|
-
|
|
131
|
+
failureCode: "unexpected-runner-failure",
|
|
132
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
130
133
|
snapshot,
|
|
131
134
|
brief,
|
|
132
135
|
modelId: model.canonicalId,
|
|
133
|
-
}
|
|
134
|
-
|
|
136
|
+
};
|
|
137
|
+
notifyReviewDone(pi, failure);
|
|
138
|
+
injectReviewMessage(pi, failure);
|
|
139
|
+
ctx.ui.notify("Reviewer ended unexpectedly.", "warning");
|
|
135
140
|
return;
|
|
136
141
|
}
|
|
137
142
|
|
|
@@ -174,20 +179,27 @@ function notifySynthesisFailure(
|
|
|
174
179
|
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
175
180
|
ctx: CommandContext,
|
|
176
181
|
): void {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
ctx.ui.notify(
|
|
183
|
+
formatBriefSynthesisFailureCopy(result),
|
|
184
|
+
result.kind === "failed" ? "error" : "warning",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Persist a brief-synthesis failure so bounded diagnostics remain inspectable in parent history. */
|
|
189
|
+
function injectBriefSynthesisFailureMessage(
|
|
190
|
+
pi: ExtensionAPI,
|
|
191
|
+
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
192
|
+
snapshot: ReviewSnapshot,
|
|
193
|
+
modelId: string,
|
|
194
|
+
): void {
|
|
195
|
+
pi.sendMessage({
|
|
196
|
+
customType: "supi-review",
|
|
197
|
+
content: formatBriefSynthesisFailureContent(result),
|
|
198
|
+
display: true,
|
|
199
|
+
details: {
|
|
200
|
+
briefSynthesisFailure: { result, snapshot, modelId },
|
|
201
|
+
},
|
|
202
|
+
});
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
/** Ring the terminal bell so the user knows to check back. */
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
ReviewProgress,
|
|
21
21
|
ReviewTargetSpec,
|
|
22
22
|
} from "../types.ts";
|
|
23
|
+
import { formatBriefSynthesisFailureCopy } from "../ui/format-content.ts";
|
|
23
24
|
import { formatAgentReviewBatch, formatPreparedAgentReview } from "../ui/review-tool-format.ts";
|
|
24
25
|
import {
|
|
25
26
|
type AgentReviewProgressDetails,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
runAgentReviewSchema,
|
|
36
37
|
} from "./agent-review-schemas.ts";
|
|
37
38
|
import { prepareAgentReviewPlan, runAgentReviewBatch } from "./agent-review-workflow.ts";
|
|
39
|
+
import { formatChildLifecycleTrace } from "./child-lifecycle-trace.ts";
|
|
38
40
|
import {
|
|
39
41
|
PREPARE_REVIEW_TOOL_NAME,
|
|
40
42
|
prepareReviewPromptGuidelines,
|
|
@@ -81,7 +83,6 @@ export function registerAgentReviewTools(
|
|
|
81
83
|
note,
|
|
82
84
|
serializedContext,
|
|
83
85
|
model,
|
|
84
|
-
modelRegistry: ctx.modelRegistry,
|
|
85
86
|
signal,
|
|
86
87
|
planStore,
|
|
87
88
|
onProgress: (progress) => {
|
|
@@ -136,7 +137,6 @@ export function registerAgentReviewTools(
|
|
|
136
137
|
critique: input.critique,
|
|
137
138
|
revisedBrief: input.revisedBrief,
|
|
138
139
|
reviewers: input.reviewers,
|
|
139
|
-
modelRegistry: ctx.modelRegistry,
|
|
140
140
|
signal,
|
|
141
141
|
planStore,
|
|
142
142
|
onBriefEvaluation: (evaluation) => {
|
|
@@ -230,14 +230,12 @@ function formatPreparationFailure(
|
|
|
230
230
|
outcome: Exclude<Awaited<ReturnType<typeof prepareAgentReviewPlan>>, { kind: "prepared" }>,
|
|
231
231
|
): string {
|
|
232
232
|
if (outcome.kind === "no-snapshot") return outcome.reason;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return `Brief synthesis timed out after ${(outcome.result.timeoutMs / 1_000).toFixed(0)}s.`;
|
|
240
|
-
}
|
|
233
|
+
|
|
234
|
+
const { result } = outcome;
|
|
235
|
+
const trace = result.diagnostics?.lifecycleTrace;
|
|
236
|
+
return trace
|
|
237
|
+
? `${formatBriefSynthesisFailureCopy(result)}\n\n${formatChildLifecycleTrace(trace)}`
|
|
238
|
+
: formatBriefSynthesisFailureCopy(result);
|
|
241
239
|
}
|
|
242
240
|
|
|
243
241
|
function activateRunTool(pi: ExtensionAPI): void {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import {
|
|
3
2
|
checkReviewSnapshotFreshness,
|
|
4
3
|
fingerprintReviewSnapshot,
|
|
@@ -23,8 +22,8 @@ import type {
|
|
|
23
22
|
ReviewTargetSpec,
|
|
24
23
|
SynthesizedReviewBrief,
|
|
25
24
|
} from "../types.ts";
|
|
25
|
+
import { createUnobservedChildFailureDiagnostics } from "./child-failure-diagnostics.ts";
|
|
26
26
|
import { runReviewer } from "./review-runner.ts";
|
|
27
|
-
|
|
28
27
|
/** Inputs required to synthesize and retain one agent-driven review plan. */
|
|
29
28
|
export interface PrepareAgentReviewWorkflowInput {
|
|
30
29
|
cwd: string;
|
|
@@ -32,18 +31,15 @@ export interface PrepareAgentReviewWorkflowInput {
|
|
|
32
31
|
note?: string;
|
|
33
32
|
serializedContext: string;
|
|
34
33
|
model: ReviewModelSelection;
|
|
35
|
-
modelRegistry: ModelRegistry;
|
|
36
34
|
signal?: AbortSignal;
|
|
37
35
|
onProgress?: (progress: ReviewProgress) => void;
|
|
38
36
|
planStore: ReviewPlanStore;
|
|
39
37
|
}
|
|
40
|
-
|
|
41
38
|
/** Typed preparation outcome before any reviewer child session starts. */
|
|
42
39
|
export type PrepareAgentReviewWorkflowOutcome =
|
|
43
40
|
| { kind: "prepared"; plan: StoredAgentReviewPlan }
|
|
44
41
|
| { kind: "no-snapshot"; reason: string }
|
|
45
42
|
| { kind: "synthesis-failed"; result: Exclude<BriefSynthesisRunResult, { kind: "success" }> };
|
|
46
|
-
|
|
47
43
|
/** Inputs required to critique and execute one prepared review plan. */
|
|
48
44
|
export interface RunAgentReviewWorkflowInput {
|
|
49
45
|
cwd: string;
|
|
@@ -51,20 +47,17 @@ export interface RunAgentReviewWorkflowInput {
|
|
|
51
47
|
critique: BriefCritique;
|
|
52
48
|
revisedBrief?: Omit<SynthesizedReviewBrief, "note">;
|
|
53
49
|
reviewers: ReviewerAssignment[];
|
|
54
|
-
modelRegistry: ModelRegistry;
|
|
55
50
|
signal?: AbortSignal;
|
|
56
51
|
onBriefEvaluation?: (evaluation: BriefEvaluation) => void;
|
|
57
52
|
onReviewerProgress?: (reviewerId: string, progress: ReviewProgress) => void;
|
|
58
53
|
onReviewerDone?: (reviewerId: string) => void;
|
|
59
54
|
planStore: ReviewPlanStore;
|
|
60
55
|
}
|
|
61
|
-
|
|
62
56
|
/** Typed batch outcome after validation and snapshot freshness checks. */
|
|
63
57
|
export type RunAgentReviewWorkflowOutcome =
|
|
64
58
|
| { kind: "completed"; details: AgentReviewBatchDetails }
|
|
65
59
|
| { kind: "invalid"; reason: string }
|
|
66
60
|
| { kind: "stale"; reason: string };
|
|
67
|
-
|
|
68
61
|
/** Prepare one session-scoped review plan without starting reviewer sessions. */
|
|
69
62
|
export async function prepareAgentReviewPlan(
|
|
70
63
|
input: PrepareAgentReviewWorkflowInput,
|
|
@@ -77,16 +70,27 @@ export async function prepareAgentReviewPlan(
|
|
|
77
70
|
};
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
let synthesis: BriefSynthesisRunResult;
|
|
74
|
+
try {
|
|
75
|
+
synthesis = await synthesizeReviewBrief({
|
|
76
|
+
model: input.model,
|
|
77
|
+
cwd: input.cwd,
|
|
78
|
+
snapshot,
|
|
79
|
+
serializedContext: input.serializedContext,
|
|
80
|
+
note: input.note,
|
|
81
|
+
signal: input.signal,
|
|
82
|
+
onProgress: input.onProgress,
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
return {
|
|
86
|
+
kind: "synthesis-failed",
|
|
87
|
+
result: {
|
|
88
|
+
kind: "failed",
|
|
89
|
+
failureCode: "unexpected-runner-failure",
|
|
90
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
90
94
|
if (synthesis.kind !== "success") {
|
|
91
95
|
return { kind: "synthesis-failed", result: synthesis };
|
|
92
96
|
}
|
|
@@ -192,7 +196,6 @@ async function runAssignment(
|
|
|
192
196
|
const rawResult = await runReviewer({
|
|
193
197
|
prompt: packet.prompt,
|
|
194
198
|
model: plan.model,
|
|
195
|
-
modelRegistry: input.modelRegistry,
|
|
196
199
|
cwd: input.cwd,
|
|
197
200
|
signal: input.signal,
|
|
198
201
|
snapshot: plan.snapshot,
|
|
@@ -200,13 +203,14 @@ async function runAssignment(
|
|
|
200
203
|
onProgress: (progress) => input.onReviewerProgress?.(assignment.id, progress),
|
|
201
204
|
});
|
|
202
205
|
return { assignment, result: compactReviewResult(normalizeReviewResult(rawResult)) };
|
|
203
|
-
} catch
|
|
206
|
+
} catch {
|
|
204
207
|
return {
|
|
205
208
|
assignment,
|
|
206
209
|
result: {
|
|
207
210
|
kind: "failed",
|
|
208
|
-
|
|
211
|
+
failureCode: "unexpected-runner-failure",
|
|
209
212
|
modelId: plan.model.canonicalId,
|
|
213
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
210
214
|
},
|
|
211
215
|
};
|
|
212
216
|
}
|
|
@@ -217,21 +221,30 @@ function compactReviewResult(result: ReviewResult): AgentReviewerResult {
|
|
|
217
221
|
case "success":
|
|
218
222
|
return { kind: "success", output: result.output, modelId: result.modelId };
|
|
219
223
|
case "failed":
|
|
224
|
+
return result.failureCode === "session-creation-failed"
|
|
225
|
+
? {
|
|
226
|
+
kind: "failed",
|
|
227
|
+
failureCode: result.failureCode,
|
|
228
|
+
modelId: result.modelId,
|
|
229
|
+
}
|
|
230
|
+
: {
|
|
231
|
+
kind: "failed",
|
|
232
|
+
failureCode: result.failureCode,
|
|
233
|
+
modelId: result.modelId,
|
|
234
|
+
diagnostics: result.diagnostics,
|
|
235
|
+
};
|
|
236
|
+
case "canceled":
|
|
220
237
|
return {
|
|
221
|
-
kind: "
|
|
222
|
-
reason: result.reason,
|
|
238
|
+
kind: "canceled",
|
|
223
239
|
modelId: result.modelId,
|
|
224
|
-
|
|
240
|
+
diagnostics: result.diagnostics,
|
|
225
241
|
};
|
|
226
|
-
case "canceled":
|
|
227
|
-
return { kind: "canceled", modelId: result.modelId, debug: result.debug };
|
|
228
242
|
case "timeout":
|
|
229
243
|
return {
|
|
230
244
|
kind: "timeout",
|
|
231
245
|
timeoutMs: result.timeoutMs,
|
|
232
|
-
partialOutput: result.partialOutput,
|
|
233
246
|
modelId: result.modelId,
|
|
234
|
-
|
|
247
|
+
diagnostics: result.diagnostics,
|
|
235
248
|
};
|
|
236
249
|
}
|
|
237
250
|
}
|
package/src/tool/brief-runner.ts
CHANGED
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
BriefSynthesisRunResult,
|
|
12
12
|
SynthesizedReviewBrief,
|
|
13
13
|
} from "../types.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { createEarlyCancellationDiagnostics } from "./child-failure-diagnostics.ts";
|
|
15
|
+
import { buildProgressTokens } from "./runner-helpers.ts";
|
|
15
16
|
import { reviewBriefSchema } from "./schemas.ts";
|
|
16
17
|
import { type LifecycleCtx, runWithLifecycle } from "./session-lifecycle.ts";
|
|
17
18
|
|
|
@@ -67,7 +68,6 @@ async function createBriefSession(
|
|
|
67
68
|
const { session } = await createAgentSession({
|
|
68
69
|
cwd: invocation.cwd,
|
|
69
70
|
model: invocation.model,
|
|
70
|
-
modelRegistry: invocation.modelRegistry,
|
|
71
71
|
thinkingLevel: clampThinkingLevel(invocation.model, "max"),
|
|
72
72
|
tools: ["submit_review_brief"],
|
|
73
73
|
customTools: [submitBriefTool],
|
|
@@ -89,24 +89,19 @@ function emitBriefProgress(
|
|
|
89
89
|
|
|
90
90
|
/** Finalize after retries and compaction recovery, never at the earlier `agent_end` boundary. */
|
|
91
91
|
function handleAgentSettled(options: {
|
|
92
|
-
session: AgentSession;
|
|
93
92
|
brief: SynthesizedReviewBrief | undefined;
|
|
94
93
|
state: { settled: boolean; aborting: boolean };
|
|
95
94
|
cleanup: (result: BriefSynthesisRunResult) => BriefSynthesisRunResult;
|
|
95
|
+
getFailureDiagnostics: LifecycleCtx<BriefSynthesisRunResult>["getFailureDiagnostics"];
|
|
96
96
|
}): BriefSynthesisRunResult | undefined {
|
|
97
|
-
const {
|
|
98
|
-
if (state.settled || state.aborting)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (brief) {
|
|
102
|
-
return cleanup({ kind: "success", brief });
|
|
103
|
-
}
|
|
104
|
-
const lastText = extractLastAssistantText(session.messages);
|
|
97
|
+
const { brief, state, cleanup, getFailureDiagnostics } = options;
|
|
98
|
+
if (state.settled || state.aborting) return undefined;
|
|
99
|
+
if (brief) return cleanup({ kind: "success", brief });
|
|
100
|
+
|
|
105
101
|
return cleanup({
|
|
106
102
|
kind: "failed",
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
: "Brief synthesizer did not produce any output.",
|
|
103
|
+
failureCode: "missing-structured-output",
|
|
104
|
+
diagnostics: getFailureDiagnostics(),
|
|
110
105
|
});
|
|
111
106
|
}
|
|
112
107
|
|
|
@@ -115,7 +110,7 @@ export async function runBriefSynthesis(
|
|
|
115
110
|
invocation: BriefSynthesisInvocation,
|
|
116
111
|
): Promise<BriefSynthesisRunResult> {
|
|
117
112
|
if (invocation.signal?.aborted) {
|
|
118
|
-
return { kind: "canceled" };
|
|
113
|
+
return { kind: "canceled", diagnostics: createEarlyCancellationDiagnostics() };
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
const resultHolder: { value: SynthesizedReviewBrief | undefined } = { value: undefined };
|
|
@@ -124,11 +119,8 @@ export async function runBriefSynthesis(
|
|
|
124
119
|
let session: AgentSession;
|
|
125
120
|
try {
|
|
126
121
|
session = await createBriefSession(invocation, submitBriefTool);
|
|
127
|
-
} catch
|
|
128
|
-
return {
|
|
129
|
-
kind: "failed",
|
|
130
|
-
reason: `Failed to create brief synthesis session: ${error instanceof Error ? error.message : String(error)}`,
|
|
131
|
-
};
|
|
122
|
+
} catch {
|
|
123
|
+
return { kind: "failed", failureCode: "session-creation-failed" };
|
|
132
124
|
}
|
|
133
125
|
|
|
134
126
|
return runWithLifecycle<BriefSynthesisRunResult>({
|
|
@@ -156,10 +148,10 @@ export async function runBriefSynthesis(
|
|
|
156
148
|
break;
|
|
157
149
|
case "agent_settled": {
|
|
158
150
|
const result = handleAgentSettled({
|
|
159
|
-
session,
|
|
160
151
|
brief: resultHolder.value,
|
|
161
152
|
state: ctx.state,
|
|
162
153
|
cleanup: ctx.cleanup,
|
|
154
|
+
getFailureDiagnostics: ctx.getFailureDiagnostics,
|
|
163
155
|
});
|
|
164
156
|
if (result) {
|
|
165
157
|
ctx.resolve(result);
|
|
@@ -170,11 +162,19 @@ export async function runBriefSynthesis(
|
|
|
170
162
|
break;
|
|
171
163
|
}
|
|
172
164
|
},
|
|
173
|
-
canceledResult: () => ({
|
|
174
|
-
|
|
165
|
+
canceledResult: (ctx) => ({
|
|
166
|
+
kind: "canceled" as const,
|
|
167
|
+
diagnostics: ctx.getFailureDiagnostics(),
|
|
168
|
+
}),
|
|
169
|
+
failedResult: (failureCode, ctx) => ({
|
|
175
170
|
kind: "failed" as const,
|
|
176
|
-
|
|
171
|
+
failureCode,
|
|
172
|
+
diagnostics: ctx.getFailureDiagnostics(),
|
|
173
|
+
}),
|
|
174
|
+
timeoutResult: (ms, ctx) => ({
|
|
175
|
+
kind: "timeout" as const,
|
|
176
|
+
timeoutMs: ms,
|
|
177
|
+
diagnostics: ctx.getFailureDiagnostics(),
|
|
177
178
|
}),
|
|
178
|
-
timeoutResult: (ms) => ({ kind: "timeout" as const, timeoutMs: ms }),
|
|
179
179
|
});
|
|
180
180
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type {
|
|
3
|
+
ChildFailureCode,
|
|
4
|
+
ChildFailureDiagnostics,
|
|
5
|
+
ChildStage,
|
|
6
|
+
ReviewProgress,
|
|
7
|
+
} from "../types.ts";
|
|
8
|
+
import {
|
|
9
|
+
type ChildLifecycleTrace,
|
|
10
|
+
ChildLifecycleTraceCollector,
|
|
11
|
+
formatChildLifecycleTrace,
|
|
12
|
+
getRegisteredToolNames,
|
|
13
|
+
toSafeAssistantStopReason,
|
|
14
|
+
} from "./child-lifecycle-trace.ts";
|
|
15
|
+
import { buildProgressTokens } from "./runner-helpers.ts";
|
|
16
|
+
|
|
17
|
+
/** Inputs used to create one safe non-success child-run diagnostic artifact. */
|
|
18
|
+
export interface BuildChildFailureDiagnosticsInput {
|
|
19
|
+
progress: ReviewProgress;
|
|
20
|
+
lifecycleTrace: ChildLifecycleTrace;
|
|
21
|
+
recentActivity: string[];
|
|
22
|
+
session?: AgentSession;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Build diagnostics from allowlisted control metadata only. */
|
|
26
|
+
export function buildChildFailureDiagnostics(
|
|
27
|
+
input: BuildChildFailureDiagnosticsInput,
|
|
28
|
+
): ChildFailureDiagnostics {
|
|
29
|
+
const { session } = input;
|
|
30
|
+
const tokens = session
|
|
31
|
+
? buildProgressTokens(() => session.getSessionStats())
|
|
32
|
+
: input.progress.tokens;
|
|
33
|
+
const lastAssistant = session ? extractLastAssistantMetadata(session) : undefined;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
lifecycleTrace: input.lifecycleTrace,
|
|
37
|
+
turns: input.progress.turns,
|
|
38
|
+
toolUses: input.progress.toolUses,
|
|
39
|
+
tokens,
|
|
40
|
+
recentActivity: input.recentActivity.length > 0 ? [...input.recentActivity] : undefined,
|
|
41
|
+
lastAssistantStopReason: lastAssistant?.stopReason,
|
|
42
|
+
lastAssistantToolCalls: lastAssistant?.toolCalls,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Create safe diagnostics when a host failure occurred without observed child lifecycle events. */
|
|
47
|
+
export function createUnobservedChildFailureDiagnostics(): ChildFailureDiagnostics {
|
|
48
|
+
return buildChildFailureDiagnostics({
|
|
49
|
+
progress: { turns: 0, toolUses: 0 },
|
|
50
|
+
lifecycleTrace: { entries: [], droppedCount: 0 },
|
|
51
|
+
recentActivity: [],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Create safe cancellation diagnostics when no child session was started. */
|
|
56
|
+
export function createEarlyCancellationDiagnostics(): ChildFailureDiagnostics {
|
|
57
|
+
const collector = new ChildLifecycleTraceCollector();
|
|
58
|
+
collector.recordHostMarker({ type: "abort_requested", reason: "canceled" });
|
|
59
|
+
return buildChildFailureDiagnostics({
|
|
60
|
+
progress: { turns: 0, toolUses: 0 },
|
|
61
|
+
lifecycleTrace: collector.snapshot(),
|
|
62
|
+
recentActivity: collector.recentActivitySnapshot(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Generate static parent-facing copy for a host-owned child failure code. */
|
|
67
|
+
export function formatChildFailureCopy(stage: ChildStage, code: ChildFailureCode): string {
|
|
68
|
+
const label = stage === "brief-synthesis" ? "Brief synthesis" : "Reviewer";
|
|
69
|
+
switch (code) {
|
|
70
|
+
case "session-creation-failed":
|
|
71
|
+
return `${label} session could not be created.`;
|
|
72
|
+
case "prompt-rejected":
|
|
73
|
+
return `${label} prompt was rejected before it ran.`;
|
|
74
|
+
case "missing-structured-output":
|
|
75
|
+
return `${label} ended without the required structured output.`;
|
|
76
|
+
case "unexpected-runner-failure":
|
|
77
|
+
return `${label} ended unexpectedly.`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Format the complete retained safe diagnostic artifact for parent-facing text. */
|
|
82
|
+
export function formatChildFailureDiagnostics(diagnostics: ChildFailureDiagnostics): string[] {
|
|
83
|
+
const lines = [
|
|
84
|
+
`- Turns: ${diagnostics.turns}`,
|
|
85
|
+
`- Tool uses: ${diagnostics.toolUses}`,
|
|
86
|
+
diagnostics.tokens
|
|
87
|
+
? `- Tokens: ${diagnostics.tokens.input} in / ${diagnostics.tokens.output} out / ${diagnostics.tokens.total} total`
|
|
88
|
+
: undefined,
|
|
89
|
+
diagnostics.recentActivity && diagnostics.recentActivity.length > 0
|
|
90
|
+
? `- Recent activity: ${diagnostics.recentActivity.join(" → ")}`
|
|
91
|
+
: undefined,
|
|
92
|
+
diagnostics.lastAssistantStopReason
|
|
93
|
+
? `- Last assistant stop: ${diagnostics.lastAssistantStopReason}`
|
|
94
|
+
: undefined,
|
|
95
|
+
diagnostics.lastAssistantToolCalls && diagnostics.lastAssistantToolCalls.length > 0
|
|
96
|
+
? `- Last assistant tools: ${diagnostics.lastAssistantToolCalls.join(", ")}`
|
|
97
|
+
: undefined,
|
|
98
|
+
`- ${formatChildLifecycleTrace(diagnostics.lifecycleTrace)}`,
|
|
99
|
+
];
|
|
100
|
+
return lines.filter((line): line is string => !!line);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface LastAssistantMetadata {
|
|
104
|
+
stopReason?: string;
|
|
105
|
+
toolCalls?: string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractLastAssistantMetadata(session: AgentSession): LastAssistantMetadata | undefined {
|
|
109
|
+
try {
|
|
110
|
+
const messages = session.messages as unknown;
|
|
111
|
+
if (!Array.isArray(messages)) return undefined;
|
|
112
|
+
|
|
113
|
+
const registeredToolNames = getRegisteredToolNames(session);
|
|
114
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
115
|
+
const message = messages[index] as Record<string, unknown> | undefined;
|
|
116
|
+
if (message?.role !== "assistant") continue;
|
|
117
|
+
|
|
118
|
+
const stopReason = toSafeAssistantStopReason(message.stopReason);
|
|
119
|
+
const toolCalls = extractAssistantToolCalls(message.content, registeredToolNames);
|
|
120
|
+
return {
|
|
121
|
+
stopReason,
|
|
122
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
} catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractAssistantToolCalls(content: unknown, registeredToolNames: Set<string>): string[] {
|
|
132
|
+
if (!Array.isArray(content)) return [];
|
|
133
|
+
|
|
134
|
+
return content
|
|
135
|
+
.map((part) => {
|
|
136
|
+
if (typeof part !== "object" || !part) return undefined;
|
|
137
|
+
const tool = part as { type?: unknown; name?: unknown };
|
|
138
|
+
return tool.type === "toolCall" &&
|
|
139
|
+
typeof tool.name === "string" &&
|
|
140
|
+
registeredToolNames.has(tool.name)
|
|
141
|
+
? tool.name
|
|
142
|
+
: undefined;
|
|
143
|
+
})
|
|
144
|
+
.filter((name): name is string => !!name);
|
|
145
|
+
}
|