@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DnYQtd8R.js → CompareDrawer-DVNvS0GQ.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-By3XtQhZ.js +115 -0
- package/.output/public/assets/{ReplayDialog-B-3V95xT.js → ReplayDialog-Cw6bN-NR.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CT8hLGLa.js → RequestAnatomy-DnIsirBE.js} +1 -1
- package/.output/public/assets/{ResponseView-DK3CYom0.js → ResponseView-ygsUs7FU.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-D3jJutjp.js → StreamingChunkSequence-DMUn8qXi.js} +1 -1
- package/.output/public/assets/_sessionId-DoqvnFx7.js +1 -0
- package/.output/public/assets/index-B0YcFeL-.js +1 -0
- package/.output/public/assets/index-DsiKfWCp.css +1 -0
- package/.output/public/assets/{main-hF_572U6.js → main-kBQ49n13.js} +2 -2
- package/.output/server/{_sessionId-BqhNE2M8.mjs → _sessionId-CYKaI68v.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-C4LQW4S1.mjs → CompareDrawer-CHk4p41X.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-BKInvawk.mjs → ProxyViewerContainer-Co9ENCKV.mjs} +276 -47
- package/.output/server/_ssr/{ReplayDialog-C3ZiRihL.mjs → ReplayDialog-BRs3fk8H.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-DlVAPIQZ.mjs → RequestAnatomy-Cb68EeZz.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-nMS0rkxu.mjs → ResponseView-DGdbfEjs.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-PDWqrNiO.mjs → StreamingChunkSequence-DptYgirK.mjs} +2 -2
- package/.output/server/_ssr/{index-Cd1zD-lo.mjs → index-Ef5mCuww.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-R9gm7_sh.mjs → router-Cvv7SJnd.mjs} +192 -15
- package/.output/server/{_tanstack-start-manifest_v-BnLVH_EX.mjs → _tanstack-start-manifest_v-DQoXxIFQ.mjs} +1 -1
- package/.output/server/index.mjs +56 -56
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +11 -1
- package/src/components/ProxyViewerContainer.tsx +21 -15
- package/src/components/providers/ProviderForm.tsx +176 -16
- package/src/lib/export-logs.ts +54 -11
- package/src/lib/providerTestPrompt.ts +4 -4
- package/src/lib/utils.ts +46 -0
- package/src/mcp/toolHandlers.ts +58 -8
- package/src/proxy/logFinalizer.ts +79 -1
- package/src/proxy/logIndex.ts +6 -1
- package/src/proxy/schemas.ts +3 -0
- package/src/proxy/store.ts +79 -2
- package/src/routes/api/logs.stream.ts +5 -2
- package/src/routes/api/logs.ts +4 -1
- package/.output/public/assets/ProxyViewerContainer-BTYGkg36.js +0 -115
- package/.output/public/assets/_sessionId-DpdfAzbo.js +0 -1
- package/.output/public/assets/index-780zTVwp.css +0 -1
- package/.output/public/assets/index-BpJNbm9G.js +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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;
|
package/src/proxy/logIndex.ts
CHANGED
|
@@ -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;
|
package/src/proxy/schemas.ts
CHANGED
|
@@ -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
|
});
|
package/src/proxy/store.ts
CHANGED
|
@@ -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
|
|
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`;
|
package/src/routes/api/logs.ts
CHANGED
|
@@ -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(
|
|
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;
|