@tonyclaw/agent-inspector 2.0.25 → 2.0.27

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 (45) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DVNvS0GQ.js → CompareDrawer-B_p6vmMQ.js} +1 -1
  3. package/.output/public/assets/{ProxyViewerContainer-By3XtQhZ.js → ProxyViewerContainer-DItqh1EO.js} +4 -4
  4. package/.output/public/assets/{ReplayDialog-Cw6bN-NR.js → ReplayDialog-BmprJ6D3.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-DnIsirBE.js → RequestAnatomy-DqO0_SVJ.js} +1 -1
  6. package/.output/public/assets/{ResponseView-ygsUs7FU.js → ResponseView-FZbPNdHa.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-DMUn8qXi.js → StreamingChunkSequence-BZPzfJWj.js} +1 -1
  8. package/.output/public/assets/_sessionId-y2ATIp51.js +1 -0
  9. package/.output/public/assets/index-CQT4higx.js +1 -0
  10. package/.output/public/assets/{main-kBQ49n13.js → main-CzItFZlM.js} +2 -2
  11. package/.output/server/_libs/modelcontextprotocol__server.mjs +228 -0
  12. package/.output/server/{_sessionId-CYKaI68v.mjs → _sessionId-DZH8SrEZ.mjs} +3 -3
  13. package/.output/server/_ssr/{CompareDrawer-CHk4p41X.mjs → CompareDrawer-L0aE1UV1.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-Co9ENCKV.mjs → ProxyViewerContainer-B62RB9ER.mjs} +7 -7
  15. package/.output/server/_ssr/{ReplayDialog-BRs3fk8H.mjs → ReplayDialog-FVWnpx2C.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-Cb68EeZz.mjs → RequestAnatomy-DIyqW-Ny.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-DGdbfEjs.mjs → ResponseView-BX4mxEZ5.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-DptYgirK.mjs → StreamingChunkSequence-Cs3nzOor.mjs} +2 -2
  19. package/.output/server/_ssr/{index-Ef5mCuww.mjs → index-Dik2Mc3h.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-Cvv7SJnd.mjs → router-DCPg8ykx.mjs} +2087 -80
  22. package/.output/server/_tanstack-start-manifest_v-BaoL3JCh.mjs +4 -0
  23. package/.output/server/index.mjs +61 -61
  24. package/README.md +55 -1
  25. package/package.json +1 -1
  26. package/src/lib/runContract.ts +162 -0
  27. package/src/lib/sessionInfoContract.ts +69 -0
  28. package/src/mcp/server.ts +580 -2
  29. package/src/mcp/toolHandlers.ts +181 -0
  30. package/src/proxy/evidenceAnalysis.ts +522 -0
  31. package/src/proxy/evidenceExporter.ts +215 -0
  32. package/src/proxy/logSearch.ts +118 -0
  33. package/src/proxy/runFailures.ts +100 -0
  34. package/src/proxy/runStore.ts +159 -0
  35. package/src/proxy/sessionInfo.ts +222 -0
  36. package/src/proxy/sessionSupervisor.ts +8 -5
  37. package/src/proxy/store.ts +77 -0
  38. package/src/routes/api/logs.ts +16 -0
  39. package/src/routes/api/runs.$runId.evidence.ts +62 -0
  40. package/src/routes/api/runs.$runId.ts +50 -0
  41. package/src/routes/api/runs.ts +58 -0
  42. package/src/routes/api/sessions.ts +29 -2
  43. package/.output/public/assets/_sessionId-DoqvnFx7.js +0 -1
  44. package/.output/public/assets/index-B0YcFeL-.js +0 -1
  45. package/.output/server/_tanstack-start-manifest_v-DQoXxIFQ.mjs +0 -4
@@ -0,0 +1,522 @@
1
+ import type { SessionInfo, SessionLogSummary } from "../lib/sessionInfoContract";
2
+ import type {
3
+ FailureCategory,
4
+ FailureClassification,
5
+ InspectorRun,
6
+ JenkinsReport,
7
+ RunTimelineEvent,
8
+ } from "../lib/runContract";
9
+
10
+ type AnalyzeEvidenceInput = {
11
+ run: InspectorRun;
12
+ session: SessionInfo | null;
13
+ exportedAt: string;
14
+ evidencePaths: {
15
+ jsonPath: string;
16
+ markdownPath: string;
17
+ htmlPath: string;
18
+ };
19
+ };
20
+
21
+ type ErrorCandidate = {
22
+ log: SessionLogSummary | null;
23
+ text: string;
24
+ };
25
+
26
+ function formatNullable(value: string | number | null): string {
27
+ return value === null ? "n/a" : String(value);
28
+ }
29
+
30
+ function formatList(values: readonly string[]): string {
31
+ return values.length > 0 ? values.join(", ") : "n/a";
32
+ }
33
+
34
+ function lower(value: string): string {
35
+ return value.toLowerCase();
36
+ }
37
+
38
+ function logEvidenceText(log: SessionLogSummary): string {
39
+ return lower(
40
+ [
41
+ log.error,
42
+ log.path,
43
+ log.model,
44
+ log.provider,
45
+ log.status === null ? null : String(log.status),
46
+ log.isStreaming ? "streaming" : "non-streaming",
47
+ ]
48
+ .filter((value) => value !== null)
49
+ .join(" "),
50
+ );
51
+ }
52
+
53
+ function collectErrorCandidates(session: SessionInfo): ErrorCandidate[] {
54
+ const candidates: ErrorCandidate[] = session.latestLogs
55
+ .filter((log) => log.hasError)
56
+ .map((log) => ({ log, text: logEvidenceText(log) }));
57
+
58
+ if (session.lastTaskError !== null) {
59
+ candidates.push({ log: null, text: lower(session.lastTaskError) });
60
+ }
61
+
62
+ return candidates;
63
+ }
64
+
65
+ function hasText(candidates: readonly ErrorCandidate[], patterns: readonly string[]): boolean {
66
+ return candidates.some((candidate) =>
67
+ patterns.some((pattern) => candidate.text.includes(pattern)),
68
+ );
69
+ }
70
+
71
+ function hasStatus(session: SessionInfo, predicate: (status: number) => boolean): boolean {
72
+ return session.latestLogs.some((log) => log.status !== null && predicate(log.status));
73
+ }
74
+
75
+ function evidenceIds(candidates: readonly ErrorCandidate[]): number[] {
76
+ const ids = new Set<number>();
77
+ for (const candidate of candidates) {
78
+ if (candidate.log !== null) {
79
+ ids.add(candidate.log.id);
80
+ }
81
+ }
82
+ return [...ids].sort((left, right) => left - right);
83
+ }
84
+
85
+ function hasStreamingMismatch(session: SessionInfo): boolean {
86
+ for (const failed of session.latestLogs.filter((log) => log.hasError)) {
87
+ const matchingSuccess = session.latestLogs.some(
88
+ (log) =>
89
+ !log.hasError &&
90
+ log.model === failed.model &&
91
+ log.provider === failed.provider &&
92
+ log.isStreaming !== failed.isStreaming,
93
+ );
94
+ if (matchingSuccess) return true;
95
+ }
96
+ return false;
97
+ }
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
+ ];
177
+ }
178
+ }
179
+
180
+ function classifyCategory(
181
+ session: SessionInfo,
182
+ candidates: readonly ErrorCandidate[],
183
+ ): FailureCategory {
184
+ if (session.status === "active") return "active";
185
+ if (session.errorCount === 0 && session.failedRequests === 0 && session.lastTaskError === null) {
186
+ return "none";
187
+ }
188
+ if (hasStreamingMismatch(session)) return "streaming-mismatch";
189
+ if (
190
+ hasText(candidates, [
191
+ "timeout",
192
+ "timed out",
193
+ "etimedout",
194
+ "deadline",
195
+ "aborterror",
196
+ "econnaborted",
197
+ ])
198
+ ) {
199
+ return "provider-timeout";
200
+ }
201
+ if (hasText(candidates, ["localhost", "econnrefused", "enotfound", "network", "fetch failed"])) {
202
+ return "network-or-container";
203
+ }
204
+ if (hasStatus(session, (status) => status === 401 || status === 403)) {
205
+ return "provider-auth-error";
206
+ }
207
+ if (hasText(candidates, ["unauthorized", "forbidden", "invalid api key", "api key"])) {
208
+ return "provider-auth-error";
209
+ }
210
+ if (hasStatus(session, (status) => status === 429)) return "provider-rate-limit";
211
+ if (hasText(candidates, ["rate limit", "quota"])) return "provider-rate-limit";
212
+ if (hasText(candidates, ["context", "too many tokens", "token limit", "maximum context"])) {
213
+ return "context-pressure";
214
+ }
215
+ if (session.lastTaskError !== null) return "runtime-task-error";
216
+ if (hasText(candidates, ["invalid json", "schema", "parse", "malformed"])) {
217
+ return "model-output-error";
218
+ }
219
+ if (hasStatus(session, (status) => status >= 400)) return "provider-http-error";
220
+ return "unknown-failure";
221
+ }
222
+
223
+ 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
+ }
243
+ }
244
+
245
+ 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
+ }
265
+ }
266
+
267
+ export function classifyRunFailure(
268
+ run: InspectorRun,
269
+ session: SessionInfo | null,
270
+ ): FailureClassification {
271
+ if (session === null) {
272
+ const category: FailureCategory = "no-session-data";
273
+ return {
274
+ outcome: outcomeFor(category),
275
+ severity: severityFor(category),
276
+ category,
277
+ summary: `${categorySummary(category)} Run ${run.id} is bound to session ${run.sessionId}.`,
278
+ evidenceLogIds: [],
279
+ hints: categoryHints(category),
280
+ };
281
+ }
282
+
283
+ const candidates = collectErrorCandidates(session);
284
+ const category = classifyCategory(session, candidates);
285
+ const summary =
286
+ category === "none"
287
+ ? `${categorySummary(category)} ${String(session.requestCount)} request(s), ${String(
288
+ session.tokenUsage.total,
289
+ )} token(s).`
290
+ : `${categorySummary(category)} ${String(session.errorCount)} error log(s), ${String(
291
+ session.failedRequests,
292
+ )} failed request(s).`;
293
+
294
+ return {
295
+ outcome: outcomeFor(category),
296
+ severity: severityFor(category),
297
+ category,
298
+ summary,
299
+ evidenceLogIds: evidenceIds(candidates),
300
+ hints: categoryHints(category),
301
+ };
302
+ }
303
+
304
+ function logTitle(log: SessionLogSummary): string {
305
+ if (log.hasError) return `Request failed: log ${String(log.id)}`;
306
+ if (log.status !== null && log.status >= 200 && log.status < 400) {
307
+ return `Request succeeded: log ${String(log.id)}`;
308
+ }
309
+ return `Request captured: log ${String(log.id)}`;
310
+ }
311
+
312
+ function logDetails(log: SessionLogSummary): string {
313
+ const parts = [
314
+ `path=${log.path}`,
315
+ `status=${formatNullable(log.status)}`,
316
+ `model=${formatNullable(log.model)}`,
317
+ `provider=${formatNullable(log.provider)}`,
318
+ `streaming=${log.isStreaming ? "yes" : "no"}`,
319
+ `latencyMs=${formatNullable(log.latencyMs)}`,
320
+ ];
321
+ if (log.error !== null) {
322
+ parts.push(`error=${log.error}`);
323
+ }
324
+ return parts.join(", ");
325
+ }
326
+
327
+ export function buildRunTimeline(
328
+ run: InspectorRun,
329
+ session: SessionInfo | null,
330
+ exportedAt: string,
331
+ ): RunTimelineEvent[] {
332
+ const events: RunTimelineEvent[] = [
333
+ {
334
+ timestamp: run.createdAt,
335
+ kind: "run-created",
336
+ severity: "info",
337
+ title: "Run declared",
338
+ details: `Run ${run.id} was declared for session ${run.sessionId}.`,
339
+ logId: null,
340
+ status: null,
341
+ model: null,
342
+ provider: null,
343
+ latencyMs: null,
344
+ isStreaming: null,
345
+ },
346
+ ];
347
+
348
+ if (session !== null && session.createdAt !== null) {
349
+ events.push({
350
+ timestamp: session.createdAt,
351
+ kind: "session-started",
352
+ severity: "info",
353
+ title: "Session started",
354
+ details: `Inspector session ${session.id} started with source ${session.source ?? "unknown"}.`,
355
+ logId: null,
356
+ status: null,
357
+ model: null,
358
+ provider: null,
359
+ latencyMs: null,
360
+ isStreaming: null,
361
+ });
362
+ }
363
+
364
+ if (session !== null) {
365
+ const logs = [...session.latestLogs].sort((left, right) =>
366
+ left.timestamp.localeCompare(right.timestamp),
367
+ );
368
+ for (const log of logs) {
369
+ events.push({
370
+ timestamp: log.timestamp,
371
+ kind: "request",
372
+ severity: log.hasError ? "error" : "success",
373
+ title: logTitle(log),
374
+ details: logDetails(log),
375
+ logId: log.id,
376
+ status: log.status,
377
+ model: log.model,
378
+ provider: log.provider,
379
+ latencyMs: log.latencyMs,
380
+ isStreaming: log.isStreaming,
381
+ });
382
+ }
383
+
384
+ if (session.updatedAt !== null) {
385
+ events.push({
386
+ timestamp: session.updatedAt,
387
+ kind: "session-finished",
388
+ severity:
389
+ session.status === "failed"
390
+ ? "error"
391
+ : session.status === "active"
392
+ ? "warning"
393
+ : "success",
394
+ title: `Session ${session.status}`,
395
+ details: `${String(session.requestCount)} request(s), ${String(
396
+ session.errorCount,
397
+ )} error(s), ${String(session.tokenUsage.total)} token(s).`,
398
+ logId: session.lastLogId,
399
+ status: null,
400
+ model: session.lastModel,
401
+ provider: null,
402
+ latencyMs: null,
403
+ isStreaming: null,
404
+ });
405
+ }
406
+ }
407
+
408
+ events.push({
409
+ timestamp: exportedAt,
410
+ kind: "evidence-exported",
411
+ severity: "info",
412
+ title: "Evidence exported",
413
+ details: `Evidence pack exported at ${exportedAt}.`,
414
+ logId: null,
415
+ status: null,
416
+ model: null,
417
+ provider: null,
418
+ latencyMs: null,
419
+ isStreaming: null,
420
+ });
421
+
422
+ return events.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
423
+ }
424
+
425
+ function reportStatus(classification: FailureClassification): JenkinsReport["status"] {
426
+ switch (classification.outcome) {
427
+ case "success":
428
+ return "PASS";
429
+ case "failure":
430
+ return "FAIL";
431
+ case "active":
432
+ return "RUNNING";
433
+ case "unknown":
434
+ return "UNKNOWN";
435
+ }
436
+ }
437
+
438
+ function timelineMarkdown(timeline: readonly RunTimelineEvent[]): string[] {
439
+ if (timeline.length === 0) return ["No timeline events were available."];
440
+ return [
441
+ "| Time | Event | Severity | Evidence |",
442
+ "| --- | --- | --- | --- |",
443
+ ...timeline.map((event) => {
444
+ const evidence = event.logId === null ? "n/a" : `log ${String(event.logId)}`;
445
+ return `| ${event.timestamp} | ${event.title} | ${event.severity} | ${evidence} |`;
446
+ }),
447
+ ];
448
+ }
449
+
450
+ export function buildJenkinsReport(input: {
451
+ run: InspectorRun;
452
+ session: SessionInfo | null;
453
+ timeline: readonly RunTimelineEvent[];
454
+ classification: FailureClassification;
455
+ evidencePaths: AnalyzeEvidenceInput["evidencePaths"];
456
+ }): JenkinsReport {
457
+ const status = reportStatus(input.classification);
458
+ const session = input.session;
459
+ const summary = `[${status}] ${input.classification.summary}`;
460
+ const lines = [
461
+ `# Jenkins Report: ${input.run.title}`,
462
+ "",
463
+ `- Status: ${status}`,
464
+ `- Run ID: ${input.run.id}`,
465
+ `- Session ID: ${input.run.sessionId}`,
466
+ `- Inspector URL: ${session?.inspectorUrl ?? "n/a"}`,
467
+ `- Category: ${input.classification.category}`,
468
+ `- Severity: ${input.classification.severity}`,
469
+ `- Summary: ${input.classification.summary}`,
470
+ `- Evidence Logs: ${
471
+ input.classification.evidenceLogIds.length > 0
472
+ ? input.classification.evidenceLogIds.map((id) => `#${String(id)}`).join(", ")
473
+ : "n/a"
474
+ }`,
475
+ "",
476
+ "## Session Metrics",
477
+ "",
478
+ `- Requests: ${String(session?.requestCount ?? 0)}`,
479
+ `- Errors: ${String(session?.errorCount ?? 0)}`,
480
+ `- Models: ${formatList(session?.models ?? [])}`,
481
+ `- Providers: ${formatList(session?.providers ?? [])}`,
482
+ `- Tokens: ${String(session?.tokenUsage.total ?? 0)}`,
483
+ "",
484
+ "## Timeline",
485
+ "",
486
+ ...timelineMarkdown(input.timeline),
487
+ "",
488
+ "## Artifacts",
489
+ "",
490
+ `- JSON: ${input.evidencePaths.jsonPath}`,
491
+ `- Markdown: ${input.evidencePaths.markdownPath}`,
492
+ `- HTML: ${input.evidencePaths.htmlPath}`,
493
+ "",
494
+ "## Next Actions",
495
+ "",
496
+ ...input.classification.hints.map((hint) => `- ${hint}`),
497
+ "",
498
+ ];
499
+
500
+ return {
501
+ status,
502
+ summary,
503
+ markdown: lines.join("\n"),
504
+ };
505
+ }
506
+
507
+ export function analyzeEvidence(input: AnalyzeEvidenceInput): {
508
+ classification: FailureClassification;
509
+ timeline: RunTimelineEvent[];
510
+ jenkinsReport: JenkinsReport;
511
+ } {
512
+ const classification = classifyRunFailure(input.run, input.session);
513
+ const timeline = buildRunTimeline(input.run, input.session, input.exportedAt);
514
+ const jenkinsReport = buildJenkinsReport({
515
+ run: input.run,
516
+ session: input.session,
517
+ timeline,
518
+ classification,
519
+ evidencePaths: input.evidencePaths,
520
+ });
521
+ return { classification, timeline, jenkinsReport };
522
+ }