@tonyclaw/agent-inspector 3.0.27 → 3.0.29
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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BVCAAbbu.js → CompareDrawer-Cb5ccPeX.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-CYOXB81n.js → ProxyViewerContainer-COBe5GTT.js} +198 -198
- package/.output/public/assets/{ReplayDialog-DMX23O5G.js → ReplayDialog-CctnITbt.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-S17pYDRc.js → RequestAnatomy-BrN0RBbL.js} +1 -1
- package/.output/public/assets/{ResponseView-Cf1XrawD.js → ResponseView-Cqq87lX_.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BdOrVlCi.js → StreamingChunkSequence-CnTa9mCr.js} +1 -1
- package/.output/public/assets/{_sessionId-BnKGmJrR.js → _sessionId-DKM91udC.js} +1 -1
- package/.output/public/assets/{index-gY3I-7-D.js → index-Bq-nqolG.js} +1 -1
- package/.output/public/assets/{index-9fMLCU4d.js → index-DPiESBo2.js} +1 -1
- package/.output/public/assets/{index-B1atk19i.js → index-DTLNVcgc.js} +7 -7
- package/.output/public/assets/index-DliqmwUw.css +1 -0
- package/.output/public/assets/{json-viewer-CpyYpPjx.js → json-viewer-CfaDJwx8.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +39 -39
- package/.output/server/{_sessionId-eA_h2_s6.mjs → _sessionId-CwGXPD4C.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-K2k9G7r1.mjs → CompareDrawer-R_P1-VNr.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-CbyBpCAj.mjs → ProxyViewerContainer-DUgxRNbj.mjs} +344 -191
- package/.output/server/_ssr/{ReplayDialog-DacI2Iv0.mjs → ReplayDialog-De9wKKzS.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-gh-pAdkO.mjs → RequestAnatomy-Db4MxUYo.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-BRJKN50-.mjs → ResponseView-8rDXOiNB.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-BQLVEuWY.mjs → StreamingChunkSequence-N9teZHkA.mjs} +2 -2
- package/.output/server/_ssr/{index-DuSWrjbH.mjs → index-D4DvByuW.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-DNs1ZjKI.mjs → json-viewer-BjoHRbjS.mjs} +3 -3
- package/.output/server/_ssr/{router-Dgx6-ot3.mjs → router-BYlePDj8.mjs} +138 -162
- package/.output/server/{_tanstack-start-manifest_v-CoGjnkQD.mjs → _tanstack-start-manifest_v-DMCA8KK3.mjs} +1 -1
- package/.output/server/index.mjs +68 -68
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +57 -1
- package/src/components/clients/ClientLogo.tsx +51 -9
- package/src/components/inspector-pet/InspectorPet.tsx +0 -7
- package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
- package/src/components/proxy-viewer/ConversationGroupList.tsx +251 -155
- package/src/components/proxy-viewer/ConversationHeader.tsx +43 -23
- package/src/lib/jsonFile.ts +46 -0
- package/src/lib/utils.ts +5 -1
- package/src/proxy/evidenceAnalysis.ts +116 -117
- package/src/proxy/groupEvidenceExporter.ts +4 -8
- package/src/proxy/groupStore.ts +3 -10
- package/src/proxy/providerSecretStore.ts +4 -8
- package/src/proxy/runFailures.ts +4 -10
- package/src/proxy/runStore.ts +4 -11
- package/.output/public/assets/index-CmeF5XXS.css +0 -1
|
@@ -22,7 +22,23 @@ import { ProviderLogoStack } from "./ProviderLogoStack";
|
|
|
22
22
|
import { resolveLogFormat } from "./log-formats";
|
|
23
23
|
|
|
24
24
|
const HEADER_ACTION_BUTTON_CLASS =
|
|
25
|
-
"
|
|
25
|
+
"inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md bg-[var(--inspector-surface-low)] text-muted-foreground transition-colors hover:bg-[var(--inspector-surface-medium)] hover:text-foreground";
|
|
26
|
+
|
|
27
|
+
export function isSyntheticSessionId(conversationId: string): boolean {
|
|
28
|
+
return (
|
|
29
|
+
conversationId.startsWith("PID:") ||
|
|
30
|
+
conversationId.startsWith("process:") ||
|
|
31
|
+
conversationId.startsWith("connection:") ||
|
|
32
|
+
conversationId.includes("|")
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function compactSessionLabel(conversationId: string): string {
|
|
37
|
+
if (isSyntheticSessionId(conversationId)) return "No session ID";
|
|
38
|
+
return conversationId.length > 24
|
|
39
|
+
? `${conversationId.slice(0, 12)}...${conversationId.slice(-12)}`
|
|
40
|
+
: conversationId;
|
|
41
|
+
}
|
|
26
42
|
|
|
27
43
|
function HeaderMetric({
|
|
28
44
|
icon,
|
|
@@ -38,7 +54,7 @@ function HeaderMetric({
|
|
|
38
54
|
return (
|
|
39
55
|
<span
|
|
40
56
|
className={cn(
|
|
41
|
-
"
|
|
57
|
+
"inline-flex h-7 min-w-0 items-center gap-1.5 rounded-md bg-[var(--inspector-surface-low)] px-2 text-xs text-muted-foreground",
|
|
42
58
|
className,
|
|
43
59
|
)}
|
|
44
60
|
title={title}
|
|
@@ -52,7 +68,7 @@ function HeaderMetric({
|
|
|
52
68
|
function PendingTurnBadge(): JSX.Element {
|
|
53
69
|
return (
|
|
54
70
|
<span
|
|
55
|
-
className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-
|
|
71
|
+
className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-[var(--inspector-surface-medium)] px-1.5 font-mono text-[10px] uppercase text-[var(--inspector-status-watch)] ring-1 ring-border/50"
|
|
56
72
|
title="This session has a turn that has not finished yet"
|
|
57
73
|
>
|
|
58
74
|
<Loader2 className="size-3 animate-spin" />
|
|
@@ -145,11 +161,8 @@ export function ConversationHeader({
|
|
|
145
161
|
const clientDisplayLabel = detectedClientLabel ?? userAgent ?? null;
|
|
146
162
|
const clientTooltip =
|
|
147
163
|
detectedClientLabel !== null && hasUserAgent ? `${detectedClientLabel} - ${userAgent}` : null;
|
|
148
|
-
const isFallbackConversation =
|
|
149
|
-
const
|
|
150
|
-
conversationId.length > 24
|
|
151
|
-
? conversationId.slice(0, 12) + "..." + conversationId.slice(-12)
|
|
152
|
-
: conversationId;
|
|
164
|
+
const isFallbackConversation = isSyntheticSessionId(conversationId);
|
|
165
|
+
const sessionLabel = compactSessionLabel(conversationId);
|
|
153
166
|
|
|
154
167
|
const timeLabel = formatTimestampRange(startTime, endTime, timeDisplayFormat);
|
|
155
168
|
const callsLabel = `${totalCalls} call${totalCalls !== 1 ? "s" : ""}`;
|
|
@@ -161,11 +174,11 @@ export function ConversationHeader({
|
|
|
161
174
|
data-nav-id={`conv-${conversationId}`}
|
|
162
175
|
data-nav-action={expanded ? "collapse" : "expand"}
|
|
163
176
|
className={cn(
|
|
164
|
-
"
|
|
165
|
-
"hover:bg-
|
|
177
|
+
"relative flex cursor-pointer items-start gap-2.5 overflow-hidden rounded-md bg-[var(--inspector-surface-low)] px-3 py-2.5 transition-colors",
|
|
178
|
+
"hover:bg-[var(--inspector-surface-medium)]",
|
|
166
179
|
"select-none",
|
|
167
180
|
"sticky top-0 z-10 mb-2",
|
|
168
|
-
"focus-visible:ring-1 focus-visible:ring-
|
|
181
|
+
"focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
|
|
169
182
|
)}
|
|
170
183
|
onClick={onToggle}
|
|
171
184
|
onKeyDown={(e) => {
|
|
@@ -185,17 +198,20 @@ export function ConversationHeader({
|
|
|
185
198
|
|
|
186
199
|
<span className="min-w-0 flex-1">
|
|
187
200
|
<span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
201
|
+
<span className="shrink-0 rounded bg-[var(--inspector-surface-medium)] px-1.5 py-0.5 font-mono text-[9px] uppercase text-muted-foreground">
|
|
202
|
+
Session
|
|
203
|
+
</span>
|
|
188
204
|
{isFallbackConversation && showProcessMetadata ? (
|
|
189
205
|
clientPid !== null && clientPid !== undefined ? (
|
|
190
206
|
<span
|
|
191
|
-
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-
|
|
207
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-foreground"
|
|
192
208
|
title={`PID ${clientPid}`}
|
|
193
209
|
>
|
|
194
210
|
PID {clientPid}
|
|
195
211
|
</span>
|
|
196
212
|
) : (
|
|
197
213
|
<span
|
|
198
|
-
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-
|
|
214
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-foreground"
|
|
199
215
|
title={conversationId}
|
|
200
216
|
>
|
|
201
217
|
{conversationId}
|
|
@@ -203,10 +219,10 @@ export function ConversationHeader({
|
|
|
203
219
|
)
|
|
204
220
|
) : (
|
|
205
221
|
<span
|
|
206
|
-
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-
|
|
222
|
+
className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-foreground"
|
|
207
223
|
title={conversationId}
|
|
208
224
|
>
|
|
209
|
-
{
|
|
225
|
+
{sessionLabel}
|
|
210
226
|
</span>
|
|
211
227
|
)}
|
|
212
228
|
|
|
@@ -215,7 +231,7 @@ export function ConversationHeader({
|
|
|
215
231
|
clientPid !== null &&
|
|
216
232
|
clientPid !== undefined && (
|
|
217
233
|
<span
|
|
218
|
-
className="
|
|
234
|
+
className="shrink-0 rounded bg-[var(--inspector-surface-medium)] px-1.5 py-0.5 font-mono text-xs tabular-nums text-muted-foreground"
|
|
219
235
|
title={`Client process ID ${clientPid}`}
|
|
220
236
|
>
|
|
221
237
|
PID {clientPid}
|
|
@@ -237,10 +253,10 @@ export function ConversationHeader({
|
|
|
237
253
|
|
|
238
254
|
{showClientIdentity && clientDisplayLabel !== null && clientDisplayLabel !== "" && (
|
|
239
255
|
<span
|
|
240
|
-
className="
|
|
256
|
+
className="inline-flex min-h-6 shrink-0 flex-wrap items-center gap-1 rounded-md bg-[var(--inspector-surface-medium)] px-1.5 py-0.5 text-xs text-muted-foreground"
|
|
241
257
|
title={clientTooltip ?? clientDisplayLabel}
|
|
242
258
|
>
|
|
243
|
-
<ClientLogo client={clientApp} className="size-4 shrink-0" />
|
|
259
|
+
<ClientLogo client={clientApp} className="size-4 shrink-0" decorative={true} />
|
|
244
260
|
<span className="font-mono leading-snug tabular-nums">{clientDisplayLabel}</span>
|
|
245
261
|
</span>
|
|
246
262
|
)}
|
|
@@ -256,9 +272,13 @@ export function ConversationHeader({
|
|
|
256
272
|
{callsLabel}
|
|
257
273
|
</HeaderMetric>
|
|
258
274
|
<HeaderMetric icon={<Zap className="size-3 shrink-0" />} className="max-w-full">
|
|
259
|
-
<span className="text-
|
|
275
|
+
<span className="text-[var(--inspector-status-info)]">
|
|
276
|
+
{formatTokens(totalInputTokens)}
|
|
277
|
+
</span>
|
|
260
278
|
{" / "}
|
|
261
|
-
<span className="text-
|
|
279
|
+
<span className="text-[var(--inspector-status-watch)]">
|
|
280
|
+
{formatTokens(totalOutputTokens)}
|
|
281
|
+
</span>
|
|
262
282
|
</HeaderMetric>
|
|
263
283
|
</span>
|
|
264
284
|
</span>
|
|
@@ -295,7 +315,7 @@ export function ConversationHeader({
|
|
|
295
315
|
|
|
296
316
|
{/* Expand chevron - shows spinner when collapsed and group has pending logs */}
|
|
297
317
|
<span
|
|
298
|
-
className="
|
|
318
|
+
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md bg-[var(--inspector-surface-low)] text-muted-foreground transition-colors hover:bg-[var(--inspector-surface-medium)] hover:text-foreground"
|
|
299
319
|
aria-hidden="true"
|
|
300
320
|
>
|
|
301
321
|
{expanded ? (
|
|
@@ -310,10 +330,10 @@ export function ConversationHeader({
|
|
|
310
330
|
|
|
311
331
|
{isLoading && (
|
|
312
332
|
<span
|
|
313
|
-
className="pointer-events-none absolute inset-x-3 bottom-0 h-px overflow-hidden rounded-full bg-
|
|
333
|
+
className="pointer-events-none absolute inset-x-3 bottom-0 h-px overflow-hidden rounded-full bg-[var(--inspector-surface-medium)]"
|
|
314
334
|
aria-hidden="true"
|
|
315
335
|
>
|
|
316
|
-
<span className="absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full bg-
|
|
336
|
+
<span className="absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full bg-[var(--inspector-status-watch)] opacity-70" />
|
|
317
337
|
</span>
|
|
318
338
|
)}
|
|
319
339
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readProperty } from "./unknown";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads a UTF-8 JSON file from disk and validates it against a Zod schema in
|
|
7
|
+
* one call. Used by config/secret/evidence files that store small records
|
|
8
|
+
* atomically rewritten on update — *not* for streaming log files (those go
|
|
9
|
+
* through line-by-line parsing in store.ts).
|
|
10
|
+
*
|
|
11
|
+
* On any failure returns `{ ok: false, reason }` rather than throwing. The
|
|
12
|
+
* caller decides between returning a fallback, logging, or surfacing an
|
|
13
|
+
* error response. To log on failure, wrap the call site with the project's
|
|
14
|
+
* `logger.warn` (the helper stays a pure parser to avoid a `lib → proxy`
|
|
15
|
+
* dependency direction).
|
|
16
|
+
*/
|
|
17
|
+
export type ReadJsonFileFailureReason = "missing" | "io-error" | "parse-error" | "schema-mismatch";
|
|
18
|
+
|
|
19
|
+
export type ReadJsonFileResult<T> =
|
|
20
|
+
| { ok: true; data: T }
|
|
21
|
+
| { ok: false; reason: ReadJsonFileFailureReason };
|
|
22
|
+
|
|
23
|
+
export function readJsonFile<T>(path: string, schema: z.ZodType<T>): ReadJsonFileResult<T> {
|
|
24
|
+
let raw: string;
|
|
25
|
+
try {
|
|
26
|
+
raw = readFileSync(path, "utf8");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (isEnoent(error)) return { ok: false, reason: "missing" };
|
|
29
|
+
return { ok: false, reason: "io-error" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let parsed: unknown;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return { ok: false, reason: "parse-error" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = schema.safeParse(parsed);
|
|
40
|
+
if (!result.success) return { ok: false, reason: "schema-mismatch" };
|
|
41
|
+
return { ok: true, data: result.data };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isEnoent(error: unknown): boolean {
|
|
45
|
+
return readProperty(error, "code") === "ENOENT";
|
|
46
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -6,7 +6,11 @@ export function cn(...inputs: ClassValue[]): string {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function formatTokens(count: number): string {
|
|
9
|
-
|
|
9
|
+
const absoluteCount = Math.abs(count);
|
|
10
|
+
if (absoluteCount < 1000) return String(count);
|
|
11
|
+
if (absoluteCount >= 1_000_000) {
|
|
12
|
+
return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
13
|
+
}
|
|
10
14
|
return (count / 1000).toFixed(1).replace(/\.0$/, "") + "K";
|
|
11
15
|
}
|
|
12
16
|
|
|
@@ -96,86 +96,113 @@ function hasStreamingMismatch(session: SessionInfo): boolean {
|
|
|
96
96
|
return false;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return "Provider rejected the request because of rate limit or quota pressure.";
|
|
113
|
-
case "network-or-container":
|
|
114
|
-
return "Network connectivity looks suspicious; check container boundaries and localhost use.";
|
|
115
|
-
case "streaming-mismatch":
|
|
116
|
-
return "Streaming and non-streaming behavior differ for the same provider/model.";
|
|
117
|
-
case "context-pressure":
|
|
118
|
-
return "The failure looks related to context window or token pressure.";
|
|
119
|
-
case "runtime-task-error":
|
|
120
|
-
return "Inspector session runtime reported a task error.";
|
|
121
|
-
case "model-output-error":
|
|
122
|
-
return "The model output or upstream payload shape appears invalid.";
|
|
123
|
-
case "unknown-failure":
|
|
124
|
-
return "Inspector captured errors, but the root cause is not yet classified.";
|
|
125
|
-
case "no-session-data":
|
|
126
|
-
return "No Inspector session data was available for this run.";
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function categoryHints(category: FailureCategory): string[] {
|
|
131
|
-
switch (category) {
|
|
132
|
-
case "none":
|
|
133
|
-
return ["Keep the evidence pack as the success proof for CI or release notes."];
|
|
134
|
-
case "active":
|
|
135
|
-
return ["Wait for the session to settle, then export evidence again."];
|
|
136
|
-
case "provider-timeout":
|
|
137
|
-
return [
|
|
138
|
-
"Increase the provider test timeout for slow models.",
|
|
139
|
-
"Compare streaming and non-streaming probes for the same model.",
|
|
140
|
-
];
|
|
141
|
-
case "provider-http-error":
|
|
142
|
-
return [
|
|
143
|
-
"Open the cited log ids and inspect upstream status, response body, and provider URL.",
|
|
144
|
-
];
|
|
145
|
-
case "provider-auth-error":
|
|
146
|
-
return ["Verify the provider API key, auth header mode, and configured base URL."];
|
|
147
|
-
case "provider-rate-limit":
|
|
148
|
-
return [
|
|
149
|
-
"Retry after provider quota recovers or switch to a less constrained provider/model.",
|
|
150
|
-
];
|
|
151
|
-
case "network-or-container":
|
|
152
|
-
return [
|
|
153
|
-
"If the coding agent and Inspector are in different containers, replace localhost with a reachable host.",
|
|
154
|
-
"Check DNS, proxy, firewall, and container network routes.",
|
|
155
|
-
];
|
|
156
|
-
case "streaming-mismatch":
|
|
157
|
-
return [
|
|
158
|
-
"Compare the streaming and non-streaming captured logs side by side.",
|
|
159
|
-
"Check whether the provider's OpenAI-compatible endpoint handles stream=false differently.",
|
|
160
|
-
];
|
|
161
|
-
case "context-pressure":
|
|
162
|
-
return [
|
|
163
|
-
"Reduce prompt/tool schema/history size or configure the model context window metadata.",
|
|
164
|
-
];
|
|
165
|
-
case "runtime-task-error":
|
|
166
|
-
return ["Inspect the session runtime error and retry after clearing stuck session tasks."];
|
|
167
|
-
case "model-output-error":
|
|
168
|
-
return ["Inspect the raw response and schema parsing details for the cited log ids."];
|
|
169
|
-
case "unknown-failure":
|
|
170
|
-
return [
|
|
171
|
-
"Open the cited logs and inspect error/status/latency fields to refine the diagnosis.",
|
|
172
|
-
];
|
|
173
|
-
case "no-session-data":
|
|
174
|
-
return [
|
|
175
|
-
"Confirm the run's sessionId matches captured traffic and that history scanning is enabled when needed.",
|
|
176
|
-
];
|
|
99
|
+
/**
|
|
100
|
+
* Per-category summary, hints, severity, and outcome. Keyed by every
|
|
101
|
+
* `FailureCategory` so TypeScript's `Record` enforces exhaustiveness —
|
|
102
|
+
* adding a new category is a single-place compile error instead of four
|
|
103
|
+
* parallel switch statements missing a case.
|
|
104
|
+
*/
|
|
105
|
+
const FAILURE_CATEGORY_INFO: Record<
|
|
106
|
+
FailureCategory,
|
|
107
|
+
{
|
|
108
|
+
summary: string;
|
|
109
|
+
hints: string[];
|
|
110
|
+
severity: FailureClassification["severity"];
|
|
111
|
+
outcome: FailureClassification["outcome"];
|
|
177
112
|
}
|
|
178
|
-
|
|
113
|
+
> = {
|
|
114
|
+
none: {
|
|
115
|
+
summary: "Session completed without captured errors.",
|
|
116
|
+
hints: ["Keep the evidence pack as the success proof for CI or release notes."],
|
|
117
|
+
severity: "none",
|
|
118
|
+
outcome: "success",
|
|
119
|
+
},
|
|
120
|
+
active: {
|
|
121
|
+
summary: "Session still has active requests or running tasks.",
|
|
122
|
+
hints: ["Wait for the session to settle, then export evidence again."],
|
|
123
|
+
severity: "low",
|
|
124
|
+
outcome: "active",
|
|
125
|
+
},
|
|
126
|
+
"provider-timeout": {
|
|
127
|
+
summary: "Provider call appears to have timed out.",
|
|
128
|
+
hints: [
|
|
129
|
+
"Increase the provider test timeout for slow models.",
|
|
130
|
+
"Compare streaming and non-streaming probes for the same model.",
|
|
131
|
+
],
|
|
132
|
+
severity: "high",
|
|
133
|
+
outcome: "failure",
|
|
134
|
+
},
|
|
135
|
+
"provider-http-error": {
|
|
136
|
+
summary: "Provider returned an HTTP error.",
|
|
137
|
+
hints: ["Open the cited log ids and inspect upstream status, response body, and provider URL."],
|
|
138
|
+
severity: "high",
|
|
139
|
+
outcome: "failure",
|
|
140
|
+
},
|
|
141
|
+
"provider-auth-error": {
|
|
142
|
+
summary: "Provider authentication or API key configuration appears invalid.",
|
|
143
|
+
hints: ["Verify the provider API key, auth header mode, and configured base URL."],
|
|
144
|
+
severity: "high",
|
|
145
|
+
outcome: "failure",
|
|
146
|
+
},
|
|
147
|
+
"provider-rate-limit": {
|
|
148
|
+
summary: "Provider rejected the request because of rate limit or quota pressure.",
|
|
149
|
+
hints: ["Retry after provider quota recovers or switch to a less constrained provider/model."],
|
|
150
|
+
severity: "high",
|
|
151
|
+
outcome: "failure",
|
|
152
|
+
},
|
|
153
|
+
"network-or-container": {
|
|
154
|
+
summary: "Network connectivity looks suspicious; check container boundaries and localhost use.",
|
|
155
|
+
hints: [
|
|
156
|
+
"If the coding agent and Inspector are in different containers, replace localhost with a reachable host.",
|
|
157
|
+
"Check DNS, proxy, firewall, and container network routes.",
|
|
158
|
+
],
|
|
159
|
+
severity: "high",
|
|
160
|
+
outcome: "failure",
|
|
161
|
+
},
|
|
162
|
+
"streaming-mismatch": {
|
|
163
|
+
summary: "Streaming and non-streaming behavior differ for the same provider/model.",
|
|
164
|
+
hints: [
|
|
165
|
+
"Compare the streaming and non-streaming captured logs side by side.",
|
|
166
|
+
"Check whether the provider's OpenAI-compatible endpoint handles stream=false differently.",
|
|
167
|
+
],
|
|
168
|
+
severity: "high",
|
|
169
|
+
outcome: "failure",
|
|
170
|
+
},
|
|
171
|
+
"context-pressure": {
|
|
172
|
+
summary: "The failure looks related to context window or token pressure.",
|
|
173
|
+
hints: [
|
|
174
|
+
"Reduce prompt/tool schema/history size or configure the model context window metadata.",
|
|
175
|
+
],
|
|
176
|
+
severity: "medium",
|
|
177
|
+
outcome: "failure",
|
|
178
|
+
},
|
|
179
|
+
"runtime-task-error": {
|
|
180
|
+
summary: "Inspector session runtime reported a task error.",
|
|
181
|
+
hints: ["Inspect the session runtime error and retry after clearing stuck session tasks."],
|
|
182
|
+
severity: "high",
|
|
183
|
+
outcome: "failure",
|
|
184
|
+
},
|
|
185
|
+
"model-output-error": {
|
|
186
|
+
summary: "The model output or upstream payload shape appears invalid.",
|
|
187
|
+
hints: ["Inspect the raw response and schema parsing details for the cited log ids."],
|
|
188
|
+
severity: "medium",
|
|
189
|
+
outcome: "failure",
|
|
190
|
+
},
|
|
191
|
+
"unknown-failure": {
|
|
192
|
+
summary: "Inspector captured errors, but the root cause is not yet classified.",
|
|
193
|
+
hints: ["Open the cited logs and inspect error/status/latency fields to refine the diagnosis."],
|
|
194
|
+
severity: "medium",
|
|
195
|
+
outcome: "failure",
|
|
196
|
+
},
|
|
197
|
+
"no-session-data": {
|
|
198
|
+
summary: "No Inspector session data was available for this run.",
|
|
199
|
+
hints: [
|
|
200
|
+
"Confirm the run's sessionId matches captured traffic and that history scanning is enabled when needed.",
|
|
201
|
+
],
|
|
202
|
+
severity: "medium",
|
|
203
|
+
outcome: "unknown",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
179
206
|
|
|
180
207
|
function classifyCategory(
|
|
181
208
|
session: SessionInfo,
|
|
@@ -221,47 +248,19 @@ function classifyCategory(
|
|
|
221
248
|
}
|
|
222
249
|
|
|
223
250
|
function severityFor(category: FailureCategory): FailureClassification["severity"] {
|
|
224
|
-
|
|
225
|
-
case "none":
|
|
226
|
-
return "none";
|
|
227
|
-
case "active":
|
|
228
|
-
return "low";
|
|
229
|
-
case "provider-timeout":
|
|
230
|
-
case "provider-http-error":
|
|
231
|
-
case "provider-auth-error":
|
|
232
|
-
case "provider-rate-limit":
|
|
233
|
-
case "network-or-container":
|
|
234
|
-
case "streaming-mismatch":
|
|
235
|
-
case "runtime-task-error":
|
|
236
|
-
return "high";
|
|
237
|
-
case "context-pressure":
|
|
238
|
-
case "model-output-error":
|
|
239
|
-
case "unknown-failure":
|
|
240
|
-
case "no-session-data":
|
|
241
|
-
return "medium";
|
|
242
|
-
}
|
|
251
|
+
return FAILURE_CATEGORY_INFO[category].severity;
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
function outcomeFor(category: FailureCategory): FailureClassification["outcome"] {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
case "provider-auth-error":
|
|
256
|
-
case "provider-rate-limit":
|
|
257
|
-
case "network-or-container":
|
|
258
|
-
case "streaming-mismatch":
|
|
259
|
-
case "context-pressure":
|
|
260
|
-
case "runtime-task-error":
|
|
261
|
-
case "model-output-error":
|
|
262
|
-
case "unknown-failure":
|
|
263
|
-
return "failure";
|
|
264
|
-
}
|
|
255
|
+
return FAILURE_CATEGORY_INFO[category].outcome;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function categorySummary(category: FailureCategory): string {
|
|
259
|
+
return FAILURE_CATEGORY_INFO[category].summary;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function categoryHints(category: FailureCategory): string[] {
|
|
263
|
+
return FAILURE_CATEGORY_INFO[category].hints;
|
|
265
264
|
}
|
|
266
265
|
|
|
267
266
|
export function classifyRunFailure(
|
|
@@ -15,6 +15,7 @@ import { getDataDir } from "./dataDir";
|
|
|
15
15
|
import { getRun } from "./runStore";
|
|
16
16
|
import { getSessionInfo } from "./store";
|
|
17
17
|
import { updateGroupEvidence } from "./groupStore";
|
|
18
|
+
import { readJsonFile } from "../lib/jsonFile";
|
|
18
19
|
|
|
19
20
|
type GroupEvidenceDocument = GroupEvidenceExportResult;
|
|
20
21
|
|
|
@@ -343,12 +344,7 @@ export function readGroupEvidenceMarkdown(group: InspectorGroup): string | null
|
|
|
343
344
|
|
|
344
345
|
export function readGroupEvidenceDocument(group: InspectorGroup): GroupEvidenceExportResult | null {
|
|
345
346
|
const path = group.evidence?.jsonPath;
|
|
346
|
-
if (path === undefined
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const parsed = GroupEvidenceExportResultSchema.safeParse(raw);
|
|
350
|
-
return parsed.success ? parsed.data : null;
|
|
351
|
-
} catch {
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
347
|
+
if (path === undefined) return null;
|
|
348
|
+
const result = readJsonFile(path, GroupEvidenceExportResultSchema);
|
|
349
|
+
return result.ok ? result.data : null;
|
|
354
350
|
}
|
package/src/proxy/groupStore.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type UpdateInspectorGroupInput,
|
|
20
20
|
} from "../lib/groupContract";
|
|
21
21
|
import { getDataDir } from "./dataDir";
|
|
22
|
+
import { readJsonFile } from "../lib/jsonFile";
|
|
22
23
|
|
|
23
24
|
const GroupsFileSchema = z.object({
|
|
24
25
|
groups: z.array(InspectorGroupSchema),
|
|
@@ -70,16 +71,8 @@ function nullableText(value: string | null | undefined): string | null {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function readGroups(): InspectorGroup[] {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const parsed: unknown = JSON.parse(readFileSync(filePath, "utf8"));
|
|
78
|
-
const result = GroupsFileSchema.safeParse(parsed);
|
|
79
|
-
return result.success ? result.data.groups : [];
|
|
80
|
-
} catch {
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
74
|
+
const result = readJsonFile(groupsFilePath(), GroupsFileSchema);
|
|
75
|
+
return result.ok ? result.data.groups : [];
|
|
83
76
|
}
|
|
84
77
|
|
|
85
78
|
function writeGroups(groups: readonly InspectorGroup[]): void {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
import { readJsonFile } from "../lib/jsonFile";
|
|
14
15
|
import { logger } from "./logger";
|
|
15
16
|
|
|
16
17
|
const SECRET_REFERENCE_PREFIX = "secret:provider:";
|
|
@@ -39,14 +40,9 @@ function emptySecretFile(): SecretFile {
|
|
|
39
40
|
|
|
40
41
|
function readSecretFile(path: string): SecretFile {
|
|
41
42
|
if (!existsSync(path)) return emptySecretFile();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (result.success) return result.data;
|
|
46
|
-
logger.warn(`[provider-secrets] Ignoring invalid secret file ${path}`);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
logger.warn(`[provider-secrets] Could not read ${path}: ${String(error)}`);
|
|
49
|
-
}
|
|
43
|
+
const result = readJsonFile(path, SecretFileSchema);
|
|
44
|
+
if (result.ok) return result.data;
|
|
45
|
+
logger.warn(`[provider-secrets] Ignoring invalid secret file ${path} (${result.reason})`);
|
|
50
46
|
return emptySecretFile();
|
|
51
47
|
}
|
|
52
48
|
|
package/src/proxy/runFailures.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
1
|
import {
|
|
3
2
|
EvidenceExportResultSchema,
|
|
4
3
|
type EvidenceExportResult,
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
type RecentFailure,
|
|
10
9
|
type RecentFailuresResponse,
|
|
11
10
|
} from "../lib/runContract";
|
|
11
|
+
import { readJsonFile } from "../lib/jsonFile";
|
|
12
12
|
import { listRuns } from "./runStore";
|
|
13
13
|
|
|
14
14
|
export const RECENT_FAILURES_DEFAULT_LIMIT = 5;
|
|
@@ -21,15 +21,9 @@ export function clampRecentFailuresLimit(value: number): number {
|
|
|
21
21
|
|
|
22
22
|
function readEvidenceDocument(run: InspectorRun): EvidenceExportResult | null {
|
|
23
23
|
const path = run.evidence?.jsonPath;
|
|
24
|
-
if (path === undefined
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
28
|
-
const result = EvidenceExportResultSchema.safeParse(parsed);
|
|
29
|
-
return result.success ? result.data : null;
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
24
|
+
if (path === undefined) return null;
|
|
25
|
+
const result = readJsonFile(path, EvidenceExportResultSchema);
|
|
26
|
+
return result.ok ? result.data : null;
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
function syntheticFailedClassification(run: InspectorRun): FailureClassification {
|
package/src/proxy/runStore.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../lib/runContract";
|
|
14
14
|
import { getDataDir } from "./dataDir";
|
|
15
15
|
import { addGroupSession, getGroup } from "./groupStore";
|
|
16
|
+
import { readJsonFile } from "../lib/jsonFile";
|
|
16
17
|
|
|
17
18
|
const RunsFileSchema = z.object({
|
|
18
19
|
runs: z.array(InspectorRunSchema),
|
|
@@ -95,16 +96,8 @@ function syncRunGroupMember(run: InspectorRun, createIfMissing: boolean): void {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function readRuns(): InspectorRun[] {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
const parsed: unknown = JSON.parse(readFileSync(filePath, "utf8"));
|
|
103
|
-
const result = RunsFileSchema.safeParse(parsed);
|
|
104
|
-
return result.success ? result.data.runs : [];
|
|
105
|
-
} catch {
|
|
106
|
-
return [];
|
|
107
|
-
}
|
|
99
|
+
const result = readJsonFile(runsFilePath(), RunsFileSchema);
|
|
100
|
+
return result.ok ? result.data.runs : [];
|
|
108
101
|
}
|
|
109
102
|
|
|
110
103
|
function writeRuns(runs: readonly InspectorRun[]): void {
|