@mrclrchtr/supi-review 2.6.1 → 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/review.ts +43 -29
- package/src/tool/agent-review-tools.ts +8 -8
- package/src/tool/agent-review-workflow.ts +41 -23
- package/src/tool/brief-runner.ts +25 -24
- 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 -43
- package/src/tool/runner-helpers.ts +0 -14
- package/src/tool/session-lifecycle.ts +105 -10
- package/src/types.ts +51 -37
- 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
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { ReviewProgress } from "../types.ts";
|
|
2
|
+
import type { ChildFailureCode, ChildFailureDiagnostics, ReviewProgress } from "../types.ts";
|
|
3
|
+
import { buildChildFailureDiagnostics } from "./child-failure-diagnostics.ts";
|
|
4
|
+
import {
|
|
5
|
+
type ChildLifecycleHostMarker,
|
|
6
|
+
type ChildLifecycleTrace,
|
|
7
|
+
ChildLifecycleTraceCollector,
|
|
8
|
+
getRegisteredToolNames,
|
|
9
|
+
} from "./child-lifecycle-trace.ts";
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Context passed to event handlers, timeout callbacks, and result factories.
|
|
@@ -22,6 +29,12 @@ export interface LifecycleCtx<TResult> {
|
|
|
22
29
|
state: { settled: boolean; aborting: boolean };
|
|
23
30
|
/** The managed agent session. */
|
|
24
31
|
session: AgentSession;
|
|
32
|
+
/** Snapshot the bounded Child Lifecycle Trace observed so far. */
|
|
33
|
+
getLifecycleTrace: () => ChildLifecycleTrace;
|
|
34
|
+
/** Build a safe non-success diagnostic artifact from the observed child run. */
|
|
35
|
+
getFailureDiagnostics: () => ChildFailureDiagnostics;
|
|
36
|
+
/** Add one allowlisted runner-control marker to the Child Lifecycle Trace. */
|
|
37
|
+
recordHostMarker: (marker: ChildLifecycleHostMarker) => void;
|
|
25
38
|
/**
|
|
26
39
|
* Register a teardown function that runs when cleanup is called.
|
|
27
40
|
* Useful for custom timers or resources set up by `onTimeout`.
|
|
@@ -55,12 +68,35 @@ export interface RunWithLifecycleConfig<TResult> {
|
|
|
55
68
|
onTimeout?: (ctx: LifecycleCtx<TResult>) => void;
|
|
56
69
|
/** Factory for the result produced when the abort signal fires. */
|
|
57
70
|
canceledResult: (ctx: LifecycleCtx<TResult>) => TResult;
|
|
58
|
-
/** Factory for the result produced when
|
|
59
|
-
failedResult: (
|
|
71
|
+
/** Factory for the result produced when prompt preflight or execution fails. */
|
|
72
|
+
failedResult: (
|
|
73
|
+
code: Extract<ChildFailureCode, "prompt-rejected" | "unexpected-runner-failure">,
|
|
74
|
+
ctx: LifecycleCtx<TResult>,
|
|
75
|
+
) => TResult;
|
|
60
76
|
/** Factory for the result produced when the timeout expires (default hard abort). */
|
|
61
77
|
timeoutResult: (timeoutMs: number, ctx: LifecycleCtx<TResult>) => TResult;
|
|
62
78
|
}
|
|
63
79
|
|
|
80
|
+
interface StartPromptOptions {
|
|
81
|
+
session: AgentSession;
|
|
82
|
+
prompt: string;
|
|
83
|
+
onPreflightRejected: () => void;
|
|
84
|
+
onFulfilled: () => void;
|
|
85
|
+
onUnexpectedFailure: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function startPrompt(options: StartPromptOptions): void {
|
|
89
|
+
const { session, prompt, onPreflightRejected, onFulfilled, onUnexpectedFailure } = options;
|
|
90
|
+
try {
|
|
91
|
+
const promptPromise = session.prompt(prompt, {
|
|
92
|
+
preflightResult: (accepted) => !accepted && onPreflightRejected(),
|
|
93
|
+
});
|
|
94
|
+
void promptPromise.then(onFulfilled, onUnexpectedFailure);
|
|
95
|
+
} catch {
|
|
96
|
+
onUnexpectedFailure();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
64
100
|
/**
|
|
65
101
|
* Manages the lifecycle of a child agent session: subscribes to events,
|
|
66
102
|
* wires abort-signal handling, enforces a timeout, and provides idempotent
|
|
@@ -74,6 +110,7 @@ export interface RunWithLifecycleConfig<TResult> {
|
|
|
74
110
|
* it by calling `ctx.resolve(ctx.cleanup(...))` itself)
|
|
75
111
|
* - `session.prompt()` rejects (resolves via `failedResult`)
|
|
76
112
|
*/
|
|
113
|
+
// biome-ignore lint/complexity/noExcessiveLinesPerFunction: lifecycle ownership must remain in one state-machine closure
|
|
77
114
|
export function runWithLifecycle<TResult>(
|
|
78
115
|
config: RunWithLifecycleConfig<TResult>,
|
|
79
116
|
): Promise<TResult> {
|
|
@@ -99,6 +136,7 @@ export function runWithLifecycle<TResult>(
|
|
|
99
136
|
aborting: false,
|
|
100
137
|
};
|
|
101
138
|
const teardownFns: (() => void)[] = [];
|
|
139
|
+
const lifecycleTrace = new ChildLifecycleTraceCollector(getRegisteredToolNames(session));
|
|
102
140
|
|
|
103
141
|
const cancelTeardown = (): void => {
|
|
104
142
|
for (const fn of teardownFns) {
|
|
@@ -132,18 +170,53 @@ export function runWithLifecycle<TResult>(
|
|
|
132
170
|
progress,
|
|
133
171
|
state,
|
|
134
172
|
session,
|
|
173
|
+
getLifecycleTrace: () => lifecycleTrace.snapshot(),
|
|
174
|
+
getFailureDiagnostics: () =>
|
|
175
|
+
buildChildFailureDiagnostics({
|
|
176
|
+
progress,
|
|
177
|
+
session,
|
|
178
|
+
lifecycleTrace: lifecycleTrace.snapshot(),
|
|
179
|
+
recentActivity: lifecycleTrace.recentActivitySnapshot(),
|
|
180
|
+
}),
|
|
181
|
+
recordHostMarker: (marker) => lifecycleTrace.recordHostMarker(marker),
|
|
135
182
|
addTeardown,
|
|
136
183
|
startTime,
|
|
137
184
|
};
|
|
138
185
|
|
|
186
|
+
let promptSettled = false;
|
|
187
|
+
let deferredAgentSettled: Extract<AgentSessionEvent, { type: "agent_settled" }> | undefined;
|
|
188
|
+
|
|
189
|
+
const dispatchEvent = (event: AgentSessionEvent): void => {
|
|
190
|
+
try {
|
|
191
|
+
onEvent(event, ctx);
|
|
192
|
+
} catch {
|
|
193
|
+
if (!state.settled && !state.aborting) {
|
|
194
|
+
resolve(cleanup(failedResult("unexpected-runner-failure", ctx)));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const flushDeferredSettlement = (): void => {
|
|
200
|
+
if (!deferredAgentSettled || state.settled || state.aborting) return;
|
|
201
|
+
const event = deferredAgentSettled;
|
|
202
|
+
deferredAgentSettled = undefined;
|
|
203
|
+
dispatchEvent(event);
|
|
204
|
+
};
|
|
205
|
+
|
|
139
206
|
session.subscribe((event: AgentSessionEvent) => {
|
|
140
|
-
|
|
207
|
+
lifecycleTrace.observe(event);
|
|
208
|
+
if (event.type === "agent_settled" && !promptSettled) {
|
|
209
|
+
deferredAgentSettled = event;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
dispatchEvent(event);
|
|
141
213
|
});
|
|
142
214
|
|
|
143
215
|
// Abort signal handler
|
|
144
216
|
const onAbort = () => {
|
|
145
217
|
if (state.settled || state.aborting) return;
|
|
146
218
|
state.aborting = true;
|
|
219
|
+
ctx.recordHostMarker({ type: "abort_requested", reason: "canceled" });
|
|
147
220
|
void session
|
|
148
221
|
.abort()
|
|
149
222
|
.catch(() => {})
|
|
@@ -163,12 +236,18 @@ export function runWithLifecycle<TResult>(
|
|
|
163
236
|
// Timeout handler
|
|
164
237
|
const onTimeoutExpired = () => {
|
|
165
238
|
if (state.settled || state.aborting) return;
|
|
239
|
+
ctx.recordHostMarker({ type: "timeout_expired" });
|
|
166
240
|
|
|
167
241
|
if (onTimeout) {
|
|
168
|
-
|
|
242
|
+
try {
|
|
243
|
+
onTimeout(ctx);
|
|
244
|
+
} catch {
|
|
245
|
+
resolve(cleanup(failedResult("unexpected-runner-failure", ctx)));
|
|
246
|
+
}
|
|
169
247
|
} else {
|
|
170
248
|
// Default: hard abort
|
|
171
249
|
state.aborting = true;
|
|
250
|
+
ctx.recordHostMarker({ type: "abort_requested", reason: "timeout" });
|
|
172
251
|
void session
|
|
173
252
|
.abort()
|
|
174
253
|
.catch(() => {})
|
|
@@ -181,11 +260,27 @@ export function runWithLifecycle<TResult>(
|
|
|
181
260
|
timeoutId.unref?.();
|
|
182
261
|
addTeardown(() => clearTimeout(timeoutId));
|
|
183
262
|
|
|
184
|
-
// Start the session
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
263
|
+
// Start the session. Prompt rejection is a distinct host-owned outcome;
|
|
264
|
+
// caught provider or runner errors are intentionally never retained.
|
|
265
|
+
const rejectPrompt = () => {
|
|
266
|
+
if (state.settled || state.aborting) return;
|
|
267
|
+
ctx.recordHostMarker({ type: "prompt_rejected" });
|
|
268
|
+
resolve(cleanup(failedResult("prompt-rejected", ctx)));
|
|
269
|
+
};
|
|
270
|
+
const failUnexpectedly = () => {
|
|
271
|
+
promptSettled = true;
|
|
272
|
+
if (state.settled || state.aborting) return;
|
|
273
|
+
resolve(cleanup(failedResult("unexpected-runner-failure", ctx)));
|
|
274
|
+
};
|
|
275
|
+
startPrompt({
|
|
276
|
+
session,
|
|
277
|
+
prompt,
|
|
278
|
+
onPreflightRejected: rejectPrompt,
|
|
279
|
+
onUnexpectedFailure: failUnexpectedly,
|
|
280
|
+
onFulfilled: () => {
|
|
281
|
+
promptSettled = true;
|
|
282
|
+
flushDeferredSettlement();
|
|
283
|
+
},
|
|
189
284
|
});
|
|
190
285
|
});
|
|
191
286
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ChildLifecycleTrace } from "./tool/child-lifecycle-trace.ts";
|
|
3
|
+
|
|
4
|
+
/** Closed, host-owned classification for a managed child failure. */
|
|
5
|
+
export type ChildFailureCode =
|
|
6
|
+
| "session-creation-failed"
|
|
7
|
+
| "prompt-rejected"
|
|
8
|
+
| "missing-structured-output"
|
|
9
|
+
| "unexpected-runner-failure";
|
|
10
|
+
|
|
11
|
+
/** Managed child role used for host-owned failure copy. */
|
|
12
|
+
export type ChildStage = "brief-synthesis" | "reviewer";
|
|
13
|
+
|
|
14
|
+
/** Failed child result fields with diagnostics required exactly when a child was observed. */
|
|
15
|
+
export type ChildFailedResult =
|
|
16
|
+
| { failureCode: "session-creation-failed"; diagnostics?: never }
|
|
17
|
+
| {
|
|
18
|
+
failureCode: Exclude<ChildFailureCode, "session-creation-failed">;
|
|
19
|
+
diagnostics: ChildFailureDiagnostics;
|
|
20
|
+
};
|
|
2
21
|
|
|
3
22
|
/** Inclusive 1-based line range reported by the reviewer. */
|
|
4
23
|
export interface ReviewLineRange {
|
|
@@ -166,23 +185,17 @@ export type AgentReviewerResult =
|
|
|
166
185
|
output: NormalizedReviewOutput;
|
|
167
186
|
modelId: string;
|
|
168
187
|
}
|
|
169
|
-
| {
|
|
170
|
-
kind: "failed";
|
|
171
|
-
reason: string;
|
|
172
|
-
modelId: string;
|
|
173
|
-
debug?: ReviewFailureDebugInfo;
|
|
174
|
-
}
|
|
188
|
+
| ({ kind: "failed"; modelId: string } & ChildFailedResult)
|
|
175
189
|
| {
|
|
176
190
|
kind: "canceled";
|
|
177
191
|
modelId: string;
|
|
178
|
-
|
|
192
|
+
diagnostics: ChildFailureDiagnostics;
|
|
179
193
|
}
|
|
180
194
|
| {
|
|
181
195
|
kind: "timeout";
|
|
182
196
|
timeoutMs: number;
|
|
183
|
-
partialOutput?: string;
|
|
184
197
|
modelId: string;
|
|
185
|
-
|
|
198
|
+
diagnostics: ChildFailureDiagnostics;
|
|
186
199
|
};
|
|
187
200
|
|
|
188
201
|
/** One normalized result from a focused reviewer child session. */
|
|
@@ -218,24 +231,6 @@ export interface ReviewPacket {
|
|
|
218
231
|
charBudget: number;
|
|
219
232
|
}
|
|
220
233
|
|
|
221
|
-
/** Lightweight diagnostics attached to non-success review runs. */
|
|
222
|
-
export interface ReviewFailureDebugInfo {
|
|
223
|
-
turns: number;
|
|
224
|
-
toolUses: number;
|
|
225
|
-
tokens?: {
|
|
226
|
-
input: number;
|
|
227
|
-
output: number;
|
|
228
|
-
total: number;
|
|
229
|
-
cacheRead?: number;
|
|
230
|
-
cacheWrite?: number;
|
|
231
|
-
};
|
|
232
|
-
recentEvents?: string[];
|
|
233
|
-
lastAssistantText?: string;
|
|
234
|
-
lastAssistantStopReason?: string;
|
|
235
|
-
lastAssistantErrorMessage?: string;
|
|
236
|
-
lastAssistantToolCalls?: string[];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
234
|
/** Fully prepared review run. */
|
|
240
235
|
export interface ReviewPlan {
|
|
241
236
|
model: ReviewModelSelection;
|
|
@@ -253,29 +248,26 @@ export type RawReviewResult =
|
|
|
253
248
|
brief?: SynthesizedReviewBrief;
|
|
254
249
|
modelId: string;
|
|
255
250
|
}
|
|
256
|
-
| {
|
|
251
|
+
| ({
|
|
257
252
|
kind: "failed";
|
|
258
|
-
reason: string;
|
|
259
253
|
snapshot: ReviewSnapshot;
|
|
260
254
|
brief?: SynthesizedReviewBrief;
|
|
261
255
|
modelId: string;
|
|
262
|
-
|
|
263
|
-
}
|
|
256
|
+
} & ChildFailedResult)
|
|
264
257
|
| {
|
|
265
258
|
kind: "canceled";
|
|
266
259
|
snapshot: ReviewSnapshot;
|
|
267
260
|
brief?: SynthesizedReviewBrief;
|
|
268
261
|
modelId: string;
|
|
269
|
-
|
|
262
|
+
diagnostics: ChildFailureDiagnostics;
|
|
270
263
|
}
|
|
271
264
|
| {
|
|
272
265
|
kind: "timeout";
|
|
273
266
|
snapshot: ReviewSnapshot;
|
|
274
267
|
timeoutMs: number;
|
|
275
|
-
partialOutput?: string;
|
|
276
268
|
brief?: SynthesizedReviewBrief;
|
|
277
269
|
modelId: string;
|
|
278
|
-
|
|
270
|
+
diagnostics: ChildFailureDiagnostics;
|
|
279
271
|
};
|
|
280
272
|
|
|
281
273
|
/** Normalized result used by rendering and follow-up logic. */
|
|
@@ -289,6 +281,28 @@ export type ReviewResult =
|
|
|
289
281
|
}
|
|
290
282
|
| Extract<RawReviewResult, { kind: "failed" | "canceled" | "timeout" }>;
|
|
291
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Safe bounded diagnostics attached only to a non-success managed child run.
|
|
286
|
+
*
|
|
287
|
+
* The trace and Recent Activity lane contain only allowlisted control metadata;
|
|
288
|
+
* no child-generated text, caught error, or raw SDK event is retained.
|
|
289
|
+
*/
|
|
290
|
+
export interface ChildFailureDiagnostics {
|
|
291
|
+
lifecycleTrace: ChildLifecycleTrace;
|
|
292
|
+
turns: number;
|
|
293
|
+
toolUses: number;
|
|
294
|
+
tokens?: {
|
|
295
|
+
input: number;
|
|
296
|
+
output: number;
|
|
297
|
+
total: number;
|
|
298
|
+
cacheRead?: number;
|
|
299
|
+
cacheWrite?: number;
|
|
300
|
+
};
|
|
301
|
+
recentActivity?: string[];
|
|
302
|
+
lastAssistantStopReason?: string;
|
|
303
|
+
lastAssistantToolCalls?: string[];
|
|
304
|
+
}
|
|
305
|
+
|
|
292
306
|
/** Progress state exposed by review/synthesis runners for widget integration. */
|
|
293
307
|
export interface ReviewProgress {
|
|
294
308
|
/** Number of agent turns completed. */
|
|
@@ -317,9 +331,9 @@ export interface ReviewProgress {
|
|
|
317
331
|
|
|
318
332
|
export type BriefSynthesisRunResult =
|
|
319
333
|
| { kind: "success"; brief: SynthesizedReviewBrief }
|
|
320
|
-
| { kind: "failed"
|
|
321
|
-
| { kind: "canceled" }
|
|
322
|
-
| { kind: "timeout"; timeoutMs: number };
|
|
334
|
+
| ({ kind: "failed" } & ChildFailedResult)
|
|
335
|
+
| { kind: "canceled"; diagnostics: ChildFailureDiagnostics }
|
|
336
|
+
| { kind: "timeout"; timeoutMs: number; diagnostics: ChildFailureDiagnostics };
|
|
323
337
|
|
|
324
338
|
export interface BriefSynthesisInvocation {
|
|
325
339
|
prompt: string;
|
package/src/ui/format-content.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
formatChildFailureCopy,
|
|
3
|
+
formatChildFailureDiagnostics,
|
|
4
|
+
} from "../tool/child-failure-diagnostics.ts";
|
|
5
|
+
import type {
|
|
6
|
+
BriefSynthesisRunResult,
|
|
7
|
+
ChildFailureDiagnostics,
|
|
8
|
+
ReviewItem,
|
|
9
|
+
ReviewResult,
|
|
10
|
+
} from "../types.ts";
|
|
2
11
|
|
|
3
12
|
/** Format a `impact` or `effort` level value. */
|
|
4
13
|
export function formatLevel(value: ReviewItem["impact"] | ReviewItem["effort"]): string {
|
|
@@ -10,6 +19,29 @@ export function formatLocation(file: string, startLine: number, endLine: number)
|
|
|
10
19
|
return `${file}:${startLine === endLine ? startLine : `${startLine}-${endLine}`}`;
|
|
11
20
|
}
|
|
12
21
|
|
|
22
|
+
/** Format one non-success brief-synthesis result for parent-facing custom-message content. */
|
|
23
|
+
export function formatBriefSynthesisFailureContent(
|
|
24
|
+
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
25
|
+
): string {
|
|
26
|
+
const parts = [formatBriefSynthesisFailureCopy(result)];
|
|
27
|
+
appendChildFailureDiagnostics(parts, result.diagnostics);
|
|
28
|
+
return parts.join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Generate short static parent-facing copy for a brief-synthesis outcome. */
|
|
32
|
+
export function formatBriefSynthesisFailureCopy(
|
|
33
|
+
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
34
|
+
): string {
|
|
35
|
+
switch (result.kind) {
|
|
36
|
+
case "failed":
|
|
37
|
+
return formatChildFailureCopy("brief-synthesis", result.failureCode);
|
|
38
|
+
case "canceled":
|
|
39
|
+
return "Brief synthesis was canceled.";
|
|
40
|
+
case "timeout":
|
|
41
|
+
return "Brief synthesis timed out.";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
/** Format review results for the LLM-visible custom message content. */
|
|
14
46
|
export function formatReviewContent(result: ReviewResult): string {
|
|
15
47
|
switch (result.kind) {
|
|
@@ -25,26 +57,31 @@ export function formatReviewContent(result: ReviewResult): string {
|
|
|
25
57
|
}
|
|
26
58
|
|
|
27
59
|
function formatFailureContent(result: Extract<ReviewResult, { kind: "failed" }>): string {
|
|
28
|
-
const parts = [
|
|
29
|
-
|
|
60
|
+
const parts = [formatChildFailureCopy("reviewer", result.failureCode)];
|
|
61
|
+
appendChildFailureDiagnostics(parts, result.diagnostics);
|
|
30
62
|
return parts.join("\n");
|
|
31
63
|
}
|
|
32
64
|
|
|
33
65
|
function formatCanceledContent(result: Extract<ReviewResult, { kind: "canceled" }>): string {
|
|
34
|
-
const parts = ["
|
|
35
|
-
|
|
66
|
+
const parts = ["Reviewer was canceled."];
|
|
67
|
+
appendChildFailureDiagnostics(parts, result.diagnostics);
|
|
36
68
|
return parts.join("\n");
|
|
37
69
|
}
|
|
38
70
|
|
|
39
71
|
function formatTimeoutContent(result: Extract<ReviewResult, { kind: "timeout" }>): string {
|
|
40
|
-
const parts = [
|
|
41
|
-
|
|
42
|
-
parts.push("", "Partial output:", result.partialOutput);
|
|
43
|
-
}
|
|
44
|
-
appendDebugContent(parts, result.debug);
|
|
72
|
+
const parts = ["Reviewer timed out."];
|
|
73
|
+
appendChildFailureDiagnostics(parts, result.diagnostics);
|
|
45
74
|
return parts.join("\n");
|
|
46
75
|
}
|
|
47
76
|
|
|
77
|
+
function appendChildFailureDiagnostics(
|
|
78
|
+
parts: string[],
|
|
79
|
+
diagnostics: ChildFailureDiagnostics | undefined,
|
|
80
|
+
): void {
|
|
81
|
+
if (!diagnostics) return;
|
|
82
|
+
parts.push("", "Diagnostics:", ...formatChildFailureDiagnostics(diagnostics));
|
|
83
|
+
}
|
|
84
|
+
|
|
48
85
|
function formatSuccessContent(result: Extract<ReviewResult, { kind: "success" }>): string {
|
|
49
86
|
const { output } = result;
|
|
50
87
|
const confidencePercent = Math.round(output.overall_confidence_score * 100);
|
|
@@ -81,34 +118,6 @@ function formatSuccessContent(result: Extract<ReviewResult, { kind: "success" }>
|
|
|
81
118
|
return lines.join("\n");
|
|
82
119
|
}
|
|
83
120
|
|
|
84
|
-
function appendDebugContent(parts: string[], debug: ReviewFailureDebugInfo | undefined): void {
|
|
85
|
-
if (!debug) return;
|
|
86
|
-
|
|
87
|
-
const lines = [
|
|
88
|
-
`- Turns: ${debug.turns}`,
|
|
89
|
-
`- Tool uses: ${debug.toolUses}`,
|
|
90
|
-
debug.tokens
|
|
91
|
-
? `- Tokens: ${debug.tokens.input} in / ${debug.tokens.output} out / ${debug.tokens.total} total`
|
|
92
|
-
: undefined,
|
|
93
|
-
debug.recentEvents && debug.recentEvents.length > 0
|
|
94
|
-
? `- Recent events: ${debug.recentEvents.join(" → ")}`
|
|
95
|
-
: undefined,
|
|
96
|
-
debug.lastAssistantStopReason
|
|
97
|
-
? `- Last assistant stop: ${debug.lastAssistantStopReason}`
|
|
98
|
-
: undefined,
|
|
99
|
-
debug.lastAssistantToolCalls && debug.lastAssistantToolCalls.length > 0
|
|
100
|
-
? `- Last assistant tools: ${debug.lastAssistantToolCalls.join(", ")}`
|
|
101
|
-
: undefined,
|
|
102
|
-
debug.lastAssistantErrorMessage
|
|
103
|
-
? `- Last assistant error: ${debug.lastAssistantErrorMessage}`
|
|
104
|
-
: undefined,
|
|
105
|
-
].filter((line): line is string => !!line);
|
|
106
|
-
|
|
107
|
-
if (lines.length === 0) return;
|
|
108
|
-
|
|
109
|
-
parts.push("", "Debug:", ...lines);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
121
|
function formatReviewItems(items: ReviewItem[]): string[] {
|
|
113
122
|
return items.flatMap((item, index) => {
|
|
114
123
|
const location = item.code_location
|
package/src/ui/renderer.ts
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
formatChildFailureCopy,
|
|
5
|
+
formatChildFailureDiagnostics,
|
|
6
|
+
} from "../tool/child-failure-diagnostics.ts";
|
|
7
|
+
import type {
|
|
8
|
+
BriefSynthesisRunResult,
|
|
9
|
+
ChildFailureDiagnostics,
|
|
10
|
+
ReviewItem,
|
|
11
|
+
ReviewResult,
|
|
12
|
+
ReviewSnapshot,
|
|
13
|
+
} from "../types.ts";
|
|
14
|
+
import { formatBriefSynthesisFailureCopy, formatLevel, formatLocation } from "./format-content.ts";
|
|
5
15
|
|
|
6
16
|
/** Register the custom TUI renderer for `supi-review` messages. */
|
|
17
|
+
interface BriefSynthesisFailureMessage {
|
|
18
|
+
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>;
|
|
19
|
+
snapshot: ReviewSnapshot;
|
|
20
|
+
modelId: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ReviewMessageDetails {
|
|
24
|
+
result?: ReviewResult;
|
|
25
|
+
briefSynthesisFailure?: BriefSynthesisFailureMessage;
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
export function registerReviewRenderer(pi: ExtensionAPI): void {
|
|
8
29
|
pi.registerMessageRenderer("supi-review", (message, { expanded }, theme) => {
|
|
9
|
-
const
|
|
30
|
+
const details = message.details as ReviewMessageDetails | undefined;
|
|
31
|
+
if (details?.briefSynthesisFailure) {
|
|
32
|
+
return renderBriefSynthesisFailure(details.briefSynthesisFailure, theme);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = details?.result;
|
|
10
36
|
if (!result) {
|
|
11
37
|
return new Text(theme.fg("dim", "No review data"), 1, 0);
|
|
12
38
|
}
|
|
@@ -26,6 +52,31 @@ export function registerReviewRenderer(pi: ExtensionAPI): void {
|
|
|
26
52
|
});
|
|
27
53
|
}
|
|
28
54
|
|
|
55
|
+
function renderBriefSynthesisFailure(
|
|
56
|
+
failure: BriefSynthesisFailureMessage,
|
|
57
|
+
theme: Parameters<Parameters<ExtensionAPI["registerMessageRenderer"]>[1]>[2],
|
|
58
|
+
): Container {
|
|
59
|
+
const container = new Container();
|
|
60
|
+
const title =
|
|
61
|
+
failure.result.kind === "failed"
|
|
62
|
+
? "◆ Brief Synthesis Failed"
|
|
63
|
+
: failure.result.kind === "timeout"
|
|
64
|
+
? "◆ Brief Synthesis Timed Out"
|
|
65
|
+
: "◆ Brief Synthesis Canceled";
|
|
66
|
+
const color = failure.result.kind === "failed" ? "error" : "warning";
|
|
67
|
+
|
|
68
|
+
container.addChild(new Text(theme.fg(color, title), 1, 0));
|
|
69
|
+
container.addChild(new Spacer(1));
|
|
70
|
+
container.addChild(new Text(theme.fg("muted", `Model: ${failure.modelId}`), 1, 0));
|
|
71
|
+
container.addChild(new Text(theme.fg("muted", `Snapshot: ${failure.snapshot.title}`), 1, 0));
|
|
72
|
+
container.addChild(new Spacer(1));
|
|
73
|
+
container.addChild(
|
|
74
|
+
new Text(theme.fg(color, formatBriefSynthesisFailureCopy(failure.result)), 1, 0),
|
|
75
|
+
);
|
|
76
|
+
renderChildFailureDiagnostics(container, failure.result.diagnostics, theme);
|
|
77
|
+
return container;
|
|
78
|
+
}
|
|
79
|
+
|
|
29
80
|
function renderSuccess(
|
|
30
81
|
result: Extract<ReviewResult, { kind: "success" }>,
|
|
31
82
|
theme: Parameters<Parameters<ExtensionAPI["registerMessageRenderer"]>[1]>[2],
|
|
@@ -160,8 +211,10 @@ function renderFailed(
|
|
|
160
211
|
container.addChild(new Text(theme.fg("muted", `Model: ${result.modelId}`), 1, 0));
|
|
161
212
|
container.addChild(new Text(theme.fg("muted", `Snapshot: ${result.snapshot.title}`), 1, 0));
|
|
162
213
|
container.addChild(new Spacer(1));
|
|
163
|
-
container.addChild(
|
|
164
|
-
|
|
214
|
+
container.addChild(
|
|
215
|
+
new Text(theme.fg("error", formatChildFailureCopy("reviewer", result.failureCode)), 1, 0),
|
|
216
|
+
);
|
|
217
|
+
renderChildFailureDiagnostics(container, result.diagnostics, theme);
|
|
165
218
|
return container;
|
|
166
219
|
}
|
|
167
220
|
|
|
@@ -175,19 +228,8 @@ function renderTimeout(
|
|
|
175
228
|
container.addChild(new Text(theme.fg("muted", `Model: ${result.modelId}`), 1, 0));
|
|
176
229
|
container.addChild(new Text(theme.fg("muted", `Snapshot: ${result.snapshot.title}`), 1, 0));
|
|
177
230
|
container.addChild(new Spacer(1));
|
|
178
|
-
container.addChild(
|
|
179
|
-
|
|
180
|
-
theme.fg("warning", `Reviewer exceeded the ${(result.timeoutMs / 1000).toFixed(0)}s timeout`),
|
|
181
|
-
1,
|
|
182
|
-
0,
|
|
183
|
-
),
|
|
184
|
-
);
|
|
185
|
-
if (result.partialOutput) {
|
|
186
|
-
container.addChild(new Spacer(1));
|
|
187
|
-
container.addChild(new Text(theme.fg("dim", "Partial output:"), 1, 0));
|
|
188
|
-
container.addChild(new Text(theme.fg("dim", result.partialOutput.slice(0, 500)), 1, 0));
|
|
189
|
-
}
|
|
190
|
-
renderFailureDebug(container, result.debug, theme);
|
|
231
|
+
container.addChild(new Text(theme.fg("warning", "Reviewer timed out."), 1, 0));
|
|
232
|
+
renderChildFailureDiagnostics(container, result.diagnostics, theme);
|
|
191
233
|
return container;
|
|
192
234
|
}
|
|
193
235
|
|
|
@@ -197,41 +239,21 @@ function renderCanceled(
|
|
|
197
239
|
): Container {
|
|
198
240
|
const container = new Container();
|
|
199
241
|
container.addChild(new Text(theme.fg("warning", "◆ Review Canceled"), 1, 0));
|
|
200
|
-
|
|
242
|
+
container.addChild(new Text(theme.fg("warning", "Reviewer was canceled."), 1, 0));
|
|
243
|
+
renderChildFailureDiagnostics(container, result.diagnostics, theme);
|
|
201
244
|
return container;
|
|
202
245
|
}
|
|
203
246
|
|
|
204
|
-
function
|
|
247
|
+
function renderChildFailureDiagnostics(
|
|
205
248
|
container: Container,
|
|
206
|
-
|
|
249
|
+
diagnostics: ChildFailureDiagnostics | undefined,
|
|
207
250
|
theme: Parameters<Parameters<ExtensionAPI["registerMessageRenderer"]>[1]>[2],
|
|
208
251
|
): void {
|
|
209
|
-
if (!
|
|
210
|
-
|
|
211
|
-
const lines = [
|
|
212
|
-
`Turns: ${debug.turns} · Tool uses: ${debug.toolUses}`,
|
|
213
|
-
debug.tokens
|
|
214
|
-
? `Tokens: ${debug.tokens.input} in / ${debug.tokens.output} out / ${debug.tokens.total} total`
|
|
215
|
-
: undefined,
|
|
216
|
-
debug.recentEvents && debug.recentEvents.length > 0
|
|
217
|
-
? `Recent events: ${debug.recentEvents.join(" → ")}`
|
|
218
|
-
: undefined,
|
|
219
|
-
debug.lastAssistantStopReason
|
|
220
|
-
? `Last assistant stop: ${debug.lastAssistantStopReason}`
|
|
221
|
-
: undefined,
|
|
222
|
-
debug.lastAssistantToolCalls && debug.lastAssistantToolCalls.length > 0
|
|
223
|
-
? `Last assistant tools: ${debug.lastAssistantToolCalls.join(", ")}`
|
|
224
|
-
: undefined,
|
|
225
|
-
debug.lastAssistantErrorMessage
|
|
226
|
-
? `Last assistant error: ${debug.lastAssistantErrorMessage}`
|
|
227
|
-
: undefined,
|
|
228
|
-
].filter((line): line is string => !!line);
|
|
229
|
-
|
|
230
|
-
if (lines.length === 0) return;
|
|
252
|
+
if (!diagnostics) return;
|
|
231
253
|
|
|
232
254
|
container.addChild(new Spacer(1));
|
|
233
|
-
container.addChild(new Text(theme.fg("dim", "
|
|
234
|
-
for (const line of
|
|
255
|
+
container.addChild(new Text(theme.fg("dim", "Diagnostics:"), 1, 0));
|
|
256
|
+
for (const line of formatChildFailureDiagnostics(diagnostics)) {
|
|
235
257
|
container.addChild(new Text(theme.fg("dim", line), 1, 0));
|
|
236
258
|
}
|
|
237
259
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { PrepareAgentReviewInput, RunAgentReviewInput } from "../tool/agent-review-schemas.ts";
|
|
4
|
+
import {
|
|
5
|
+
formatChildFailureCopy,
|
|
6
|
+
formatChildFailureDiagnostics,
|
|
7
|
+
} from "../tool/child-failure-diagnostics.ts";
|
|
4
8
|
import type {
|
|
5
9
|
AgentReviewBatchDetails,
|
|
6
10
|
PreparedAgentReviewDetails,
|
|
@@ -150,6 +154,7 @@ export function renderRunReviewResult(
|
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
if (expanded) {
|
|
157
|
+
renderReviewerDiagnostics(container, details, theme);
|
|
153
158
|
container.addChild(new Spacer(1));
|
|
154
159
|
for (const line of formatCritique(details.evaluation.critique)) {
|
|
155
160
|
container.addChild(new Text(theme.fg("dim", line), 0, 0));
|
|
@@ -165,6 +170,28 @@ export function renderRunReviewResult(
|
|
|
165
170
|
return container;
|
|
166
171
|
}
|
|
167
172
|
|
|
173
|
+
function renderReviewerDiagnostics(
|
|
174
|
+
container: Container,
|
|
175
|
+
details: AgentReviewBatchDetails,
|
|
176
|
+
theme: ReviewTheme,
|
|
177
|
+
): void {
|
|
178
|
+
if (
|
|
179
|
+
!details.results.some((entry) => entry.result.kind !== "success" && entry.result.diagnostics)
|
|
180
|
+
) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
container.addChild(new Spacer(1));
|
|
185
|
+
container.addChild(new Text(theme.fg("accent", "Reviewer diagnostics"), 0, 0));
|
|
186
|
+
for (const { assignment, result } of details.results) {
|
|
187
|
+
if (result.kind === "success" || !result.diagnostics) continue;
|
|
188
|
+
container.addChild(new Text(theme.fg("muted", `${assignment.id}:`), 0, 0));
|
|
189
|
+
for (const line of formatChildFailureDiagnostics(result.diagnostics)) {
|
|
190
|
+
container.addChild(new Text(theme.fg("dim", line), 0, 0));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
168
195
|
function renderProgress(details: AgentReviewProgressDetails, theme: ReviewTheme): Text {
|
|
169
196
|
if (details.phase === "prepare") {
|
|
170
197
|
return new Text(theme.fg("warning", "Synthesizing review brief…"), 0, 0);
|
|
@@ -189,7 +216,11 @@ function summarizeResult(result: AgentReviewBatchDetails["results"][number]["res
|
|
|
189
216
|
text: `${result.output.items.length} item(s), ${result.output.overall_correctness}`,
|
|
190
217
|
};
|
|
191
218
|
case "failed":
|
|
192
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
icon: "✗",
|
|
221
|
+
color: "error",
|
|
222
|
+
text: formatChildFailureCopy("reviewer", result.failureCode),
|
|
223
|
+
};
|
|
193
224
|
case "canceled":
|
|
194
225
|
return { icon: "○", color: "warning", text: "canceled" };
|
|
195
226
|
case "timeout":
|