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