@tonyclaw/agent-inspector 3.0.26 → 3.0.28

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.
Files changed (86) hide show
  1. package/.output/cli.js +80 -11
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-SZRUDQc9.js → CompareDrawer-TghZr5t1.js} +1 -1
  4. package/.output/public/assets/{ProxyViewerContainer-BJvwOrSi.js → ProxyViewerContainer-fShBAZjj.js} +201 -201
  5. package/.output/public/assets/{ReplayDialog-DgfmqiM3.js → ReplayDialog-CZMApaF4.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-CS6c7Qfn.js → RequestAnatomy-DNWccR7O.js} +1 -1
  7. package/.output/public/assets/{ResponseView-C0Ri_5gm.js → ResponseView-d25MdFhY.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-7dYcHWyZ.js → StreamingChunkSequence-DIFZEnWe.js} +1 -1
  9. package/.output/public/assets/{_sessionId-D-V84cgw.js → _sessionId-DRfvuE2k.js} +1 -1
  10. package/.output/public/assets/{index-DgFWCxOs.js → index-B3aAuqWq.js} +1 -1
  11. package/.output/public/assets/index-BsxAGqMA.css +1 -0
  12. package/.output/public/assets/{index-DrVJsks2.js → index-D4HkS7Ct.js} +1 -1
  13. package/.output/public/assets/{index-CSIL7-MK.js → index-DzARL2Fi.js} +3 -3
  14. package/.output/public/assets/{json-viewer-C9Ji3gYN.js → json-viewer-wuJSKaed.js} +1 -1
  15. package/.output/server/_libs/lucide-react.mjs +39 -39
  16. package/.output/server/{_sessionId-BQ6YQOO1.mjs → _sessionId-BJTNiP_O.mjs} +4 -2
  17. package/.output/server/_ssr/{CompareDrawer-CqNzIeMR.mjs → CompareDrawer-BMRH3N97.mjs} +5 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-CMC7Vsbw.mjs → ProxyViewerContainer-AFwcHAIR.mjs} +330 -223
  19. package/.output/server/_ssr/{ReplayDialog-D9p_LaTr.mjs → ReplayDialog-CbWvRHiN.mjs} +6 -4
  20. package/.output/server/_ssr/{RequestAnatomy-B4ge3qNZ.mjs → RequestAnatomy-C4kcPyqN.mjs} +4 -2
  21. package/.output/server/_ssr/{ResponseView-BCqMKApI.mjs → ResponseView-D8VYt-gZ.mjs} +5 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-BA4zTLE4.mjs → StreamingChunkSequence-BUUo6vU0.mjs} +4 -2
  23. package/.output/server/_ssr/{index-CzxhDjlq.mjs → index-DTwMEEXO.mjs} +4 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-B3qV6iEJ.mjs → json-viewer-BqF1pSQD.mjs} +5 -3
  26. package/.output/server/_ssr/{router-CZe_R31M.mjs → router-C8BOns7y.mjs} +839 -368
  27. package/.output/server/{_tanstack-start-manifest_v-B50VTLrH.mjs → _tanstack-start-manifest_v-BBf2nRhb.mjs} +1 -1
  28. package/.output/server/index.mjs +70 -70
  29. package/README.md +8 -0
  30. package/package.json +4 -5
  31. package/src/cli.ts +43 -9
  32. package/src/components/ProxyViewer.tsx +57 -1
  33. package/src/components/clients/ClientLogo.tsx +43 -4
  34. package/src/components/inspector-pet/InspectorPet.tsx +0 -7
  35. package/src/components/providers/ProviderCard.tsx +3 -36
  36. package/src/components/providers/ProviderForm.tsx +13 -12
  37. package/src/components/providers/ProvidersPanel.tsx +2 -2
  38. package/src/components/providers/SettingsDialog.tsx +3 -3
  39. package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
  40. package/src/components/proxy-viewer/ConversationGroupList.tsx +233 -152
  41. package/src/components/proxy-viewer/ConversationHeader.tsx +29 -13
  42. package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -3
  43. package/src/contracts/anthropic.ts +0 -5
  44. package/src/contracts/index.ts +0 -4
  45. package/src/contracts/log.ts +0 -1
  46. package/src/contracts/openai.ts +0 -3
  47. package/src/knowledge/openclawClient.ts +22 -9
  48. package/src/knowledge/openclawGatewayClient.ts +20 -13
  49. package/src/lib/alertContract.ts +0 -3
  50. package/src/lib/ecosystemContract.ts +0 -3
  51. package/src/lib/export-logs.ts +1 -4
  52. package/src/lib/jsonFile.ts +46 -0
  53. package/src/lib/safeFetch.ts +100 -0
  54. package/src/lib/ssrfGuard.ts +133 -0
  55. package/src/lib/stopReason.ts +1 -4
  56. package/src/lib/unknown.ts +27 -0
  57. package/src/lib/useProviders.ts +1 -1
  58. package/src/mcp/mode.ts +3 -9
  59. package/src/mcp/server.ts +5 -14
  60. package/src/mcp/toolHandlers.ts +45 -54
  61. package/src/proxy/constants.ts +17 -0
  62. package/src/proxy/evidenceAnalysis.ts +116 -117
  63. package/src/proxy/formats/anthropic/handler.ts +5 -1
  64. package/src/proxy/formats/anthropic/schemas.ts +1 -7
  65. package/src/proxy/formats/index.ts +1 -1
  66. package/src/proxy/formats/openai/schemas.ts +0 -3
  67. package/src/proxy/formats/registry.ts +0 -3
  68. package/src/proxy/groupEvidenceExporter.ts +4 -8
  69. package/src/proxy/groupStore.ts +3 -10
  70. package/src/proxy/handler.ts +91 -12
  71. package/src/proxy/identityProxy.ts +74 -1
  72. package/src/proxy/logImporter.ts +82 -12
  73. package/src/proxy/logIndex.ts +1 -4
  74. package/src/proxy/providerSecretStore.ts +177 -0
  75. package/src/proxy/providers.ts +84 -11
  76. package/src/proxy/runFailures.ts +4 -10
  77. package/src/proxy/runStore.ts +4 -11
  78. package/src/proxy/schemas.ts +1 -8
  79. package/src/proxy/sqliteLogIndex.ts +0 -16
  80. package/src/proxy/store.ts +1 -5
  81. package/src/proxy/toolSchemaWarnings.ts +1 -9
  82. package/src/routes/api/logs.$id.replay.ts +17 -5
  83. package/src/routes/api/providers.$providerId.model-metadata.ts +11 -1
  84. package/src/routes/api/providers.$providerId.ts +18 -5
  85. package/src/routes/api/providers.ts +18 -7
  86. package/.output/public/assets/index-CmeF5XXS.css +0 -1
package/src/mcp/server.ts CHANGED
@@ -1262,15 +1262,10 @@ function registerTools(server: McpServer): void {
1262
1262
  {
1263
1263
  title: "List configured LLM providers",
1264
1264
  description:
1265
- "Returns the configured ProviderConfig array with apiKey redacted by default. Set includeSecrets=true only when the user explicitly needs plaintext local provider keys; this exposes secrets from <dataDir>/providers.json.",
1266
- inputSchema: z.object({
1267
- includeSecrets: z
1268
- .boolean()
1269
- .optional()
1270
- .describe("Return plaintext provider API keys. Exposes local secrets when true."),
1271
- }),
1265
+ "Returns the configured ProviderConfig array with write-only API keys represented as configured.",
1266
+ inputSchema: z.object({}),
1272
1267
  },
1273
- (args) => safeCall(() => listProvidersImpl(callApi, args)),
1268
+ () => safeCall(() => listProvidersImpl(callApi)),
1274
1269
  );
1275
1270
 
1276
1271
  // ----- Action tools -----
@@ -1280,16 +1275,12 @@ function registerTools(server: McpServer): void {
1280
1275
  {
1281
1276
  title: "Get a single provider by id",
1282
1277
  description:
1283
- "Returns one ProviderConfig with apiKey redacted by default. Set includeSecrets=true only when the user explicitly needs the plaintext local provider key.",
1278
+ "Returns one ProviderConfig. API keys are write-only and represented as configured.",
1284
1279
  inputSchema: z.object({
1285
1280
  id: z.string().describe("The provider id."),
1286
- includeSecrets: z
1287
- .boolean()
1288
- .optional()
1289
- .describe("Return the plaintext provider API key. Exposes a local secret when true."),
1290
1281
  }),
1291
1282
  },
1292
- ({ id, includeSecrets }) => safeCall(() => getProviderImpl(callApi, id, { includeSecrets })),
1283
+ ({ id }) => safeCall(() => getProviderImpl(callApi, id)),
1293
1284
  );
1294
1285
 
1295
1286
  server.registerTool(
@@ -36,6 +36,7 @@ import {
36
36
  } from "../lib/groupContract";
37
37
  import { type CreateInspectorRunInput, type UpdateInspectorRunInput } from "../lib/runContract";
38
38
  import { SessionInfoSchema } from "../lib/sessionInfoContract";
39
+ import { isRecord, textByteLength } from "../lib/unknown";
39
40
  import { CapturedLogSchema, type CapturedLog } from "../contracts";
40
41
  import {
41
42
  addInspectorGroupSession,
@@ -112,11 +113,6 @@ function inspectorBaseUrl(): string {
112
113
  return `http://127.0.0.1:${String(getCurrentPort())}`;
113
114
  }
114
115
 
115
- function textByteLength(value: string | null): number | null {
116
- if (value === null) return null;
117
- return Buffer.byteLength(value, "utf8");
118
- }
119
-
120
116
  function hasBodyText(value: string | null, byteLength: number | null | undefined): boolean {
121
117
  if (value !== null && value.length > 0) return true;
122
118
  return byteLength !== undefined && byteLength !== null && byteLength > 0;
@@ -272,15 +268,44 @@ async function readCompactLogsPage(
272
268
  params: URLSearchParams,
273
269
  ): Promise<{ logs: CapturedLog[]; total: number } | ToolResult> {
274
270
  const path = `/api/logs?${params.toString()}`;
275
- const res = await callApi(path);
276
- if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
277
- const rawBody: unknown = await res.json();
278
- const parsed = LogsListResponseSchema.safeParse(rawBody);
279
- if (!parsed.success) return toolError("GET /api/logs returned an unparseable logs shape");
271
+ const parsed = await fetchApiJson(callApi, path, LogsListResponseSchema);
272
+ if (!parsed.ok) return parsed.error;
280
273
  const logs = parsed.data.logs ?? [];
281
274
  return { logs, total: parsed.data.total ?? logs.length };
282
275
  }
283
276
 
277
+ /**
278
+ * Fetches a JSON endpoint and validates it against a Zod schema in one call.
279
+ * On HTTP error or schema mismatch returns a `ToolResult` error so the caller
280
+ * can propagate it directly. On success returns the typed parsed value.
281
+ */
282
+ type FetchApiResult<T> = { ok: true; data: T } | { ok: false; error: ToolResult };
283
+
284
+ async function fetchApiJson<T>(
285
+ callApi: CallApiFn,
286
+ path: string,
287
+ schema: z.ZodType<T>,
288
+ init?: RequestInit,
289
+ ): Promise<FetchApiResult<T>> {
290
+ const res = await callApi(path, init);
291
+ if (!res.ok) {
292
+ return { ok: false, error: toolError(`${methodFor(init)} ${path} returned ${res.status}`) };
293
+ }
294
+ const rawBody: unknown = await res.json();
295
+ const parsed = schema.safeParse(rawBody);
296
+ if (!parsed.success) {
297
+ return {
298
+ ok: false,
299
+ error: toolError(`${methodFor(init)} ${path} returned an unparseable shape`),
300
+ };
301
+ }
302
+ return { ok: true, data: parsed.data };
303
+ }
304
+
305
+ function methodFor(init: RequestInit | undefined): string {
306
+ return (init?.method ?? "GET").toUpperCase();
307
+ }
308
+
284
309
  function isToolResult(value: unknown): value is ToolResult {
285
310
  return (
286
311
  typeof value === "object" &&
@@ -373,13 +398,8 @@ export async function searchLogsImpl(
373
398
  }
374
399
 
375
400
  export async function getLogImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
376
- const res = await callApi(`/api/logs/${id}`);
377
- if (!res.ok) return toolError(`GET /api/logs/${id} returned ${res.status}`);
378
- const rawBody: unknown = await res.json();
379
- const parsed = CapturedLogSchema.safeParse(rawBody);
380
- if (!parsed.success) {
381
- return toolError(`GET /api/logs/${id} returned an unparseable CapturedLog`);
382
- }
401
+ const parsed = await fetchApiJson(callApi, `/api/logs/${id}`, CapturedLogSchema);
402
+ if (!parsed.ok) return parsed.error;
383
403
  return textJson(buildBoundedLogDetail(parsed.data));
384
404
  }
385
405
 
@@ -413,11 +433,8 @@ export async function getSessionImpl(
413
433
  params.set("includeHistory", "1");
414
434
  }
415
435
  const path = `/api/sessions?${params.toString()}`;
416
- const res = await callApi(path);
417
- if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
418
- const rawBody: unknown = await res.json();
419
- const parsed = SessionInfoSchema.safeParse(rawBody);
420
- if (!parsed.success) return toolError("GET /api/sessions returned an unparseable session info");
436
+ const parsed = await fetchApiJson(callApi, path, SessionInfoSchema);
437
+ if (!parsed.ok) return parsed.error;
421
438
  return textJson(parsed.data);
422
439
  }
423
440
 
@@ -724,13 +741,8 @@ export async function getProcessTimelineImpl(
724
741
  }
725
742
 
726
743
  export async function getLogHeadersImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
727
- const res = await callApi(`/api/logs/${id}`);
728
- if (!res.ok) return toolError(`GET /api/logs/${id} returned ${res.status}`);
729
- const rawBody: unknown = await res.json();
730
- const parsed = CapturedLogSchema.safeParse(rawBody);
731
- if (!parsed.success) {
732
- return toolError(`GET /api/logs/${id} returned an unparseable CapturedLog`);
733
- }
744
+ const parsed = await fetchApiJson(callApi, `/api/logs/${id}`, CapturedLogSchema);
745
+ if (!parsed.ok) return parsed.error;
734
746
  const log = parsed.data;
735
747
  return textJson({
736
748
  id: log.id,
@@ -1045,31 +1057,14 @@ export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
1045
1057
  return textJson(await res.json());
1046
1058
  }
1047
1059
 
1048
- export type ProviderReadArgs = {
1049
- includeSecrets?: boolean;
1050
- };
1051
-
1052
- function includeSecretsQuery(includeSecrets: boolean | undefined): string {
1053
- return includeSecrets === true ? "?includeSecrets=1" : "";
1054
- }
1055
-
1056
- export async function listProvidersImpl(
1057
- callApi: CallApiFn,
1058
- args: ProviderReadArgs = {},
1059
- ): Promise<ToolResult> {
1060
- const res = await callApi(`/api/providers${includeSecretsQuery(args.includeSecrets)}`);
1060
+ export async function listProvidersImpl(callApi: CallApiFn): Promise<ToolResult> {
1061
+ const res = await callApi("/api/providers");
1061
1062
  if (!res.ok) return toolError(`GET /api/providers returned ${res.status}`);
1062
1063
  return textJson(await res.json());
1063
1064
  }
1064
1065
 
1065
- export async function getProviderImpl(
1066
- callApi: CallApiFn,
1067
- id: string,
1068
- args: ProviderReadArgs = {},
1069
- ): Promise<ToolResult> {
1070
- const res = await callApi(
1071
- `/api/providers/${encodeURIComponent(id)}${includeSecretsQuery(args.includeSecrets)}`,
1072
- );
1066
+ export async function getProviderImpl(callApi: CallApiFn, id: string): Promise<ToolResult> {
1067
+ const res = await callApi(`/api/providers/${encodeURIComponent(id)}`);
1073
1068
  if (!res.ok) return toolError(`GET /api/providers/${id} returned ${res.status}`);
1074
1069
  return textJson(await res.json());
1075
1070
  }
@@ -1182,10 +1177,6 @@ type ProviderDiagnosticIssue = {
1182
1177
  endpoint: string | null;
1183
1178
  };
1184
1179
 
1185
- function isRecord(value: unknown): value is Record<string, unknown> {
1186
- return typeof value === "object" && value !== null && !Array.isArray(value);
1187
- }
1188
-
1189
1180
  function stringField(record: Record<string, unknown>, key: string): string | null {
1190
1181
  const value = record[key];
1191
1182
  return typeof value === "string" && value.length > 0 ? value : null;
@@ -26,12 +26,29 @@ export const HEADER_HOST = "host";
26
26
  // Auth header types
27
27
  export const AUTH_HEADER_X_API_KEY = "x-api-key";
28
28
 
29
+ // Headers whose values contain secrets (API keys, bearer tokens).
30
+ // These are redacted before being written to logs when captureMode is "full".
31
+ export const SENSITIVE_HEADERS = new Set([
32
+ HEADER_AUTHORIZATION,
33
+ HEADER_X_API_KEY,
34
+ "cookie",
35
+ "set-cookie",
36
+ "proxy-authorization",
37
+ ]);
38
+
29
39
  // Content types
30
40
  export const CONTENT_TYPE_EVENT_STREAM = "text/event-stream";
31
41
 
32
42
  // Response status
33
43
  export const STATUS_FORBIDDEN = 403;
34
44
  export const STATUS_BAD_GATEWAY = 502;
45
+ export const STATUS_PAYLOAD_TOO_LARGE = 413;
46
+
47
+ // Maximum request body size accepted by the proxy (50 MB).
48
+ // Legitimate LLM API requests (system prompts, conversation history, tool
49
+ // definitions) are well under this limit; larger payloads are almost certainly
50
+ // malicious or malformed.
51
+ export const MAX_REQUEST_BODY_BYTES = 50 * 1024 * 1024;
35
52
 
36
53
  // Standard/safe headers to preserve when forwarding to upstream.
37
54
  // NOTE: Authorization is intentionally excluded to prevent client auth tokens from leaking.
@@ -96,86 +96,113 @@ function hasStreamingMismatch(session: SessionInfo): boolean {
96
96
  return false;
97
97
  }
98
98
 
99
- function categorySummary(category: FailureCategory): string {
100
- switch (category) {
101
- case "none":
102
- return "Session completed without captured errors.";
103
- case "active":
104
- return "Session still has active requests or running tasks.";
105
- case "provider-timeout":
106
- return "Provider call appears to have timed out.";
107
- case "provider-http-error":
108
- return "Provider returned an HTTP error.";
109
- case "provider-auth-error":
110
- return "Provider authentication or API key configuration appears invalid.";
111
- case "provider-rate-limit":
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
- switch (category) {
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
- switch (category) {
247
- case "none":
248
- return "success";
249
- case "active":
250
- return "active";
251
- case "no-session-data":
252
- return "unknown";
253
- case "provider-timeout":
254
- case "provider-http-error":
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(
@@ -13,7 +13,11 @@ export const AnthropicFormatHandler: FormatHandler = {
13
13
  if (result.success) {
14
14
  return {
15
15
  model: result.data.model,
16
- sessionId: result.data.metadata?.user_id ?? headers?.get("x-session-affinity") ?? null,
16
+ sessionId:
17
+ result.data.metadata?.user_id ??
18
+ headers?.get("x-agent-inspector-session-id") ??
19
+ headers?.get("x-session-affinity") ??
20
+ null,
17
21
  };
18
22
  }
19
23
  return null;
@@ -1,12 +1,6 @@
1
1
  export {
2
2
  AnthropicRequestSchema,
3
3
  AnthropicResponseSchema,
4
- InspectorRequestSchema,
5
- InspectorResponseSchema,
6
4
  SseEventSchema,
7
5
  } from "../../../contracts/anthropic";
8
- export type {
9
- AnthropicResponse,
10
- InspectorResponse,
11
- ResponseContentBlockType,
12
- } from "../../../contracts/anthropic";
6
+ export type { AnthropicResponse, ResponseContentBlockType } from "../../../contracts/anthropic";
@@ -9,5 +9,5 @@ import "./openai/zhipuProvider";
9
9
  import "./openai/alibabaProvider";
10
10
 
11
11
  export type { FormatHandler, ParsedRequest } from "./handler";
12
- export { apiFormatForPath, formatRegistry, formatForPath, requestFormatForPath } from "./registry";
12
+ export { apiFormatForPath, formatRegistry, formatForPath } from "./registry";
13
13
  export { registry } from "./providers";
@@ -20,9 +20,6 @@ export {
20
20
  } from "../../../contracts/openai";
21
21
  export type {
22
22
  OpenAIResponse,
23
- OpenAIResponsesRequest,
24
23
  OpenAIResponsesResponse,
25
- OpenAIResponsesSSEvent,
26
- OpenAIResponsesOutputItem,
27
24
  OpenAIToolCall,
28
25
  } from "../../../contracts/openai";
@@ -72,9 +72,6 @@ export function apiFormatForPath(path: string): ApiFormat {
72
72
  return formatRegistry.getApiFormatByPath(path) ?? "unknown";
73
73
  }
74
74
 
75
- /** Backward-compatible alias for older internal imports. */
76
- export const requestFormatForPath = apiFormatForPath;
77
-
78
75
  // Register known paths
79
76
  formatRegistry.registerPath(PATH_V1_MESSAGES, "anthropic");
80
77
  formatRegistry.registerPath(PATH_V1_CHAT_COMPLETIONS, "openai");
@@ -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 || !existsSync(path)) return null;
347
- try {
348
- const raw: unknown = JSON.parse(readFileSync(path, "utf8"));
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
  }
@@ -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 filePath = groupsFilePath();
74
- if (!existsSync(filePath)) return [];
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 {