@tonyclaw/agent-inspector 2.0.23 → 2.0.25

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 (41) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DnYQtd8R.js → CompareDrawer-DVNvS0GQ.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-By3XtQhZ.js +115 -0
  4. package/.output/public/assets/{ReplayDialog-B-3V95xT.js → ReplayDialog-Cw6bN-NR.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-CT8hLGLa.js → RequestAnatomy-DnIsirBE.js} +1 -1
  6. package/.output/public/assets/{ResponseView-DK3CYom0.js → ResponseView-ygsUs7FU.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-D3jJutjp.js → StreamingChunkSequence-DMUn8qXi.js} +1 -1
  8. package/.output/public/assets/_sessionId-DoqvnFx7.js +1 -0
  9. package/.output/public/assets/index-B0YcFeL-.js +1 -0
  10. package/.output/public/assets/index-DsiKfWCp.css +1 -0
  11. package/.output/public/assets/{main-hF_572U6.js → main-kBQ49n13.js} +2 -2
  12. package/.output/server/{_sessionId-BqhNE2M8.mjs → _sessionId-CYKaI68v.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-C4LQW4S1.mjs → CompareDrawer-CHk4p41X.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-BKInvawk.mjs → ProxyViewerContainer-Co9ENCKV.mjs} +276 -47
  15. package/.output/server/_ssr/{ReplayDialog-C3ZiRihL.mjs → ReplayDialog-BRs3fk8H.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-DlVAPIQZ.mjs → RequestAnatomy-Cb68EeZz.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-nMS0rkxu.mjs → ResponseView-DGdbfEjs.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-PDWqrNiO.mjs → StreamingChunkSequence-DptYgirK.mjs} +2 -2
  19. package/.output/server/_ssr/{index-Cd1zD-lo.mjs → index-Ef5mCuww.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-R9gm7_sh.mjs → router-Cvv7SJnd.mjs} +192 -15
  22. package/.output/server/{_tanstack-start-manifest_v-BnLVH_EX.mjs → _tanstack-start-manifest_v-DQoXxIFQ.mjs} +1 -1
  23. package/.output/server/index.mjs +56 -56
  24. package/package.json +1 -1
  25. package/src/components/ProxyViewer.tsx +11 -1
  26. package/src/components/ProxyViewerContainer.tsx +21 -15
  27. package/src/components/providers/ProviderForm.tsx +176 -16
  28. package/src/lib/export-logs.ts +54 -11
  29. package/src/lib/providerTestPrompt.ts +4 -4
  30. package/src/lib/utils.ts +46 -0
  31. package/src/mcp/toolHandlers.ts +58 -8
  32. package/src/proxy/logFinalizer.ts +79 -1
  33. package/src/proxy/logIndex.ts +6 -1
  34. package/src/proxy/schemas.ts +3 -0
  35. package/src/proxy/store.ts +79 -2
  36. package/src/routes/api/logs.stream.ts +5 -2
  37. package/src/routes/api/logs.ts +4 -1
  38. package/.output/public/assets/ProxyViewerContainer-BTYGkg36.js +0 -115
  39. package/.output/public/assets/_sessionId-DpdfAzbo.js +0 -1
  40. package/.output/public/assets/index-780zTVwp.css +0 -1
  41. package/.output/public/assets/index-BpJNbm9G.js +0 -1
@@ -1,4 +1,5 @@
1
- import { readFileSync, rmSync } from "node:fs";
1
+ import { Buffer } from "node:buffer";
2
+ import { readFileSync, rmSync, statSync } from "node:fs";
2
3
  import { Worker } from "node:worker_threads";
3
4
  import { writeChunks } from "./chunkStorage";
4
5
  import { formatForPath } from "./formats";
@@ -46,6 +47,16 @@ export type FinalizeLogResult = {
46
47
  cleanupRawStreamPath?: string | null;
47
48
  };
48
49
 
50
+ const DEFAULT_MAX_FINALIZE_STREAM_BYTES = 20 * 1024 * 1024;
51
+
52
+ function resolveMaxFinalizeStreamBytes(): number {
53
+ const raw = process.env["AGENT_INSPECTOR_MAX_FINALIZE_STREAM_BYTES"];
54
+ if (raw === undefined || raw === "") return DEFAULT_MAX_FINALIZE_STREAM_BYTES;
55
+ const parsed = Number(raw);
56
+ if (!Number.isFinite(parsed) || parsed < 1) return DEFAULT_MAX_FINALIZE_STREAM_BYTES;
57
+ return Math.floor(parsed);
58
+ }
59
+
49
60
  export function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record<string, unknown> {
50
61
  return {
51
62
  timestamp: log.timestamp,
@@ -148,6 +159,50 @@ function cleanupPathForRawStream(rawStream: string | RawStreamSource): string |
148
159
  }
149
160
  }
150
161
 
162
+ type OversizedRawStream = {
163
+ byteLength: number;
164
+ limit: number;
165
+ };
166
+
167
+ function rawStreamByteLength(rawStream: string | RawStreamSource): number | null {
168
+ if (typeof rawStream === "string") {
169
+ return Buffer.byteLength(rawStream, "utf8");
170
+ }
171
+
172
+ switch (rawStream.type) {
173
+ case "memory":
174
+ return Buffer.byteLength(rawStream.rawStream, "utf8");
175
+ case "file":
176
+ try {
177
+ return statSync(rawStream.path).size;
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+ }
183
+
184
+ function getOversizedRawStream(rawStream: string | RawStreamSource): OversizedRawStream | null {
185
+ const limit = resolveMaxFinalizeStreamBytes();
186
+ const byteLength = rawStreamByteLength(rawStream);
187
+ if (byteLength === null || byteLength <= limit) return null;
188
+ return { byteLength, limit };
189
+ }
190
+
191
+ function formatBytes(bytes: number): string {
192
+ if (bytes < 1024) return `${bytes} bytes`;
193
+ if (bytes < 1024 * 1024) return `${Math.ceil(bytes / 1024)} KB`;
194
+ return `${Math.ceil(bytes / (1024 * 1024))} MB`;
195
+ }
196
+
197
+ function oversizedRawStreamText(oversized: OversizedRawStream): string {
198
+ return (
199
+ `Streaming response omitted because the captured raw stream is ${formatBytes(
200
+ oversized.byteLength,
201
+ )}, exceeding the finalize limit of ${formatBytes(oversized.limit)}. ` +
202
+ "Increase AGENT_INSPECTOR_MAX_FINALIZE_STREAM_BYTES if this response must be parsed."
203
+ );
204
+ }
205
+
151
206
  function readRawStream(rawStream: string | RawStreamSource): {
152
207
  rawStreamText: string;
153
208
  cleanupRawStreamPath: string | null;
@@ -180,6 +235,19 @@ function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): Fina
180
235
  );
181
236
  }
182
237
 
238
+ const oversized = getOversizedRawStream(job.rawStream);
239
+ if (oversized !== null) {
240
+ log.elapsedMs = job.elapsedMs;
241
+ log.responseStatus = job.responseStatus;
242
+ log.responseText = oversizedRawStreamText(oversized);
243
+ return {
244
+ log,
245
+ upstreamUrl: job.upstreamUrl,
246
+ error: null,
247
+ cleanupRawStreamPath: cleanupPathForRawStream(job.rawStream),
248
+ };
249
+ }
250
+
183
251
  let rawStreamText = "";
184
252
  let cleanupRawStreamPath: string | null = null;
185
253
  try {
@@ -227,6 +295,16 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
227
295
  try {
228
296
  log.elapsedMs = job.elapsedMs;
229
297
  if (job.hasChunks && formatHandler !== null) {
298
+ const oversized = getOversizedRawStream(job.rawStream);
299
+ if (oversized !== null) {
300
+ log.responseText = `Client aborted. ${oversizedRawStreamText(oversized)}`;
301
+ return {
302
+ log,
303
+ upstreamUrl: job.upstreamUrl,
304
+ error: "Client aborted",
305
+ cleanupRawStreamPath: cleanupPathForRawStream(job.rawStream),
306
+ };
307
+ }
230
308
  const raw = readRawStream(job.rawStream);
231
309
  rawStreamText = raw.rawStreamText;
232
310
  cleanupRawStreamPath = raw.cleanupRawStreamPath;
@@ -5,7 +5,7 @@ import { createInterface } from "node:readline";
5
5
  import { Buffer } from "node:buffer";
6
6
  import { resolveLogDir, logger } from "./logger";
7
7
 
8
- type LogIndexEntry = {
8
+ export type LogIndexEntry = {
9
9
  id: number;
10
10
  file: string;
11
11
  byteOffset: number;
@@ -144,6 +144,11 @@ export async function findInIndex(id: number): Promise<LogIndexEntry | null> {
144
144
  return index.entries[id] ?? null;
145
145
  }
146
146
 
147
+ export async function listIndexEntries(): Promise<LogIndexEntry[]> {
148
+ const index = await loadIndex();
149
+ return Object.values(index.entries).sort((left, right) => left.id - right.id);
150
+ }
151
+
147
152
  type FileIndexResult = {
148
153
  entries: Record<number, LogIndexEntry>;
149
154
  maxId: number;
@@ -56,6 +56,9 @@ export const CapturedLogSchema = z.object({
56
56
  clientProjectFolder: z.string().nullable().optional(),
57
57
  streamingChunks: StreamingChunksArraySchema.optional(),
58
58
  streamingChunksPath: z.string().nullable().optional(),
59
+ rawRequestBodyBytes: z.number().int().nonnegative().nullable().optional(),
60
+ responseTextBytes: z.number().int().nonnegative().nullable().optional(),
61
+ bodyContentMode: z.enum(["full", "compact", "truncated"]).optional(),
59
62
  /** Error message from streaming response (e.g., SSE error event) */
60
63
  error: z.string().nullable().optional(),
61
64
  });
@@ -25,7 +25,9 @@ import {
25
25
  flushIndex,
26
26
  getNextLogId,
27
27
  getCurrentLogFile,
28
+ listIndexEntries,
28
29
  rebuildIndex,
30
+ type LogIndexEntry,
29
31
  } from "./logIndex";
30
32
  import { writeChunks } from "./chunkStorage";
31
33
  import type { CapturedLog } from "./schemas";
@@ -75,6 +77,7 @@ export type ListLogsPageOptions = {
75
77
  model?: string;
76
78
  offset: number;
77
79
  limit: number;
80
+ includeBodies?: boolean;
78
81
  };
79
82
 
80
83
  export type ListLogsPageResult = {
@@ -116,6 +119,29 @@ function removeFromCache(id: number): void {
116
119
  memoryCache.delete(id);
117
120
  }
118
121
 
122
+ function textByteLength(value: string | null): number | null {
123
+ if (value === null) return null;
124
+ return Buffer.byteLength(value, "utf8");
125
+ }
126
+
127
+ export function compactLogForList(log: CapturedLog): CapturedLog {
128
+ return {
129
+ ...log,
130
+ rawRequestBody: null,
131
+ responseText: null,
132
+ rawHeaders: undefined,
133
+ headers: undefined,
134
+ streamingChunks: undefined,
135
+ rawRequestBodyBytes: textByteLength(log.rawRequestBody),
136
+ responseTextBytes: textByteLength(log.responseText),
137
+ bodyContentMode: "compact",
138
+ };
139
+ }
140
+
141
+ function prepareLogForList(log: CapturedLog, includeBodies: boolean): CapturedLog {
142
+ return includeBodies ? log : compactLogForList(log);
143
+ }
144
+
119
145
  /**
120
146
  * Add a test log entry to the in-memory store and persistent log file.
121
147
  * This is used by the provider test endpoint to seed the provider-test session.
@@ -385,11 +411,62 @@ async function visitPersistedLogFile(
385
411
  }
386
412
  }
387
413
 
414
+ function canUseIndexedListPage(options: ListLogsPageOptions): boolean {
415
+ return options.sessionId === undefined && options.model === undefined;
416
+ }
417
+
418
+ function hasUsableIndexOffset(entry: LogIndexEntry): boolean {
419
+ return entry.byteOffset >= 0 && entry.byteLength > 0;
420
+ }
421
+
422
+ async function listLogsPageFromIndex(
423
+ options: ListLogsPageOptions,
424
+ allowRebuild: boolean,
425
+ ): Promise<ListLogsPageResult | null> {
426
+ if (!canUseIndexedListPage(options)) return null;
427
+
428
+ let entries = await listIndexEntries();
429
+ if (entries.length === 0 && allowRebuild) {
430
+ await rebuildIndex();
431
+ entries = await listIndexEntries();
432
+ }
433
+
434
+ const pageStart = options.offset;
435
+ const pageEnd = options.offset + options.limit;
436
+ const pageEntries = entries.slice(pageStart, pageEnd);
437
+ const hasIncompleteOffset = pageEntries.some((entry) => !hasUsableIndexOffset(entry));
438
+
439
+ if (hasIncompleteOffset) {
440
+ if (!allowRebuild) return null;
441
+ await rebuildIndex();
442
+ return await listLogsPageFromIndex(options, false);
443
+ }
444
+
445
+ const logs: CapturedLog[] = [];
446
+ const includeBodies = options.includeBodies !== false;
447
+ for (const entry of pageEntries) {
448
+ const log = await getLogById(entry.id);
449
+ if (log === null) return null;
450
+ logs.push(prepareLogForList(log, includeBodies));
451
+ }
452
+
453
+ return {
454
+ logs,
455
+ total: entries.length,
456
+ offset: options.offset,
457
+ limit: options.limit,
458
+ };
459
+ }
460
+
388
461
  export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLogsPageResult> {
462
+ const indexedPage = await listLogsPageFromIndex(options, true);
463
+ if (indexedPage !== null) return indexedPage;
464
+
389
465
  const seenIds = new Set<number>();
390
466
  const pageById = new Map<number, CapturedLog>();
391
467
  const pageStart = options.offset;
392
468
  const pageEnd = options.offset + options.limit;
469
+ const includeBodies = options.includeBodies !== false;
393
470
  let total = 0;
394
471
 
395
472
  const visitLog = (log: CapturedLog): void => {
@@ -400,13 +477,13 @@ export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLo
400
477
  const position = total;
401
478
  total += 1;
402
479
  if (position >= pageStart && position < pageEnd) {
403
- pageById.set(log.id, log);
480
+ pageById.set(log.id, prepareLogForList(log, includeBodies));
404
481
  }
405
482
  return;
406
483
  }
407
484
 
408
485
  if (pageById.has(log.id)) {
409
- pageById.set(log.id, log);
486
+ pageById.set(log.id, prepareLogForList(log, includeBodies));
410
487
  }
411
488
  };
412
489
 
@@ -1,5 +1,5 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { onLogUpdate, getLogSessionId, listLogsPage } from "../../proxy/store";
2
+ import { compactLogForList, onLogUpdate, getLogSessionId, listLogsPage } from "../../proxy/store";
3
3
  import { type CapturedLog } from "../../proxy/schemas";
4
4
 
5
5
  const INITIAL_STREAM_LOG_LIMIT = 100;
@@ -11,6 +11,7 @@ export const Route = createFileRoute("/api/logs/stream")({
11
11
  const url = new URL(request.url);
12
12
  const sessionId = url.searchParams.get("sessionId") ?? undefined;
13
13
  const model = url.searchParams.get("model") ?? undefined;
14
+ const includeBodies = url.searchParams.get("compact") !== "1";
14
15
 
15
16
  let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
16
17
  let cleanedUp = false;
@@ -21,7 +22,8 @@ export const Route = createFileRoute("/api/logs/stream")({
21
22
  if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return;
22
23
  if (model !== undefined && log.model !== model) return;
23
24
  try {
24
- const data = `data: ${JSON.stringify({ type: "update", log })}\n\n`;
25
+ const payloadLog = includeBodies ? log : compactLogForList(log);
26
+ const data = `data: ${JSON.stringify({ type: "update", log: payloadLog })}\n\n`;
25
27
  controllerRef.enqueue(new TextEncoder().encode(data));
26
28
  } catch {
27
29
  cleanup();
@@ -59,6 +61,7 @@ export const Route = createFileRoute("/api/logs/stream")({
59
61
  model,
60
62
  offset: 0,
61
63
  limit: INITIAL_STREAM_LOG_LIMIT,
64
+ includeBodies,
62
65
  });
63
66
  const logs = result.logs;
64
67
  const initData = `data: ${JSON.stringify({ type: "init", logs })}\n\n`;
@@ -49,8 +49,11 @@ export const Route = createFileRoute("/api/logs")({
49
49
  const model = url.searchParams.get("model") ?? undefined;
50
50
  const offset = parseNonNegativeInt(url.searchParams.get("offset"), 0);
51
51
  const limit = parsePositiveInt(url.searchParams.get("limit"), 50);
52
+ const includeBodies = url.searchParams.get("compact") !== "1";
52
53
 
53
- return Response.json(await listLogsPage({ sessionId, model, offset, limit }));
54
+ return Response.json(
55
+ await listLogsPage({ sessionId, model, offset, limit, includeBodies }),
56
+ );
54
57
  },
55
58
  DELETE: async ({ request }: { request: Request }) => {
56
59
  let body: z.infer<typeof DeleteBodySchema> = undefined;