@mrclrchtr/supi-review 2.6.1 → 2.8.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/README.md +24 -3
- package/node_modules/@mrclrchtr/supi-core/README.md +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +1 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +31 -14
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
- package/package.json +2 -2
- package/src/config.ts +55 -0
- package/src/model.ts +33 -0
- package/src/review.ts +45 -29
- package/src/tool/agent-review-tools.ts +20 -11
- 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
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
SessionManager,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { RawReviewResult, ReviewInvocation, ReviewOutputEvent } from "../types.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { createEarlyCancellationDiagnostics } from "./child-failure-diagnostics.ts";
|
|
10
10
|
import {
|
|
11
11
|
createSubmitReviewTool,
|
|
12
12
|
handleSessionEvent,
|
|
@@ -69,16 +69,12 @@ function buildTimeoutResult(
|
|
|
69
69
|
runner: ReviewerRunnerState,
|
|
70
70
|
): RawReviewResult {
|
|
71
71
|
return {
|
|
72
|
-
kind: "timeout"
|
|
72
|
+
kind: "timeout",
|
|
73
73
|
snapshot: runner.invocation.snapshot,
|
|
74
74
|
timeoutMs: runner.invocation.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
75
75
|
brief: runner.invocation.brief,
|
|
76
76
|
modelId: runner.invocation.model.canonicalId,
|
|
77
|
-
|
|
78
|
-
progress: lcCtx.progress,
|
|
79
|
-
session: lcCtx.session,
|
|
80
|
-
recentEvents: runner.debug.recentEvents,
|
|
81
|
-
}),
|
|
77
|
+
diagnostics: lcCtx.getFailureDiagnostics(),
|
|
82
78
|
};
|
|
83
79
|
}
|
|
84
80
|
|
|
@@ -87,34 +83,26 @@ function buildCanceledResult(
|
|
|
87
83
|
runner: ReviewerRunnerState,
|
|
88
84
|
): RawReviewResult {
|
|
89
85
|
return {
|
|
90
|
-
kind: "canceled"
|
|
86
|
+
kind: "canceled",
|
|
91
87
|
snapshot: runner.invocation.snapshot,
|
|
92
88
|
brief: runner.invocation.brief,
|
|
93
89
|
modelId: runner.invocation.model.canonicalId,
|
|
94
|
-
|
|
95
|
-
progress: lcCtx.progress,
|
|
96
|
-
session: lcCtx.session,
|
|
97
|
-
recentEvents: runner.debug.recentEvents,
|
|
98
|
-
}),
|
|
90
|
+
diagnostics: lcCtx.getFailureDiagnostics(),
|
|
99
91
|
};
|
|
100
92
|
}
|
|
101
93
|
|
|
102
94
|
function buildFailedResult(
|
|
103
|
-
|
|
95
|
+
failureCode: "prompt-rejected" | "unexpected-runner-failure",
|
|
104
96
|
lcCtx: LifecycleCtx<RawReviewResult>,
|
|
105
97
|
runner: ReviewerRunnerState,
|
|
106
98
|
): RawReviewResult {
|
|
107
99
|
return {
|
|
108
|
-
kind: "failed"
|
|
109
|
-
|
|
100
|
+
kind: "failed",
|
|
101
|
+
failureCode,
|
|
110
102
|
snapshot: runner.invocation.snapshot,
|
|
111
103
|
brief: runner.invocation.brief,
|
|
112
104
|
modelId: runner.invocation.model.canonicalId,
|
|
113
|
-
|
|
114
|
-
progress: lcCtx.progress,
|
|
115
|
-
session: lcCtx.session,
|
|
116
|
-
recentEvents: runner.debug.recentEvents,
|
|
117
|
-
}),
|
|
105
|
+
diagnostics: lcCtx.getFailureDiagnostics(),
|
|
118
106
|
};
|
|
119
107
|
}
|
|
120
108
|
|
|
@@ -124,49 +112,43 @@ interface ReviewerRunnerState {
|
|
|
124
112
|
submitSteered: boolean;
|
|
125
113
|
timeoutSteered: boolean;
|
|
126
114
|
graceTurnsRemaining: number | undefined;
|
|
127
|
-
debug: { recentEvents: string[] };
|
|
128
115
|
}
|
|
129
116
|
|
|
130
117
|
// ---------------------------------------------------------------------------
|
|
131
118
|
// Steer / abort helpers
|
|
132
119
|
// ---------------------------------------------------------------------------
|
|
133
120
|
|
|
134
|
-
/** Abort the session and resolve with a timeout result
|
|
121
|
+
/** Abort the session and resolve with a timeout result from the lifecycle context. */
|
|
135
122
|
function doFinalAbortFromLifecycle(
|
|
136
123
|
lcCtx: LifecycleCtx<RawReviewResult>,
|
|
137
124
|
runner: ReviewerRunnerState,
|
|
138
125
|
): void {
|
|
126
|
+
if (lcCtx.state.settled || lcCtx.state.aborting) return;
|
|
127
|
+
|
|
139
128
|
lcCtx.state.aborting = true;
|
|
129
|
+
lcCtx.recordHostMarker({ type: "abort_requested", reason: "timeout" });
|
|
140
130
|
void lcCtx.session
|
|
141
131
|
.abort()
|
|
142
132
|
.catch(() => {})
|
|
143
133
|
.finally(() => {
|
|
144
|
-
const partialText = extractLastAssistantDebug(lcCtx.session)?.text;
|
|
145
134
|
lcCtx.resolve(
|
|
146
135
|
lcCtx.cleanup({
|
|
147
|
-
kind: "timeout"
|
|
136
|
+
kind: "timeout",
|
|
148
137
|
snapshot: runner.invocation.snapshot,
|
|
149
138
|
timeoutMs: runner.invocation.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
150
|
-
partialOutput: partialText,
|
|
151
139
|
brief: runner.invocation.brief,
|
|
152
140
|
modelId: runner.invocation.model.canonicalId,
|
|
153
|
-
|
|
154
|
-
progress: lcCtx.progress,
|
|
155
|
-
session: lcCtx.session,
|
|
156
|
-
recentEvents: runner.debug.recentEvents,
|
|
157
|
-
}),
|
|
141
|
+
diagnostics: lcCtx.getFailureDiagnostics(),
|
|
158
142
|
}),
|
|
159
143
|
);
|
|
160
144
|
});
|
|
161
145
|
}
|
|
162
146
|
|
|
163
147
|
/**
|
|
164
|
-
* Build the custom
|
|
148
|
+
* Build the custom soft-timeout behavior for reviewer sessions.
|
|
165
149
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* 2. Starts a grace timer (hard abort after GRACE_TURNS or HARD_ABORT_GRACE_MS)
|
|
169
|
-
* 3. If steer fails, immediately hard-aborts
|
|
150
|
+
* The shared lifecycle runner records `timeout_expired`; this runner adds a
|
|
151
|
+
* timeout steering marker and later records the hard-abort marker if needed.
|
|
170
152
|
*/
|
|
171
153
|
function buildReviewTimeoutHandler(
|
|
172
154
|
runner: ReviewerRunnerState,
|
|
@@ -174,9 +156,9 @@ function buildReviewTimeoutHandler(
|
|
|
174
156
|
): void {
|
|
175
157
|
runner.timeoutSteered = true;
|
|
176
158
|
runner.graceTurnsRemaining = GRACE_TURNS;
|
|
159
|
+
lcCtx.recordHostMarker({ type: "steer_requested", reason: "timeout" });
|
|
177
160
|
|
|
178
161
|
const hardAbortTimer = setTimeout(() => {
|
|
179
|
-
if (lcCtx.state.settled) return;
|
|
180
162
|
doFinalAbortFromLifecycle(lcCtx, runner);
|
|
181
163
|
}, HARD_ABORT_GRACE_MS);
|
|
182
164
|
hardAbortTimer.unref?.();
|
|
@@ -199,7 +181,6 @@ function buildRunnerCtx(runner: ReviewerRunnerState): RunnerContext {
|
|
|
199
181
|
ctx.submitSteered = runner.submitSteered;
|
|
200
182
|
ctx.timeoutSteered = runner.timeoutSteered;
|
|
201
183
|
ctx.graceTurnsRemaining = runner.graceTurnsRemaining;
|
|
202
|
-
ctx.debug = runner.debug;
|
|
203
184
|
ctx.toolCounts = {};
|
|
204
185
|
ctx.inspectedFiles = new Set();
|
|
205
186
|
return ctx;
|
|
@@ -216,6 +197,8 @@ function syncCtxFromLifecycle(
|
|
|
216
197
|
ctx.cleanup = lcCtx.cleanup;
|
|
217
198
|
ctx.state = lcCtx.state;
|
|
218
199
|
ctx.startTime = lcCtx.startTime;
|
|
200
|
+
ctx.getFailureDiagnostics = lcCtx.getFailureDiagnostics;
|
|
201
|
+
ctx.recordHostMarker = lcCtx.recordHostMarker;
|
|
219
202
|
ctx.submitSteered = runner.submitSteered;
|
|
220
203
|
ctx.timeoutSteered = runner.timeoutSteered;
|
|
221
204
|
ctx.graceTurnsRemaining = runner.graceTurnsRemaining;
|
|
@@ -239,6 +222,7 @@ export async function runReviewer(invocation: ReviewInvocation): Promise<RawRevi
|
|
|
239
222
|
snapshot: invocation.snapshot,
|
|
240
223
|
brief: invocation.brief,
|
|
241
224
|
modelId: invocation.model.canonicalId,
|
|
225
|
+
diagnostics: createEarlyCancellationDiagnostics(),
|
|
242
226
|
};
|
|
243
227
|
}
|
|
244
228
|
|
|
@@ -255,10 +239,10 @@ export async function runReviewer(invocation: ReviewInvocation): Promise<RawRevi
|
|
|
255
239
|
snapshotDiffTool,
|
|
256
240
|
snapshotFileTool,
|
|
257
241
|
);
|
|
258
|
-
} catch
|
|
242
|
+
} catch {
|
|
259
243
|
return {
|
|
260
244
|
kind: "failed",
|
|
261
|
-
|
|
245
|
+
failureCode: "session-creation-failed",
|
|
262
246
|
snapshot: invocation.snapshot,
|
|
263
247
|
brief: invocation.brief,
|
|
264
248
|
modelId: invocation.model.canonicalId,
|
|
@@ -271,9 +255,7 @@ export async function runReviewer(invocation: ReviewInvocation): Promise<RawRevi
|
|
|
271
255
|
submitSteered: false,
|
|
272
256
|
timeoutSteered: false,
|
|
273
257
|
graceTurnsRemaining: undefined,
|
|
274
|
-
debug: { recentEvents: [] },
|
|
275
258
|
};
|
|
276
|
-
|
|
277
259
|
const ctx = buildRunnerCtx(runner);
|
|
278
260
|
|
|
279
261
|
return runWithLifecycle<RawReviewResult>({
|
|
@@ -288,7 +270,7 @@ export async function runReviewer(invocation: ReviewInvocation): Promise<RawRevi
|
|
|
288
270
|
},
|
|
289
271
|
onTimeout: (lcCtx) => buildReviewTimeoutHandler(runner, lcCtx),
|
|
290
272
|
canceledResult: (lcCtx) => buildCanceledResult(lcCtx, runner),
|
|
291
|
-
failedResult: (
|
|
273
|
+
failedResult: (failureCode, lcCtx) => buildFailedResult(failureCode, lcCtx, runner),
|
|
292
274
|
timeoutResult: (_, lcCtx) => buildTimeoutResult(lcCtx, runner),
|
|
293
275
|
});
|
|
294
276
|
}
|
|
@@ -5,20 +5,6 @@
|
|
|
5
5
|
* eliminate duplication.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/** Extract the last assistant text from a session's message history. */
|
|
9
|
-
export function extractLastAssistantText(
|
|
10
|
-
messages: ArrayLike<unknown> | undefined,
|
|
11
|
-
): string | undefined {
|
|
12
|
-
if (!messages) return undefined;
|
|
13
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
14
|
-
const message = messages[i] as { role?: string; content?: unknown } | undefined;
|
|
15
|
-
if (message?.role !== "assistant") continue;
|
|
16
|
-
const text = extractAssistantText(message.content);
|
|
17
|
-
if (text) return text;
|
|
18
|
-
}
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
8
|
/** Extract text content from a message content value (string | content-part[]). */
|
|
23
9
|
export function extractAssistantText(content: unknown): string | undefined {
|
|
24
10
|
if (typeof content === "string") {
|
|
@@ -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
|