@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
|
@@ -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,5 +1,23 @@
|
|
|
1
1
|
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import type {
|
|
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
|
+
};
|
|
3
21
|
|
|
4
22
|
/** Inclusive 1-based line range reported by the reviewer. */
|
|
5
23
|
export interface ReviewLineRange {
|
|
@@ -167,23 +185,17 @@ export type AgentReviewerResult =
|
|
|
167
185
|
output: NormalizedReviewOutput;
|
|
168
186
|
modelId: string;
|
|
169
187
|
}
|
|
170
|
-
| {
|
|
171
|
-
kind: "failed";
|
|
172
|
-
reason: string;
|
|
173
|
-
modelId: string;
|
|
174
|
-
debug?: ReviewFailureDebugInfo;
|
|
175
|
-
}
|
|
188
|
+
| ({ kind: "failed"; modelId: string } & ChildFailedResult)
|
|
176
189
|
| {
|
|
177
190
|
kind: "canceled";
|
|
178
191
|
modelId: string;
|
|
179
|
-
|
|
192
|
+
diagnostics: ChildFailureDiagnostics;
|
|
180
193
|
}
|
|
181
194
|
| {
|
|
182
195
|
kind: "timeout";
|
|
183
196
|
timeoutMs: number;
|
|
184
|
-
partialOutput?: string;
|
|
185
197
|
modelId: string;
|
|
186
|
-
|
|
198
|
+
diagnostics: ChildFailureDiagnostics;
|
|
187
199
|
};
|
|
188
200
|
|
|
189
201
|
/** One normalized result from a focused reviewer child session. */
|
|
@@ -219,24 +231,6 @@ export interface ReviewPacket {
|
|
|
219
231
|
charBudget: number;
|
|
220
232
|
}
|
|
221
233
|
|
|
222
|
-
/** Lightweight diagnostics attached to non-success review runs. */
|
|
223
|
-
export interface ReviewFailureDebugInfo {
|
|
224
|
-
turns: number;
|
|
225
|
-
toolUses: number;
|
|
226
|
-
tokens?: {
|
|
227
|
-
input: number;
|
|
228
|
-
output: number;
|
|
229
|
-
total: number;
|
|
230
|
-
cacheRead?: number;
|
|
231
|
-
cacheWrite?: number;
|
|
232
|
-
};
|
|
233
|
-
recentEvents?: string[];
|
|
234
|
-
lastAssistantText?: string;
|
|
235
|
-
lastAssistantStopReason?: string;
|
|
236
|
-
lastAssistantErrorMessage?: string;
|
|
237
|
-
lastAssistantToolCalls?: string[];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
234
|
/** Fully prepared review run. */
|
|
241
235
|
export interface ReviewPlan {
|
|
242
236
|
model: ReviewModelSelection;
|
|
@@ -254,29 +248,26 @@ export type RawReviewResult =
|
|
|
254
248
|
brief?: SynthesizedReviewBrief;
|
|
255
249
|
modelId: string;
|
|
256
250
|
}
|
|
257
|
-
| {
|
|
251
|
+
| ({
|
|
258
252
|
kind: "failed";
|
|
259
|
-
reason: string;
|
|
260
253
|
snapshot: ReviewSnapshot;
|
|
261
254
|
brief?: SynthesizedReviewBrief;
|
|
262
255
|
modelId: string;
|
|
263
|
-
|
|
264
|
-
}
|
|
256
|
+
} & ChildFailedResult)
|
|
265
257
|
| {
|
|
266
258
|
kind: "canceled";
|
|
267
259
|
snapshot: ReviewSnapshot;
|
|
268
260
|
brief?: SynthesizedReviewBrief;
|
|
269
261
|
modelId: string;
|
|
270
|
-
|
|
262
|
+
diagnostics: ChildFailureDiagnostics;
|
|
271
263
|
}
|
|
272
264
|
| {
|
|
273
265
|
kind: "timeout";
|
|
274
266
|
snapshot: ReviewSnapshot;
|
|
275
267
|
timeoutMs: number;
|
|
276
|
-
partialOutput?: string;
|
|
277
268
|
brief?: SynthesizedReviewBrief;
|
|
278
269
|
modelId: string;
|
|
279
|
-
|
|
270
|
+
diagnostics: ChildFailureDiagnostics;
|
|
280
271
|
};
|
|
281
272
|
|
|
282
273
|
/** Normalized result used by rendering and follow-up logic. */
|
|
@@ -290,6 +281,28 @@ export type ReviewResult =
|
|
|
290
281
|
}
|
|
291
282
|
| Extract<RawReviewResult, { kind: "failed" | "canceled" | "timeout" }>;
|
|
292
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
|
+
|
|
293
306
|
/** Progress state exposed by review/synthesis runners for widget integration. */
|
|
294
307
|
export interface ReviewProgress {
|
|
295
308
|
/** Number of agent turns completed. */
|
|
@@ -318,15 +331,14 @@ export interface ReviewProgress {
|
|
|
318
331
|
|
|
319
332
|
export type BriefSynthesisRunResult =
|
|
320
333
|
| { kind: "success"; brief: SynthesizedReviewBrief }
|
|
321
|
-
| { kind: "failed"
|
|
322
|
-
| { kind: "canceled" }
|
|
323
|
-
| { kind: "timeout"; timeoutMs: number };
|
|
334
|
+
| ({ kind: "failed" } & ChildFailedResult)
|
|
335
|
+
| { kind: "canceled"; diagnostics: ChildFailureDiagnostics }
|
|
336
|
+
| { kind: "timeout"; timeoutMs: number; diagnostics: ChildFailureDiagnostics };
|
|
324
337
|
|
|
325
338
|
export interface BriefSynthesisInvocation {
|
|
326
339
|
prompt: string;
|
|
327
340
|
// biome-ignore lint/suspicious/noExplicitAny: Model<any> is pi's canonical type
|
|
328
341
|
model: Model<any>;
|
|
329
|
-
modelRegistry?: ModelRegistry;
|
|
330
342
|
cwd: string;
|
|
331
343
|
signal?: AbortSignal;
|
|
332
344
|
timeoutMs?: number;
|
|
@@ -336,7 +348,6 @@ export interface BriefSynthesisInvocation {
|
|
|
336
348
|
export interface ReviewInvocation {
|
|
337
349
|
prompt: string;
|
|
338
350
|
model: ReviewModelSelection;
|
|
339
|
-
modelRegistry?: ModelRegistry;
|
|
340
351
|
cwd: string;
|
|
341
352
|
signal?: AbortSignal;
|
|
342
353
|
snapshot: ReviewSnapshot;
|
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":
|