@tonyclaw/agent-inspector 2.0.22 → 2.0.24
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-NEdpe1wl.js → CompareDrawer-DrY-gAzy.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CiLvRNie.js +115 -0
- package/.output/public/assets/{ReplayDialog-DijzJwaQ.js → ReplayDialog-CFyw6Vu4.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CYbosGYy.js → RequestAnatomy-DHGJ9py7.js} +1 -1
- package/.output/public/assets/{ResponseView-B3dPM63X.js → ResponseView-CB_3yc08.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-DYDx3xRY.js → StreamingChunkSequence-CqhHwgDn.js} +1 -1
- package/.output/public/assets/_sessionId-CgG9CQ9r.js +1 -0
- package/.output/public/assets/index-CViEXkqa.js +1 -0
- package/.output/public/assets/{main-N-sjHJUM.js → main-CVw7-JyE.js} +2 -2
- package/.output/server/{_sessionId-BR46y_bO.mjs → _sessionId-V33sBWBC.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-DDxpmdPs.mjs → CompareDrawer-C_Fk9joM.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-B1zAefzz.mjs → ProxyViewerContainer-CyFKksQa.mjs} +75 -32
- package/.output/server/_ssr/{ReplayDialog-DleMFvMx.mjs → ReplayDialog-BpQHN7O2.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-B-P9tC3D.mjs → RequestAnatomy-CuhmKAma.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-DWfGn0i3.mjs → ResponseView-CO6LyBmj.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-C8kFF6xJ.mjs → StreamingChunkSequence-DLeVetNR.mjs} +2 -2
- package/.output/server/_ssr/{index-DzENi2RO.mjs → index-DWUKl9Bf.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-Dy1VEjxy.mjs → router-BrK_DcTS.mjs} +337 -86
- package/.output/server/{_tanstack-start-manifest_v-CyzuAD8A.mjs → _tanstack-start-manifest_v-BjW2XeWE.mjs} +1 -1
- package/.output/server/index.mjs +57 -57
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +11 -1
- package/src/components/ProxyViewerContainer.tsx +21 -15
- package/src/lib/export-logs.ts +54 -11
- package/src/lib/providerTestPrompt.ts +4 -4
- 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 +275 -135
- package/src/routes/api/logs.stream.ts +6 -3
- package/src/routes/api/logs.ts +4 -1
- package/.output/public/assets/ProxyViewerContainer-Dwhi4Q1c.js +0 -115
- package/.output/public/assets/_sessionId-b1KDnByv.js +0 -1
- package/.output/public/assets/index-CUyRKgII.js +0 -1
package/.output/server/index.mjs
CHANGED
|
@@ -38,100 +38,100 @@ const assets = {
|
|
|
38
38
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
39
39
|
"type": "image/svg+xml",
|
|
40
40
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
41
|
-
"mtime": "2026-06-
|
|
41
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
42
42
|
"size": 5915,
|
|
43
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
44
|
},
|
|
45
|
-
"/assets/
|
|
45
|
+
"/assets/CompareDrawer-DrY-gAzy.js": {
|
|
46
46
|
"type": "text/javascript; charset=utf-8",
|
|
47
|
-
"etag": '"
|
|
48
|
-
"mtime": "2026-06-
|
|
47
|
+
"etag": '"49fe-MpHiIJshHfw4LFDNCsCMDZDrWGg"',
|
|
48
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
49
|
+
"size": 18942,
|
|
50
|
+
"path": "../public/assets/CompareDrawer-DrY-gAzy.js"
|
|
51
|
+
},
|
|
52
|
+
"/assets/index-CViEXkqa.js": {
|
|
53
|
+
"type": "text/javascript; charset=utf-8",
|
|
54
|
+
"etag": '"74-U7AFlYRRMX3IzoDVXXko4xBPBvM"',
|
|
55
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
49
56
|
"size": 116,
|
|
50
|
-
"path": "../public/assets/index-
|
|
57
|
+
"path": "../public/assets/index-CViEXkqa.js"
|
|
51
58
|
},
|
|
52
|
-
"/assets/
|
|
59
|
+
"/assets/ReplayDialog-CFyw6Vu4.js": {
|
|
53
60
|
"type": "text/javascript; charset=utf-8",
|
|
54
|
-
"etag": '"
|
|
55
|
-
"mtime": "2026-06-
|
|
56
|
-
"size":
|
|
57
|
-
"path": "../public/assets/
|
|
61
|
+
"etag": '"318c-yVT324FTJnGgK/IiiYVI1v6/IEw"',
|
|
62
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
63
|
+
"size": 12684,
|
|
64
|
+
"path": "../public/assets/ReplayDialog-CFyw6Vu4.js"
|
|
65
|
+
},
|
|
66
|
+
"/assets/RequestAnatomy-DHGJ9py7.js": {
|
|
67
|
+
"type": "text/javascript; charset=utf-8",
|
|
68
|
+
"etag": '"4485-2C+vaW+lYZaQ0wLkm/KTOsK3AOY"',
|
|
69
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
70
|
+
"size": 17541,
|
|
71
|
+
"path": "../public/assets/RequestAnatomy-DHGJ9py7.js"
|
|
58
72
|
},
|
|
59
73
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
60
74
|
"type": "image/jpeg",
|
|
61
75
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
62
|
-
"mtime": "2026-06-
|
|
76
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
63
77
|
"size": 6918,
|
|
64
78
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
65
79
|
},
|
|
66
|
-
"/assets/
|
|
67
|
-
"type": "text/
|
|
68
|
-
"etag": '"
|
|
69
|
-
"mtime": "2026-06-
|
|
70
|
-
"size":
|
|
71
|
-
"path": "../public/assets/
|
|
72
|
-
},
|
|
73
|
-
"/assets/main-N-sjHJUM.js": {
|
|
74
|
-
"type": "text/javascript; charset=utf-8",
|
|
75
|
-
"etag": '"51726-Oiv87HUBZCp4NUFhL1cAZMdsJk4"',
|
|
76
|
-
"mtime": "2026-06-29T10:13:59.057Z",
|
|
77
|
-
"size": 333606,
|
|
78
|
-
"path": "../public/assets/main-N-sjHJUM.js"
|
|
79
|
-
},
|
|
80
|
-
"/assets/RequestAnatomy-CYbosGYy.js": {
|
|
81
|
-
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"4485-A1lqcM9CzkgAhkltbvT6xUyCtnk"',
|
|
83
|
-
"mtime": "2026-06-29T10:13:59.057Z",
|
|
84
|
-
"size": 17541,
|
|
85
|
-
"path": "../public/assets/RequestAnatomy-CYbosGYy.js"
|
|
80
|
+
"/assets/index-780zTVwp.css": {
|
|
81
|
+
"type": "text/css; charset=utf-8",
|
|
82
|
+
"etag": '"1a380-Lz+Shs13XyKSlh9PTaGE50w/BpU"',
|
|
83
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
84
|
+
"size": 107392,
|
|
85
|
+
"path": "../public/assets/index-780zTVwp.css"
|
|
86
86
|
},
|
|
87
|
-
"/assets/ResponseView-
|
|
87
|
+
"/assets/ResponseView-CB_3yc08.js": {
|
|
88
88
|
"type": "text/javascript; charset=utf-8",
|
|
89
|
-
"etag": '"30d0-
|
|
90
|
-
"mtime": "2026-06-
|
|
89
|
+
"etag": '"30d0-M6AktqEyUeyPg4kcv2fzak6vFlw"',
|
|
90
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
91
91
|
"size": 12496,
|
|
92
|
-
"path": "../public/assets/ResponseView-
|
|
92
|
+
"path": "../public/assets/ResponseView-CB_3yc08.js"
|
|
93
93
|
},
|
|
94
|
-
"/assets/StreamingChunkSequence-
|
|
94
|
+
"/assets/StreamingChunkSequence-CqhHwgDn.js": {
|
|
95
95
|
"type": "text/javascript; charset=utf-8",
|
|
96
|
-
"etag": '"d57-
|
|
97
|
-
"mtime": "2026-06-
|
|
96
|
+
"etag": '"d57-C9p9lFGRMu7L/bxJPes/RpOKks0"',
|
|
97
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
98
98
|
"size": 3415,
|
|
99
|
-
"path": "../public/assets/StreamingChunkSequence-
|
|
99
|
+
"path": "../public/assets/StreamingChunkSequence-CqhHwgDn.js"
|
|
100
100
|
},
|
|
101
|
-
"/assets/
|
|
102
|
-
"type": "text/css; charset=utf-8",
|
|
103
|
-
"etag": '"1a380-Lz+Shs13XyKSlh9PTaGE50w/BpU"',
|
|
104
|
-
"mtime": "2026-06-29T10:13:59.057Z",
|
|
105
|
-
"size": 107392,
|
|
106
|
-
"path": "../public/assets/index-780zTVwp.css"
|
|
107
|
-
},
|
|
108
|
-
"/assets/_sessionId-b1KDnByv.js": {
|
|
101
|
+
"/assets/_sessionId-CgG9CQ9r.js": {
|
|
109
102
|
"type": "text/javascript; charset=utf-8",
|
|
110
|
-
"etag": '"d2-
|
|
111
|
-
"mtime": "2026-06-
|
|
103
|
+
"etag": '"d2-QqZWnWujfEY0vnXMOs1NyoEJukI"',
|
|
104
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
112
105
|
"size": 210,
|
|
113
|
-
"path": "../public/assets/_sessionId-
|
|
106
|
+
"path": "../public/assets/_sessionId-CgG9CQ9r.js"
|
|
114
107
|
},
|
|
115
108
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
116
109
|
"type": "image/svg+xml",
|
|
117
110
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
118
|
-
"mtime": "2026-06-
|
|
111
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
119
112
|
"size": 11256,
|
|
120
113
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
121
114
|
},
|
|
115
|
+
"/assets/main-CVw7-JyE.js": {
|
|
116
|
+
"type": "text/javascript; charset=utf-8",
|
|
117
|
+
"etag": '"51726-W+9s8t7uIYMEgBdKzOagDvVRdKc"',
|
|
118
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
119
|
+
"size": 333606,
|
|
120
|
+
"path": "../public/assets/main-CVw7-JyE.js"
|
|
121
|
+
},
|
|
122
122
|
"/assets/qwen-CONDcHqt.png": {
|
|
123
123
|
"type": "image/png",
|
|
124
124
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
125
|
-
"mtime": "2026-06-
|
|
125
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
126
126
|
"size": 357059,
|
|
127
127
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
128
128
|
},
|
|
129
|
-
"/assets/ProxyViewerContainer-
|
|
129
|
+
"/assets/ProxyViewerContainer-CiLvRNie.js": {
|
|
130
130
|
"type": "text/javascript; charset=utf-8",
|
|
131
|
-
"etag": '"
|
|
132
|
-
"mtime": "2026-06-
|
|
133
|
-
"size":
|
|
134
|
-
"path": "../public/assets/ProxyViewerContainer-
|
|
131
|
+
"etag": '"ae3c0-Cpzx6KeaKuGDUgS/cMjRV9y+6zQ"',
|
|
132
|
+
"mtime": "2026-06-29T12:29:42.542Z",
|
|
133
|
+
"size": 713664,
|
|
134
|
+
"path": "../public/assets/ProxyViewerContainer-CiLvRNie.js"
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -637,6 +637,7 @@ export function ProxyViewer({
|
|
|
637
637
|
const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
|
|
638
638
|
const { providers } = useProviders();
|
|
639
639
|
const [exporting, setExporting] = useState<ExportMode | null>(null);
|
|
640
|
+
const [exportError, setExportError] = useState<string | null>(null);
|
|
640
641
|
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
641
642
|
const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
|
|
642
643
|
"hidden",
|
|
@@ -669,8 +670,12 @@ export function ProxyViewer({
|
|
|
669
670
|
const handleExport = useCallback(
|
|
670
671
|
async (mode: ExportMode) => {
|
|
671
672
|
setExporting(mode);
|
|
673
|
+
setExportError(null);
|
|
672
674
|
try {
|
|
673
|
-
await exportLogsAsZip(logs, mode);
|
|
675
|
+
const result = await exportLogsAsZip(logs, mode);
|
|
676
|
+
if (!result.ok) {
|
|
677
|
+
setExportError(result.message);
|
|
678
|
+
}
|
|
674
679
|
} finally {
|
|
675
680
|
setExporting(null);
|
|
676
681
|
}
|
|
@@ -908,6 +913,11 @@ export function ProxyViewer({
|
|
|
908
913
|
Clear
|
|
909
914
|
</button>
|
|
910
915
|
</div>
|
|
916
|
+
{exportError !== null && (
|
|
917
|
+
<div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
918
|
+
{exportError}
|
|
919
|
+
</div>
|
|
920
|
+
)}
|
|
911
921
|
</div>
|
|
912
922
|
|
|
913
923
|
{/* Log list */}
|
|
@@ -64,6 +64,7 @@ function filterLogs(
|
|
|
64
64
|
const DEBOUNCE_MS = 50;
|
|
65
65
|
const HASH_SCROLL_ATTEMPTS = 12;
|
|
66
66
|
const HASH_HIGHLIGHT_MS = 1800;
|
|
67
|
+
const MAX_CLIENT_LOGS = 500;
|
|
67
68
|
|
|
68
69
|
function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
69
70
|
if (sessionId === undefined) return "/api/logs/stream";
|
|
@@ -71,6 +72,20 @@ function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
|
71
72
|
return `/api/logs/stream?${params.toString()}`;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
function buildLogIndex(logs: readonly CapturedLog[]): Map<number, number> {
|
|
76
|
+
const idx = new Map<number, number>();
|
|
77
|
+
for (let i = 0; i < logs.length; i++) {
|
|
78
|
+
const log = logs[i];
|
|
79
|
+
if (log !== undefined) idx.set(log.id, i);
|
|
80
|
+
}
|
|
81
|
+
return idx;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function trimClientLogs(logs: CapturedLog[]): CapturedLog[] {
|
|
85
|
+
if (logs.length <= MAX_CLIENT_LOGS) return logs;
|
|
86
|
+
return logs.slice(logs.length - MAX_CLIENT_LOGS);
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
export function ProxyViewerContainer({
|
|
75
90
|
initialSessionId,
|
|
76
91
|
}: {
|
|
@@ -132,6 +147,8 @@ export function ProxyViewerContainer({
|
|
|
132
147
|
next = [...next, log];
|
|
133
148
|
}
|
|
134
149
|
}
|
|
150
|
+
next = trimClientLogs(next);
|
|
151
|
+
logIndexRef.current = buildLogIndex(next);
|
|
135
152
|
return next;
|
|
136
153
|
});
|
|
137
154
|
}, []);
|
|
@@ -173,14 +190,9 @@ export function ProxyViewerContainer({
|
|
|
173
190
|
}
|
|
174
191
|
pendingUpdatesRef.current = [];
|
|
175
192
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const log = update.logs[i];
|
|
180
|
-
if (log !== undefined) idx.set(log.id, i);
|
|
181
|
-
}
|
|
182
|
-
logIndexRef.current = idx;
|
|
183
|
-
setAllLogs(update.logs);
|
|
193
|
+
const nextLogs = trimClientLogs(update.logs);
|
|
194
|
+
logIndexRef.current = buildLogIndex(nextLogs);
|
|
195
|
+
setAllLogs(nextLogs);
|
|
184
196
|
setError(null);
|
|
185
197
|
} else if (update.type === "update") {
|
|
186
198
|
scheduleUpdate(update.log);
|
|
@@ -317,13 +329,7 @@ export function ProxyViewerContainer({
|
|
|
317
329
|
const idSet = new Set(ids);
|
|
318
330
|
setAllLogs((prev) => {
|
|
319
331
|
const remaining = prev.filter((l) => !idSet.has(l.id));
|
|
320
|
-
|
|
321
|
-
const idx = new Map<number, number>();
|
|
322
|
-
for (let i = 0; i < remaining.length; i++) {
|
|
323
|
-
const log = remaining[i];
|
|
324
|
-
if (log !== undefined) idx.set(log.id, i);
|
|
325
|
-
}
|
|
326
|
-
logIndexRef.current = idx;
|
|
332
|
+
logIndexRef.current = buildLogIndex(remaining);
|
|
327
333
|
return remaining;
|
|
328
334
|
});
|
|
329
335
|
setError(null);
|
package/src/lib/export-logs.ts
CHANGED
|
@@ -2,6 +2,12 @@ import JSZip from "jszip";
|
|
|
2
2
|
import type { CapturedLog } from "../proxy/schemas";
|
|
3
3
|
|
|
4
4
|
export type ExportMode = "redacted" | "raw";
|
|
5
|
+
export type ExportLogsResult =
|
|
6
|
+
| { ok: true }
|
|
7
|
+
| {
|
|
8
|
+
ok: false;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
5
11
|
|
|
6
12
|
const REDACTED = "[REDACTED]";
|
|
7
13
|
const SECRET_KEY_PATTERN =
|
|
@@ -13,6 +19,39 @@ const SECRET_TEXT_PATTERNS: readonly RegExp[] = [
|
|
|
13
19
|
/\bghp_[A-Za-z0-9_]{12,}\b/g,
|
|
14
20
|
/\bglpat-[A-Za-z0-9_-]{12,}\b/g,
|
|
15
21
|
];
|
|
22
|
+
const MAX_EXPORT_LOGS = 200;
|
|
23
|
+
const MAX_EXPORT_TEXT_CHARS = 25_000_000;
|
|
24
|
+
const MAX_STREAMING_CHUNK_EXPORTS = 50;
|
|
25
|
+
|
|
26
|
+
function formatApproxSize(chars: number): string {
|
|
27
|
+
if (chars < 1024) return `${chars} chars`;
|
|
28
|
+
if (chars < 1024 * 1024) return `${Math.ceil(chars / 1024)} KB`;
|
|
29
|
+
return `${Math.ceil(chars / (1024 * 1024))} MB`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function estimateExportTextChars(logs: readonly CapturedLog[]): number {
|
|
33
|
+
let total = 0;
|
|
34
|
+
for (const log of logs) {
|
|
35
|
+
total += log.rawRequestBody?.length ?? 0;
|
|
36
|
+
total += log.responseText?.length ?? 0;
|
|
37
|
+
}
|
|
38
|
+
return total;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function exportLimitMessage(logs: readonly CapturedLog[]): string | null {
|
|
42
|
+
if (logs.length > MAX_EXPORT_LOGS) {
|
|
43
|
+
return `Export is limited to ${MAX_EXPORT_LOGS} logs at once. Narrow the filter and try again.`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const estimatedChars = estimateExportTextChars(logs);
|
|
47
|
+
if (estimatedChars > MAX_EXPORT_TEXT_CHARS) {
|
|
48
|
+
return `Export is too large (${formatApproxSize(
|
|
49
|
+
estimatedChars,
|
|
50
|
+
)} of text). Narrow the filter and try again.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
16
55
|
|
|
17
56
|
async function fetchStreamingChunks(logId: number): Promise<string | null> {
|
|
18
57
|
try {
|
|
@@ -64,9 +103,13 @@ function exportText(text: string, mode: ExportMode): string {
|
|
|
64
103
|
export async function exportLogsAsZip(
|
|
65
104
|
logs: CapturedLog[],
|
|
66
105
|
mode: ExportMode = "redacted",
|
|
67
|
-
): Promise<
|
|
106
|
+
): Promise<ExportLogsResult> {
|
|
107
|
+
const limitMessage = exportLimitMessage(logs);
|
|
108
|
+
if (limitMessage !== null) return { ok: false, message: limitMessage };
|
|
109
|
+
|
|
68
110
|
const zip = new JSZip();
|
|
69
111
|
const suffix = mode === "redacted" ? "redacted" : "raw";
|
|
112
|
+
const streamingLogs = logs.filter((log) => log.streaming).slice(0, MAX_STREAMING_CHUNK_EXPORTS);
|
|
70
113
|
zip.file(
|
|
71
114
|
"manifest.json",
|
|
72
115
|
JSON.stringify(
|
|
@@ -76,6 +119,8 @@ export async function exportLogsAsZip(
|
|
|
76
119
|
redacted: mode === "redacted",
|
|
77
120
|
logCount: logs.length,
|
|
78
121
|
logIds: logs.map((log) => log.id),
|
|
122
|
+
streamingChunkExportLimit: MAX_STREAMING_CHUNK_EXPORTS,
|
|
123
|
+
streamingChunkLogIds: streamingLogs.map((log) => log.id),
|
|
79
124
|
},
|
|
80
125
|
null,
|
|
81
126
|
2,
|
|
@@ -92,16 +137,13 @@ export async function exportLogsAsZip(
|
|
|
92
137
|
}
|
|
93
138
|
}
|
|
94
139
|
|
|
95
|
-
// Fetch SSE chunks
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
}),
|
|
104
|
-
);
|
|
140
|
+
// Fetch SSE chunks sequentially so exports do not spike browser memory.
|
|
141
|
+
for (const log of streamingLogs) {
|
|
142
|
+
const chunks = await fetchStreamingChunks(log.id);
|
|
143
|
+
if (chunks !== null) {
|
|
144
|
+
zip.file(`#${log.id}.SSE.Response.json`, exportText(chunks, mode));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
105
147
|
|
|
106
148
|
// Generate and download ZIP
|
|
107
149
|
const blob = await zip.generateAsync({ type: "blob" });
|
|
@@ -115,4 +157,5 @@ export async function exportLogsAsZip(
|
|
|
115
157
|
document.body.removeChild(anchor);
|
|
116
158
|
|
|
117
159
|
URL.revokeObjectURL(url);
|
|
160
|
+
return { ok: true };
|
|
118
161
|
}
|
|
@@ -9,7 +9,7 @@ export type ProviderTestRequestBody = {
|
|
|
9
9
|
model: string;
|
|
10
10
|
messages: ProviderTestMessage[];
|
|
11
11
|
max_tokens: number;
|
|
12
|
-
stream
|
|
12
|
+
stream: boolean;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const MEMORY_PROBE_FACTS = [
|
|
@@ -32,9 +32,9 @@ function modeInstruction(mode: ProviderTestMode): string {
|
|
|
32
32
|
function maxTokensForMode(mode: ProviderTestMode): number {
|
|
33
33
|
switch (mode) {
|
|
34
34
|
case "non-streaming":
|
|
35
|
-
return
|
|
35
|
+
return 256;
|
|
36
36
|
case "streaming":
|
|
37
|
-
return
|
|
37
|
+
return 256;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -71,7 +71,7 @@ export function buildProviderTestRequestBody(
|
|
|
71
71
|
};
|
|
72
72
|
switch (mode) {
|
|
73
73
|
case "non-streaming":
|
|
74
|
-
return base;
|
|
74
|
+
return { ...base, stream: false };
|
|
75
75
|
case "streaming":
|
|
76
76
|
return { ...base, stream: true };
|
|
77
77
|
}
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
* `isError: true` results from inside the impl.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { Buffer } from "node:buffer";
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
import { LoopbackTimeoutError, type CallApiOptions } from "./loopback";
|
|
18
19
|
import { extractLastUserMessagePreview, extractResponsePreview } from "./previewExtractor";
|
|
19
20
|
import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
|
|
20
21
|
|
|
21
|
-
/** Minimal callApi contract
|
|
22
|
+
/** Minimal callApi contract: same shape as the real `callApi` in loopback.ts. */
|
|
22
23
|
export type CallApiFn = (path: string, options?: CallApiOptions) => Promise<Response>;
|
|
23
24
|
|
|
24
25
|
export type ToolResult = {
|
|
@@ -26,7 +27,7 @@ export type ToolResult = {
|
|
|
26
27
|
isError?: true;
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
// Loose shape returned by `GET /api/logs
|
|
30
|
+
// Loose shape returned by `GET /api/logs`: `logs: CapturedLog[]` and a few
|
|
30
31
|
// pagination fields. The CapturedLog items are fully validated downstream.
|
|
31
32
|
const LogsListResponseSchema = z.object({
|
|
32
33
|
logs: z.array(CapturedLogSchema).optional(),
|
|
@@ -37,6 +38,7 @@ const LogsListResponseSchema = z.object({
|
|
|
37
38
|
|
|
38
39
|
const PAGINATION_DEFAULTS = { offset: 0, limit: 3 };
|
|
39
40
|
export const LIMIT_HARD_CAP = 5;
|
|
41
|
+
const LOG_DETAIL_TEXT_LIMIT = 200_000;
|
|
40
42
|
|
|
41
43
|
export function clampLimit(input: number | undefined): number {
|
|
42
44
|
if (input === undefined) return PAGINATION_DEFAULTS.limit;
|
|
@@ -52,10 +54,55 @@ function toolError(message: string): ToolResult {
|
|
|
52
54
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function textByteLength(value: string | null): number | null {
|
|
58
|
+
if (value === null) return null;
|
|
59
|
+
return Buffer.byteLength(value, "utf8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasBodyText(value: string | null, byteLength: number | null | undefined): boolean {
|
|
63
|
+
if (value !== null && value.length > 0) return true;
|
|
64
|
+
return byteLength !== undefined && byteLength !== null && byteLength > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function truncateToolText(value: string | null): {
|
|
68
|
+
value: string | null;
|
|
69
|
+
byteLength: number | null;
|
|
70
|
+
truncated: boolean;
|
|
71
|
+
} {
|
|
72
|
+
if (value === null) return { value: null, byteLength: null, truncated: false };
|
|
73
|
+
const byteLength = textByteLength(value);
|
|
74
|
+
if (value.length <= LOG_DETAIL_TEXT_LIMIT) {
|
|
75
|
+
return { value, byteLength, truncated: false };
|
|
76
|
+
}
|
|
77
|
+
const suffix = `\n[truncated by agent-inspector MCP: original ${byteLength ?? 0} bytes]`;
|
|
78
|
+
return {
|
|
79
|
+
value: `${value.slice(0, LOG_DETAIL_TEXT_LIMIT)}${suffix}`,
|
|
80
|
+
byteLength,
|
|
81
|
+
truncated: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildBoundedLogDetail(log: CapturedLog): Record<string, unknown> {
|
|
86
|
+
const { streamingChunks: _chunks, rawRequestBody, responseText, ...rest } = log;
|
|
87
|
+
const boundedRequest = truncateToolText(rawRequestBody);
|
|
88
|
+
const boundedResponse = truncateToolText(responseText);
|
|
89
|
+
return {
|
|
90
|
+
...rest,
|
|
91
|
+
rawRequestBody: boundedRequest.value,
|
|
92
|
+
responseText: boundedResponse.value,
|
|
93
|
+
rawRequestBodyBytes: boundedRequest.byteLength,
|
|
94
|
+
responseTextBytes: boundedResponse.byteLength,
|
|
95
|
+
rawRequestBodyTruncated: boundedRequest.truncated,
|
|
96
|
+
responseTextTruncated: boundedResponse.truncated,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
export function buildLogSummary(log: CapturedLog) {
|
|
56
101
|
const hasError =
|
|
57
102
|
(log.error !== null && log.error !== undefined && log.error.length > 0) ||
|
|
58
103
|
(log.responseStatus !== null && log.responseStatus !== undefined && log.responseStatus >= 400);
|
|
104
|
+
const rawRequestBodyBytes = log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody);
|
|
105
|
+
const responseTextBytes = log.responseTextBytes ?? textByteLength(log.responseText);
|
|
59
106
|
return {
|
|
60
107
|
id: log.id,
|
|
61
108
|
timestamp: log.timestamp,
|
|
@@ -81,7 +128,9 @@ export function buildLogSummary(log: CapturedLog) {
|
|
|
81
128
|
hasChunks:
|
|
82
129
|
(log.streamingChunks !== undefined && log.streamingChunks !== null) ||
|
|
83
130
|
(log.streamingChunksPath !== null && log.streamingChunksPath !== undefined),
|
|
84
|
-
hasRawRequestBody: log.rawRequestBody
|
|
131
|
+
hasRawRequestBody: hasBodyText(log.rawRequestBody, rawRequestBodyBytes),
|
|
132
|
+
rawRequestBodyBytes,
|
|
133
|
+
responseTextBytes,
|
|
85
134
|
};
|
|
86
135
|
}
|
|
87
136
|
|
|
@@ -99,7 +148,11 @@ export type ListLogsArgs = {
|
|
|
99
148
|
export async function listLogsImpl(callApi: CallApiFn, args: ListLogsArgs): Promise<ToolResult> {
|
|
100
149
|
const offset = args.offset ?? PAGINATION_DEFAULTS.offset;
|
|
101
150
|
const limit = clampLimit(args.limit);
|
|
102
|
-
const params = new URLSearchParams({
|
|
151
|
+
const params = new URLSearchParams({
|
|
152
|
+
offset: String(offset),
|
|
153
|
+
limit: String(limit),
|
|
154
|
+
compact: "1",
|
|
155
|
+
});
|
|
103
156
|
if (args.sessionId !== undefined && args.sessionId.length > 0) {
|
|
104
157
|
params.set("sessionId", args.sessionId);
|
|
105
158
|
}
|
|
@@ -123,10 +176,7 @@ export async function getLogImpl(callApi: CallApiFn, id: number): Promise<ToolRe
|
|
|
123
176
|
if (!parsed.success) {
|
|
124
177
|
return toolError(`GET /api/logs/${id} returned an unparseable CapturedLog`);
|
|
125
178
|
}
|
|
126
|
-
|
|
127
|
-
// explicitly via inspector_get_log_chunks.
|
|
128
|
-
const { streamingChunks: _chunks, ...rest } = parsed.data;
|
|
129
|
-
return textJson(rest);
|
|
179
|
+
return textJson(buildBoundedLogDetail(parsed.data));
|
|
130
180
|
}
|
|
131
181
|
|
|
132
182
|
export async function getLogChunksImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
|
|
@@ -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
|
});
|