@tonyclaw/agent-inspector 2.1.16 → 2.1.18
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/cli.js +57 -13
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-Ccrrcx1j.js → CompareDrawer-Fqqw5Lci.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CP_RzwW9.js +106 -0
- package/.output/public/assets/{ReplayDialog-C7axhr-l.js → ReplayDialog-hYhz_i6W.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-WOptg8j_.js → RequestAnatomy-CTuWbtwC.js} +1 -1
- package/.output/public/assets/{ResponseView-CSAcxh_M.js → ResponseView-CsZ7S5Gv.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-DJS5KhPx.js → StreamingChunkSequence-PIbJMotU.js} +1 -1
- package/.output/public/assets/_sessionId-CUGA9qU3.js +1 -0
- package/.output/public/assets/{index-DMPNh46t.js → index-F7mynwuH.js} +1 -1
- package/.output/public/assets/index-G4dTUobw.js +1 -0
- package/.output/public/assets/index-gvYkgMPF.css +1 -0
- package/.output/public/assets/{json-viewer-inX5QSa3.js → json-viewer-DCXtNmmD.js} +1 -1
- package/.output/public/assets/{main-aCAKYGSD.js → main-BJZf7_Ib.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +220 -202
- package/.output/server/{_sessionId-C0fhvQnn.mjs → _sessionId-DjSHDNh8.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CO-Ti5sg.mjs → CompareDrawer-Dal52IFo.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-N4J7uBCX.mjs → ProxyViewerContainer-CVqFZGff.mjs} +1833 -1366
- package/.output/server/_ssr/{ReplayDialog-J1VxC0In.mjs → ReplayDialog-BOEgdMes.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-DBH-F3rq.mjs → RequestAnatomy-gch7_vCV.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-DLFwV7Hd.mjs → ResponseView-lEWPy-H4.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-BywmuND0.mjs → StreamingChunkSequence-DnudbJ1O.mjs} +2 -2
- package/.output/server/_ssr/{index-CStymIc0.mjs → index-Cza8zDrW.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-DlQu0N1x.mjs → json-viewer-C2lIVEX3.mjs} +3 -3
- package/.output/server/_ssr/{router-CQ4Zxhmr.mjs → router-bF9uzd1k.mjs} +939 -113
- package/.output/server/{_tanstack-start-manifest_v-BHeJBGfo.mjs → _tanstack-start-manifest_v-ErvW-mtr.mjs} +1 -1
- package/.output/server/index.mjs +74 -74
- package/README.md +57 -2
- package/package.json +1 -1
- package/src/cli/startupOutput.ts +16 -0
- package/src/cli.ts +61 -15
- package/src/components/ProxyViewer.tsx +395 -64
- package/src/components/providers/ProviderCard.tsx +132 -31
- package/src/components/providers/ProviderForm.tsx +43 -11
- package/src/components/providers/ProvidersPanel.tsx +1 -1
- package/src/components/proxy-viewer/ConversationHeader.tsx +133 -112
- package/src/components/proxy-viewer/LogEntry.tsx +109 -26
- package/src/components/proxy-viewer/LogEntryHeader.tsx +339 -329
- package/src/components/proxy-viewer/ThreadConnector.tsx +49 -46
- package/src/components/proxy-viewer/TurnGroup.tsx +22 -62
- package/src/components/proxy-viewer/bodyHydration.ts +56 -0
- package/src/lib/providerTestContract.ts +56 -10
- package/src/lib/providerTestPrompt.ts +23 -1
- package/src/lib/upstreamUrl.ts +105 -1
- package/src/mcp/server.ts +275 -0
- package/src/mcp/toolHandlers.ts +381 -4
- package/src/proxy/logSearch.ts +3 -0
- package/src/proxy/store.ts +40 -15
- package/src/routes/api/logs.ts +8 -2
- package/src/routes/api/providers.$providerId.test.log.ts +85 -10
- package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +0 -106
- package/.output/public/assets/_sessionId-DUSalzKH.js +0 -1
- package/.output/public/assets/index-BPpA21dY.css +0 -1
- package/.output/public/assets/index-Bt0Az2I2.js +0 -1
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -85,6 +85,8 @@ const SearchLogsResponseSchema = z.object({
|
|
|
85
85
|
const PAGINATION_DEFAULTS = { offset: 0, limit: 3 };
|
|
86
86
|
export const LIMIT_HARD_CAP = 5;
|
|
87
87
|
const LOG_DETAIL_TEXT_LIMIT = 200_000;
|
|
88
|
+
const PROCESS_LOG_LIMIT_DEFAULT = 50;
|
|
89
|
+
const PROCESS_LOG_LIMIT_MAX = 200;
|
|
88
90
|
|
|
89
91
|
export function clampLimit(input: number | undefined): number {
|
|
90
92
|
if (input === undefined) return PAGINATION_DEFAULTS.limit;
|
|
@@ -92,6 +94,12 @@ export function clampLimit(input: number | undefined): number {
|
|
|
92
94
|
return Math.min(Math.floor(input), LIMIT_HARD_CAP);
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
function clampProcessLogLimit(input: number | undefined): number {
|
|
98
|
+
if (input === undefined) return PROCESS_LOG_LIMIT_DEFAULT;
|
|
99
|
+
if (!Number.isFinite(input) || input < 1) return 1;
|
|
100
|
+
return Math.min(Math.floor(input), PROCESS_LOG_LIMIT_MAX);
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
function textJson(data: unknown): ToolResult {
|
|
96
104
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
97
105
|
}
|
|
@@ -147,10 +155,15 @@ function buildBoundedLogDetail(log: CapturedLog): Record<string, unknown> {
|
|
|
147
155
|
};
|
|
148
156
|
}
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
function logHasError(log: CapturedLog): boolean {
|
|
159
|
+
return (
|
|
152
160
|
(log.error !== null && log.error !== undefined && log.error.length > 0) ||
|
|
153
|
-
(log.responseStatus !== null && log.responseStatus !== undefined && log.responseStatus >= 400)
|
|
161
|
+
(log.responseStatus !== null && log.responseStatus !== undefined && log.responseStatus >= 400)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function buildLogSummary(log: CapturedLog) {
|
|
166
|
+
const hasError = logHasError(log);
|
|
154
167
|
const rawRequestBodyBytes = log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody);
|
|
155
168
|
const responseTextBytes = log.responseTextBytes ?? textByteLength(log.responseText);
|
|
156
169
|
return {
|
|
@@ -190,6 +203,92 @@ export function buildLogSummary(log: CapturedLog) {
|
|
|
190
203
|
};
|
|
191
204
|
}
|
|
192
205
|
|
|
206
|
+
function sortedUniqueStrings(values: Array<string | null | undefined>): string[] {
|
|
207
|
+
return [
|
|
208
|
+
...new Set(
|
|
209
|
+
values.filter(
|
|
210
|
+
(value): value is string => value !== null && value !== undefined && value !== "",
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
].sort((left, right) => left.localeCompare(right));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sortedUniqueNumbers(values: Array<number | null | undefined>): number[] {
|
|
217
|
+
return [
|
|
218
|
+
...new Set(
|
|
219
|
+
values.filter(
|
|
220
|
+
(value): value is number => value !== null && value !== undefined && Number.isFinite(value),
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
].sort((left, right) => left - right);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function inferAgentFromUserAgent(userAgent: string | null | undefined): string | null {
|
|
227
|
+
if (userAgent === null || userAgent === undefined) return null;
|
|
228
|
+
const lower = userAgent.toLowerCase();
|
|
229
|
+
if (lower.includes("codex")) return "Codex";
|
|
230
|
+
if (lower.includes("opencode")) return "OpenCode";
|
|
231
|
+
if (lower.includes("claude")) return "Claude Code";
|
|
232
|
+
if (lower.includes("mimocode")) return "MiMoCode";
|
|
233
|
+
if (lower.includes("ai-sdk")) return "AI SDK";
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function processSummary(
|
|
238
|
+
clientPid: number,
|
|
239
|
+
logs: readonly CapturedLog[],
|
|
240
|
+
total: number,
|
|
241
|
+
limit: number,
|
|
242
|
+
): Record<string, unknown> {
|
|
243
|
+
const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
|
|
244
|
+
const first = sortedLogs[0];
|
|
245
|
+
const last = sortedLogs[sortedLogs.length - 1];
|
|
246
|
+
const userAgents = sortedUniqueStrings(sortedLogs.map((log) => log.userAgent));
|
|
247
|
+
const agents = sortedUniqueStrings(userAgents.map(inferAgentFromUserAgent));
|
|
248
|
+
const sessions = sortedUniqueStrings(sortedLogs.map((log) => log.sessionId));
|
|
249
|
+
return {
|
|
250
|
+
clientPid,
|
|
251
|
+
total,
|
|
252
|
+
limit,
|
|
253
|
+
logCount: sortedLogs.length,
|
|
254
|
+
sessionCount: sessions.length,
|
|
255
|
+
sessions,
|
|
256
|
+
agents,
|
|
257
|
+
userAgents,
|
|
258
|
+
clientPorts: sortedUniqueNumbers(sortedLogs.map((log) => log.clientPort)),
|
|
259
|
+
clientCwds: sortedUniqueStrings(sortedLogs.map((log) => log.clientCwd)),
|
|
260
|
+
clientProjectFolders: sortedUniqueStrings(sortedLogs.map((log) => log.clientProjectFolder)),
|
|
261
|
+
models: sortedUniqueStrings(sortedLogs.map((log) => log.model)),
|
|
262
|
+
providers: sortedUniqueStrings(sortedLogs.map((log) => log.providerName)),
|
|
263
|
+
errorCount: sortedLogs.filter(logHasError).length,
|
|
264
|
+
startedAt: first?.timestamp ?? null,
|
|
265
|
+
updatedAt: last?.timestamp ?? null,
|
|
266
|
+
latestLogs: sortedLogs.slice(-Math.min(10, sortedLogs.length)).reverse().map(buildLogSummary),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function readCompactLogsPage(
|
|
271
|
+
callApi: CallApiFn,
|
|
272
|
+
params: URLSearchParams,
|
|
273
|
+
): Promise<{ logs: CapturedLog[]; total: number } | ToolResult> {
|
|
274
|
+
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");
|
|
280
|
+
const logs = parsed.data.logs ?? [];
|
|
281
|
+
return { logs, total: parsed.data.total ?? logs.length };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isToolResult(value: unknown): value is ToolResult {
|
|
285
|
+
return (
|
|
286
|
+
typeof value === "object" &&
|
|
287
|
+
value !== null &&
|
|
288
|
+
Object.getOwnPropertyDescriptor(value, "content") !== undefined
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
193
292
|
// ===========================================================================
|
|
194
293
|
// Per-tool impls
|
|
195
294
|
// ===========================================================================
|
|
@@ -199,6 +298,7 @@ export type ListLogsArgs = {
|
|
|
199
298
|
limit?: number;
|
|
200
299
|
sessionId?: string;
|
|
201
300
|
model?: string;
|
|
301
|
+
clientPid?: number;
|
|
202
302
|
};
|
|
203
303
|
|
|
204
304
|
export async function listLogsImpl(callApi: CallApiFn, args: ListLogsArgs): Promise<ToolResult> {
|
|
@@ -215,6 +315,9 @@ export async function listLogsImpl(callApi: CallApiFn, args: ListLogsArgs): Prom
|
|
|
215
315
|
if (args.model !== undefined && args.model.length > 0) {
|
|
216
316
|
params.set("model", args.model);
|
|
217
317
|
}
|
|
318
|
+
if (args.clientPid !== undefined) {
|
|
319
|
+
params.set("clientPid", String(args.clientPid));
|
|
320
|
+
}
|
|
218
321
|
const res = await callApi(`/api/logs?${params.toString()}`);
|
|
219
322
|
if (!res.ok) return toolError(`GET /api/logs returned ${res.status}`);
|
|
220
323
|
const rawBody: unknown = await res.json();
|
|
@@ -231,6 +334,7 @@ export type SearchLogsArgs = {
|
|
|
231
334
|
scanLimit?: number;
|
|
232
335
|
sessionId?: string;
|
|
233
336
|
model?: string;
|
|
337
|
+
clientPid?: number;
|
|
234
338
|
};
|
|
235
339
|
|
|
236
340
|
export async function searchLogsImpl(
|
|
@@ -253,6 +357,9 @@ export async function searchLogsImpl(
|
|
|
253
357
|
if (args.model !== undefined && args.model.length > 0) {
|
|
254
358
|
params.set("model", args.model);
|
|
255
359
|
}
|
|
360
|
+
if (args.clientPid !== undefined) {
|
|
361
|
+
params.set("clientPid", String(args.clientPid));
|
|
362
|
+
}
|
|
256
363
|
|
|
257
364
|
const res = await callApi(`/api/logs?${params.toString()}`);
|
|
258
365
|
if (!res.ok) return toolError(`GET /api/logs search returned ${res.status}`);
|
|
@@ -514,6 +621,276 @@ export async function getSessionTimelineImpl(
|
|
|
514
621
|
);
|
|
515
622
|
}
|
|
516
623
|
|
|
624
|
+
export type ListProcessesArgs = {
|
|
625
|
+
limit?: number;
|
|
626
|
+
scanLimit?: number;
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
export async function listProcessesImpl(
|
|
630
|
+
callApi: CallApiFn,
|
|
631
|
+
args: ListProcessesArgs = {},
|
|
632
|
+
): Promise<ToolResult> {
|
|
633
|
+
const scanLimit = clampProcessLogLimit(args.scanLimit ?? args.limit);
|
|
634
|
+
const params = new URLSearchParams({
|
|
635
|
+
q: "",
|
|
636
|
+
offset: "0",
|
|
637
|
+
limit: String(Math.min(scanLimit, 50)),
|
|
638
|
+
scanLimit: String(scanLimit),
|
|
639
|
+
});
|
|
640
|
+
const res = await callApi(`/api/logs?${params.toString()}`);
|
|
641
|
+
if (!res.ok) return toolError(`GET /api/logs process scan returned ${res.status}`);
|
|
642
|
+
const rawBody: unknown = await res.json();
|
|
643
|
+
const parsed = SearchLogsResponseSchema.safeParse(rawBody);
|
|
644
|
+
if (!parsed.success) return toolError("GET /api/logs returned an unparseable process scan shape");
|
|
645
|
+
|
|
646
|
+
const grouped = new Map<number, CapturedLog[]>();
|
|
647
|
+
for (const log of parsed.data.logs) {
|
|
648
|
+
if (log.clientPid === null || log.clientPid === undefined) continue;
|
|
649
|
+
const existing = grouped.get(log.clientPid) ?? [];
|
|
650
|
+
existing.push(log);
|
|
651
|
+
grouped.set(log.clientPid, existing);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const processes = [...grouped.entries()]
|
|
655
|
+
.map(([clientPid, logs]) => processSummary(clientPid, logs, logs.length, logs.length))
|
|
656
|
+
.sort((left, right) => {
|
|
657
|
+
const leftUpdated = typeof left["updatedAt"] === "string" ? left["updatedAt"] : "";
|
|
658
|
+
const rightUpdated = typeof right["updatedAt"] === "string" ? right["updatedAt"] : "";
|
|
659
|
+
return rightUpdated.localeCompare(leftUpdated);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
return textJson({
|
|
663
|
+
scanned: parsed.data.scanned ?? parsed.data.logs.length,
|
|
664
|
+
processCount: processes.length,
|
|
665
|
+
processes,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export type ProcessArgs = {
|
|
670
|
+
clientPid: number;
|
|
671
|
+
limit?: number;
|
|
672
|
+
offset?: number;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
export async function getProcessImpl(callApi: CallApiFn, args: ProcessArgs): Promise<ToolResult> {
|
|
676
|
+
const limit = clampProcessLogLimit(args.limit);
|
|
677
|
+
const offset = args.offset ?? 0;
|
|
678
|
+
const params = new URLSearchParams({
|
|
679
|
+
clientPid: String(args.clientPid),
|
|
680
|
+
offset: String(offset),
|
|
681
|
+
limit: String(limit),
|
|
682
|
+
compact: "1",
|
|
683
|
+
});
|
|
684
|
+
const result = await readCompactLogsPage(callApi, params);
|
|
685
|
+
if (isToolResult(result)) return result;
|
|
686
|
+
return textJson(processSummary(args.clientPid, result.logs, result.total, limit));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export async function getProcessTimelineImpl(
|
|
690
|
+
callApi: CallApiFn,
|
|
691
|
+
args: ProcessArgs,
|
|
692
|
+
): Promise<ToolResult> {
|
|
693
|
+
const limit = clampTimelineLimit(args.limit);
|
|
694
|
+
const offset = args.offset ?? 0;
|
|
695
|
+
const params = new URLSearchParams({
|
|
696
|
+
clientPid: String(args.clientPid),
|
|
697
|
+
offset: String(offset),
|
|
698
|
+
limit: String(limit),
|
|
699
|
+
compact: "1",
|
|
700
|
+
});
|
|
701
|
+
const result = await readCompactLogsPage(callApi, params);
|
|
702
|
+
if (isToolResult(result)) return result;
|
|
703
|
+
const sortedLogs = [...result.logs].sort((left, right) => left.id - right.id);
|
|
704
|
+
return textJson({
|
|
705
|
+
clientPid: args.clientPid,
|
|
706
|
+
logCoverage: "bounded",
|
|
707
|
+
total: result.total,
|
|
708
|
+
limit,
|
|
709
|
+
eventCount: sortedLogs.length,
|
|
710
|
+
requestCount: sortedLogs.length,
|
|
711
|
+
errorCount: sortedLogs.filter(logHasError).length,
|
|
712
|
+
startedAt: sortedLogs[0]?.timestamp ?? null,
|
|
713
|
+
updatedAt: sortedLogs.at(-1)?.timestamp ?? null,
|
|
714
|
+
sessions: sortedUniqueStrings(sortedLogs.map((log) => log.sessionId)),
|
|
715
|
+
models: sortedUniqueStrings(sortedLogs.map((log) => log.model)),
|
|
716
|
+
providers: sortedUniqueStrings(sortedLogs.map((log) => log.providerName)),
|
|
717
|
+
events: sortedLogs.map((log, index) => ({
|
|
718
|
+
sequence: index + 1,
|
|
719
|
+
kind: "request",
|
|
720
|
+
logId: log.id,
|
|
721
|
+
...buildLogSummary(log),
|
|
722
|
+
})),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
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
|
+
}
|
|
734
|
+
const log = parsed.data;
|
|
735
|
+
return textJson({
|
|
736
|
+
id: log.id,
|
|
737
|
+
sessionId: log.sessionId,
|
|
738
|
+
userAgent: log.userAgent,
|
|
739
|
+
origin: log.origin,
|
|
740
|
+
clientPid: log.clientPid ?? null,
|
|
741
|
+
clientPort: log.clientPort ?? null,
|
|
742
|
+
clientCwd: log.clientCwd ?? null,
|
|
743
|
+
clientProjectFolder: log.clientProjectFolder ?? null,
|
|
744
|
+
apiFormat: log.apiFormat,
|
|
745
|
+
method: log.method,
|
|
746
|
+
path: log.path,
|
|
747
|
+
model: log.model,
|
|
748
|
+
rawHeaders: log.rawHeaders ?? null,
|
|
749
|
+
headers: log.headers ?? null,
|
|
750
|
+
hasRawHeaders: log.rawHeaders !== undefined && Object.keys(log.rawHeaders).length > 0,
|
|
751
|
+
hasHeaders: log.headers !== undefined && Object.keys(log.headers).length > 0,
|
|
752
|
+
bodyContentMode: log.bodyContentMode ?? null,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export async function getRuntimeConfigImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
757
|
+
const configRes = await callApi("/api/config");
|
|
758
|
+
if (!configRes.ok) return toolError(`GET /api/config returned ${configRes.status}`);
|
|
759
|
+
const statsRes = await callApi("/api/logs?stats=1");
|
|
760
|
+
if (!statsRes.ok) return toolError(`GET /api/logs?stats=1 returned ${statsRes.status}`);
|
|
761
|
+
const config: unknown = await configRes.json();
|
|
762
|
+
const storage: unknown = await statsRes.json();
|
|
763
|
+
return textJson({
|
|
764
|
+
config,
|
|
765
|
+
storage,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export type DiagnoseCaptureArgs = {
|
|
770
|
+
id?: number;
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
export async function diagnoseCaptureImpl(
|
|
774
|
+
callApi: CallApiFn,
|
|
775
|
+
args: DiagnoseCaptureArgs = {},
|
|
776
|
+
): Promise<ToolResult> {
|
|
777
|
+
const runtime = await getRuntimeConfigImpl(callApi);
|
|
778
|
+
if (runtime.isError === true) return runtime;
|
|
779
|
+
const runtimeText = runtime.content[0]?.text ?? "{}";
|
|
780
|
+
const runtimePayload: unknown = JSON.parse(runtimeText);
|
|
781
|
+
const runtimeRecord = isRecord(runtimePayload) ? runtimePayload : {};
|
|
782
|
+
const configRecord = isRecord(runtimeRecord["config"]) ? runtimeRecord["config"] : {};
|
|
783
|
+
const captureMode = stringField(configRecord, "captureMode");
|
|
784
|
+
|
|
785
|
+
let log: CapturedLog | null = null;
|
|
786
|
+
if (args.id !== undefined) {
|
|
787
|
+
const logRes = await callApi(`/api/logs/${args.id}`);
|
|
788
|
+
if (!logRes.ok) return toolError(`GET /api/logs/${args.id} returned ${logRes.status}`);
|
|
789
|
+
const rawLog: unknown = await logRes.json();
|
|
790
|
+
const parsed = CapturedLogSchema.safeParse(rawLog);
|
|
791
|
+
if (!parsed.success) return toolError(`GET /api/logs/${args.id} returned an unparseable log`);
|
|
792
|
+
log = parsed.data;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const findings: string[] = [];
|
|
796
|
+
const recommendations: string[] = [];
|
|
797
|
+
if (captureMode !== "full") {
|
|
798
|
+
findings.push(
|
|
799
|
+
"Runtime captureMode is not full, so request/response bodies and headers may be compacted.",
|
|
800
|
+
);
|
|
801
|
+
recommendations.push(
|
|
802
|
+
"Restart or launch Agent Inspector with full capture mode when header/body evidence is required.",
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
if (log !== null) {
|
|
806
|
+
if (log.rawHeaders === undefined || Object.keys(log.rawHeaders).length === 0) {
|
|
807
|
+
findings.push(`Log ${String(log.id)} has no rawHeaders stored.`);
|
|
808
|
+
}
|
|
809
|
+
if (log.headers === undefined || Object.keys(log.headers).length === 0) {
|
|
810
|
+
findings.push(`Log ${String(log.id)} has no upstream headers stored.`);
|
|
811
|
+
}
|
|
812
|
+
if (log.rawRequestBody === null) {
|
|
813
|
+
findings.push(`Log ${String(log.id)} does not include rawRequestBody in this response.`);
|
|
814
|
+
}
|
|
815
|
+
if (log.bodyContentMode === "compact") {
|
|
816
|
+
findings.push(`Log ${String(log.id)} is in compact body mode.`);
|
|
817
|
+
recommendations.push(
|
|
818
|
+
"Use inspector_get_log_headers for header-only checks and enable full capture for future body inspection.",
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return textJson({
|
|
824
|
+
captureMode,
|
|
825
|
+
logId: log?.id ?? null,
|
|
826
|
+
hasLog: log !== null,
|
|
827
|
+
findings,
|
|
828
|
+
recommendations,
|
|
829
|
+
runtime: runtimeRecord,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export type AddGroupProcessArgs = {
|
|
834
|
+
groupId: string;
|
|
835
|
+
clientPid: number;
|
|
836
|
+
labelPrefix?: string;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
export async function addGroupProcessImpl(
|
|
840
|
+
callApi: CallApiFn,
|
|
841
|
+
args: AddGroupProcessArgs,
|
|
842
|
+
): Promise<ToolResult> {
|
|
843
|
+
const params = new URLSearchParams({
|
|
844
|
+
clientPid: String(args.clientPid),
|
|
845
|
+
offset: "0",
|
|
846
|
+
limit: String(PROCESS_LOG_LIMIT_MAX),
|
|
847
|
+
compact: "1",
|
|
848
|
+
});
|
|
849
|
+
const result = await readCompactLogsPage(callApi, params);
|
|
850
|
+
if (isToolResult(result)) return result;
|
|
851
|
+
|
|
852
|
+
const bySession = new Map<string, CapturedLog[]>();
|
|
853
|
+
for (const log of result.logs) {
|
|
854
|
+
if (log.sessionId === null || log.sessionId === "") continue;
|
|
855
|
+
const existing = bySession.get(log.sessionId) ?? [];
|
|
856
|
+
existing.push(log);
|
|
857
|
+
bySession.set(log.sessionId, existing);
|
|
858
|
+
}
|
|
859
|
+
if (bySession.size === 0) {
|
|
860
|
+
return toolError(`No sessions found for clientPid ${String(args.clientPid)}`);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
let groupResult: ReturnType<typeof addInspectorGroupSession> | null = null;
|
|
864
|
+
const attached: Array<Record<string, unknown>> = [];
|
|
865
|
+
for (const [sessionId, logs] of bySession.entries()) {
|
|
866
|
+
const first = logs[0];
|
|
867
|
+
const last = logs.at(-1);
|
|
868
|
+
const userAgent = last?.userAgent ?? first?.userAgent ?? null;
|
|
869
|
+
const agent = inferAgentFromUserAgent(userAgent);
|
|
870
|
+
groupResult = addInspectorGroupSession(args.groupId, {
|
|
871
|
+
sessionId,
|
|
872
|
+
label: `${args.labelPrefix ?? `PID ${String(args.clientPid)}`} - ${sessionId}`,
|
|
873
|
+
agent,
|
|
874
|
+
provider: last?.providerName ?? first?.providerName ?? null,
|
|
875
|
+
model: last?.model ?? first?.model ?? null,
|
|
876
|
+
metadata: {
|
|
877
|
+
clientPid: args.clientPid,
|
|
878
|
+
processLogCount: logs.length,
|
|
879
|
+
userAgent: userAgent ?? "",
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
if (!groupResult.ok) return toolError(groupResult.message);
|
|
883
|
+
attached.push({ sessionId, logCount: logs.length, agent, userAgent });
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (groupResult === null || !groupResult.ok) return toolError("Group process attach failed");
|
|
887
|
+
return textJson({
|
|
888
|
+
group: groupResult.value,
|
|
889
|
+
clientPid: args.clientPid,
|
|
890
|
+
attached,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
517
894
|
type EvidenceFileKind = "json" | "markdown" | "html" | "zip";
|
|
518
895
|
|
|
519
896
|
type EvidenceFileSummary = {
|
|
@@ -907,7 +1284,7 @@ function providerDiagnosticHints(result: unknown): string[] {
|
|
|
907
1284
|
}
|
|
908
1285
|
if (text.includes("nonstream") || text.includes("non-stream")) {
|
|
909
1286
|
hints.push(
|
|
910
|
-
"The failure mentions non-streaming. Compare the
|
|
1287
|
+
"The failure mentions non-streaming. Compare the Anthropic, OpenAI Chat, and OpenAI Responses non-streaming path and payload with the streaming request.",
|
|
911
1288
|
);
|
|
912
1289
|
}
|
|
913
1290
|
return [...new Set(hints)];
|
package/src/proxy/logSearch.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type LogSearchOptions = {
|
|
|
10
10
|
query: string;
|
|
11
11
|
sessionId?: string;
|
|
12
12
|
model?: string;
|
|
13
|
+
clientPid?: number;
|
|
13
14
|
offset: number;
|
|
14
15
|
limit: number;
|
|
15
16
|
scanLimit: number;
|
|
@@ -93,6 +94,7 @@ export async function searchLogsPage(options: LogSearchOptions): Promise<LogSear
|
|
|
93
94
|
const firstPage = await listLogsPage({
|
|
94
95
|
sessionId: options.sessionId,
|
|
95
96
|
model: options.model,
|
|
97
|
+
clientPid: options.clientPid,
|
|
96
98
|
offset: 0,
|
|
97
99
|
limit: 1,
|
|
98
100
|
includeBodies: false,
|
|
@@ -101,6 +103,7 @@ export async function searchLogsPage(options: LogSearchOptions): Promise<LogSear
|
|
|
101
103
|
const scanPage = await listLogsPage({
|
|
102
104
|
sessionId: options.sessionId,
|
|
103
105
|
model: options.model,
|
|
106
|
+
clientPid: options.clientPid,
|
|
104
107
|
offset: scanOffset,
|
|
105
108
|
limit: scanLimit,
|
|
106
109
|
includeBodies: false,
|
package/src/proxy/store.ts
CHANGED
|
@@ -99,6 +99,7 @@ export type ClearPersistedLogsByIdsResult = {
|
|
|
99
99
|
export type ListLogsPageOptions = {
|
|
100
100
|
sessionId?: string;
|
|
101
101
|
model?: string;
|
|
102
|
+
clientPid?: number;
|
|
102
103
|
offset: number;
|
|
103
104
|
limit: number;
|
|
104
105
|
includeBodies?: boolean;
|
|
@@ -114,6 +115,7 @@ export type ListLogsPageResult = {
|
|
|
114
115
|
export type ListLogsCursorPageOptions = {
|
|
115
116
|
sessionId?: string;
|
|
116
117
|
model?: string;
|
|
118
|
+
clientPid?: number;
|
|
117
119
|
limit: number;
|
|
118
120
|
includeBodies?: boolean;
|
|
119
121
|
beforeLogId?: number;
|
|
@@ -145,6 +147,7 @@ type LogCursorPageIndex = {
|
|
|
145
147
|
key: string;
|
|
146
148
|
sessionId?: string;
|
|
147
149
|
model?: string;
|
|
150
|
+
clientPid?: number;
|
|
148
151
|
ids: number[];
|
|
149
152
|
logsById: Map<number, CapturedLog>;
|
|
150
153
|
entriesById: Map<number, LogIndexEntry>;
|
|
@@ -155,17 +158,19 @@ let logCursorPageIndexBuildCount = 0;
|
|
|
155
158
|
let sessionArchiveQueue: Promise<void> = Promise.resolve();
|
|
156
159
|
|
|
157
160
|
function cursorPageIndexKey(
|
|
158
|
-
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
|
|
161
|
+
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
|
|
159
162
|
): string {
|
|
160
163
|
return JSON.stringify({
|
|
161
164
|
sessionId: options.sessionId ?? null,
|
|
162
165
|
model: options.model ?? null,
|
|
166
|
+
clientPid: options.clientPid ?? null,
|
|
163
167
|
});
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
function matchesCursorIndexFilters(log: CapturedLog, index: LogCursorPageIndex): boolean {
|
|
167
171
|
if (index.sessionId !== undefined && getLogSessionId(log) !== index.sessionId) return false;
|
|
168
172
|
if (index.model !== undefined && log.model !== index.model) return false;
|
|
173
|
+
if (index.clientPid !== undefined && log.clientPid !== index.clientPid) return false;
|
|
169
174
|
return true;
|
|
170
175
|
}
|
|
171
176
|
|
|
@@ -561,17 +566,27 @@ async function scanLogFileForId(filePath: string, id: number): Promise<CapturedL
|
|
|
561
566
|
return lastMatch;
|
|
562
567
|
}
|
|
563
568
|
|
|
564
|
-
export function getFilteredLogs(
|
|
569
|
+
export function getFilteredLogs(
|
|
570
|
+
sessionId?: string,
|
|
571
|
+
model?: string,
|
|
572
|
+
clientPid?: number,
|
|
573
|
+
): CapturedLog[] {
|
|
565
574
|
// Cache maintains insertion order (sorted by ID since logs are added in ID order)
|
|
566
575
|
// Use spread instead of Array.from for slightly better performance
|
|
567
576
|
return [...memoryCache.values()].filter((l) => {
|
|
568
|
-
return matchesLogFilters(l, sessionId, model);
|
|
577
|
+
return matchesLogFilters(l, sessionId, model, clientPid);
|
|
569
578
|
});
|
|
570
579
|
}
|
|
571
580
|
|
|
572
|
-
function matchesLogFilters(
|
|
581
|
+
function matchesLogFilters(
|
|
582
|
+
log: CapturedLog,
|
|
583
|
+
sessionId?: string,
|
|
584
|
+
model?: string,
|
|
585
|
+
clientPid?: number,
|
|
586
|
+
): boolean {
|
|
573
587
|
if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return false;
|
|
574
588
|
if (model !== undefined && log.model !== model) return false;
|
|
589
|
+
if (clientPid !== undefined && log.clientPid !== clientPid) return false;
|
|
575
590
|
return true;
|
|
576
591
|
}
|
|
577
592
|
|
|
@@ -637,7 +652,11 @@ async function visitArchivedSessionLogs(
|
|
|
637
652
|
}
|
|
638
653
|
|
|
639
654
|
function canUseIndexedListPage(options: ListLogsPageOptions): boolean {
|
|
640
|
-
return
|
|
655
|
+
return (
|
|
656
|
+
options.sessionId === undefined &&
|
|
657
|
+
options.model === undefined &&
|
|
658
|
+
options.clientPid === undefined
|
|
659
|
+
);
|
|
641
660
|
}
|
|
642
661
|
|
|
643
662
|
function hasUsableIndexOffset(entry: LogIndexEntry): boolean {
|
|
@@ -695,7 +714,7 @@ export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLo
|
|
|
695
714
|
let total = 0;
|
|
696
715
|
|
|
697
716
|
const visitLog = (log: CapturedLog): void => {
|
|
698
|
-
if (!matchesLogFilters(log, options.sessionId, options.model)) return;
|
|
717
|
+
if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) return;
|
|
699
718
|
|
|
700
719
|
if (!seenIds.has(log.id)) {
|
|
701
720
|
seenIds.add(log.id);
|
|
@@ -781,16 +800,20 @@ function matchesLogIndexEntryFilters(
|
|
|
781
800
|
entry: LogIndexEntry,
|
|
782
801
|
sessionId: string | undefined,
|
|
783
802
|
model: string | undefined,
|
|
803
|
+
clientPid: number | undefined,
|
|
784
804
|
): boolean {
|
|
785
805
|
if (sessionId !== undefined && entry.sessionId !== sessionId) return false;
|
|
786
806
|
if (model !== undefined && entry.model !== model) return false;
|
|
807
|
+
if (clientPid !== undefined && entry.summary?.clientPid !== clientPid) return false;
|
|
787
808
|
return true;
|
|
788
809
|
}
|
|
789
810
|
|
|
790
811
|
async function buildLogCursorPageIndexFromLogIndex(
|
|
791
|
-
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
|
|
812
|
+
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
|
|
792
813
|
allowRebuild: boolean,
|
|
793
814
|
): Promise<LogCursorPageIndex | null> {
|
|
815
|
+
if (options.clientPid !== undefined) return null;
|
|
816
|
+
|
|
794
817
|
let entries = await listFilteredIndexEntries(options);
|
|
795
818
|
if (entries.length === 0 && allowRebuild) {
|
|
796
819
|
await rebuildIndex();
|
|
@@ -804,7 +827,7 @@ async function buildLogCursorPageIndexFromLogIndex(
|
|
|
804
827
|
}
|
|
805
828
|
|
|
806
829
|
const matchingEntries = entries.filter((entry) =>
|
|
807
|
-
matchesLogIndexEntryFilters(entry, options.sessionId, options.model),
|
|
830
|
+
matchesLogIndexEntryFilters(entry, options.sessionId, options.model, options.clientPid),
|
|
808
831
|
);
|
|
809
832
|
|
|
810
833
|
if (matchingEntries.some((entry) => !hasLogIndexSummary(entry))) {
|
|
@@ -828,7 +851,7 @@ async function buildLogCursorPageIndexFromLogIndex(
|
|
|
828
851
|
|
|
829
852
|
const logsById = new Map<number, CapturedLog>();
|
|
830
853
|
for (const log of memoryCache.values()) {
|
|
831
|
-
if (!matchesLogFilters(log, options.sessionId, options.model)) continue;
|
|
854
|
+
if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) continue;
|
|
832
855
|
idsById.add(log.id);
|
|
833
856
|
logsById.set(log.id, log);
|
|
834
857
|
}
|
|
@@ -838,7 +861,7 @@ async function buildLogCursorPageIndexFromLogIndex(
|
|
|
838
861
|
const archivedLogs = await listArchivedSessionLogs(options.sessionId);
|
|
839
862
|
for (const archivedLog of archivedLogs) {
|
|
840
863
|
const log = normalizeLog(archivedLog);
|
|
841
|
-
if (!matchesLogFilters(log, options.sessionId, options.model)) continue;
|
|
864
|
+
if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) continue;
|
|
842
865
|
observeSessionLog(log);
|
|
843
866
|
idsById.add(log.id);
|
|
844
867
|
logsById.set(log.id, log);
|
|
@@ -849,6 +872,7 @@ async function buildLogCursorPageIndexFromLogIndex(
|
|
|
849
872
|
key: cursorPageIndexKey(options),
|
|
850
873
|
sessionId: options.sessionId,
|
|
851
874
|
model: options.model,
|
|
875
|
+
clientPid: options.clientPid,
|
|
852
876
|
ids: [...idsById].sort((left, right) => left - right),
|
|
853
877
|
logsById,
|
|
854
878
|
entriesById,
|
|
@@ -856,7 +880,7 @@ async function buildLogCursorPageIndexFromLogIndex(
|
|
|
856
880
|
}
|
|
857
881
|
|
|
858
882
|
async function buildLogCursorPageIndex(
|
|
859
|
-
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
|
|
883
|
+
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
|
|
860
884
|
): Promise<LogCursorPageIndex> {
|
|
861
885
|
logCursorPageIndexBuildCount += 1;
|
|
862
886
|
const indexed = await buildLogCursorPageIndexFromLogIndex(options, true);
|
|
@@ -866,7 +890,7 @@ async function buildLogCursorPageIndex(
|
|
|
866
890
|
const logsById = new Map<number, CapturedLog>();
|
|
867
891
|
|
|
868
892
|
const visitLog = (log: CapturedLog): void => {
|
|
869
|
-
if (!matchesLogFilters(log, options.sessionId, options.model)) return;
|
|
893
|
+
if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) return;
|
|
870
894
|
logsById.set(log.id, log);
|
|
871
895
|
};
|
|
872
896
|
|
|
@@ -885,6 +909,7 @@ async function buildLogCursorPageIndex(
|
|
|
885
909
|
key,
|
|
886
910
|
sessionId: options.sessionId,
|
|
887
911
|
model: options.model,
|
|
912
|
+
clientPid: options.clientPid,
|
|
888
913
|
ids: [...logsById.keys()].sort((left, right) => left - right),
|
|
889
914
|
logsById,
|
|
890
915
|
entriesById: new Map<number, LogIndexEntry>(),
|
|
@@ -892,7 +917,7 @@ async function buildLogCursorPageIndex(
|
|
|
892
917
|
}
|
|
893
918
|
|
|
894
919
|
async function getLogCursorPageIndex(
|
|
895
|
-
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
|
|
920
|
+
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
|
|
896
921
|
): Promise<LogCursorPageIndex> {
|
|
897
922
|
const key = cursorPageIndexKey(options);
|
|
898
923
|
const cached = logCursorPageIndexes.get(key);
|
|
@@ -1027,7 +1052,7 @@ async function collectIndexedSessionLogSummaries(sessionId: string): Promise<Ses
|
|
|
1027
1052
|
|
|
1028
1053
|
const needsMetadataRebuild = entries.some((entry) => !hasLogIndexFilterMetadata(entry));
|
|
1029
1054
|
const needsSummaryRebuild = entries.some((entry) => {
|
|
1030
|
-
if (!matchesLogIndexEntryFilters(entry, sessionId, undefined)) return false;
|
|
1055
|
+
if (!matchesLogIndexEntryFilters(entry, sessionId, undefined, undefined)) return false;
|
|
1031
1056
|
return !hasLogIndexSummary(entry);
|
|
1032
1057
|
});
|
|
1033
1058
|
|
|
@@ -1038,7 +1063,7 @@ async function collectIndexedSessionLogSummaries(sessionId: string): Promise<Ses
|
|
|
1038
1063
|
|
|
1039
1064
|
const summaries: SessionLogSummary[] = [];
|
|
1040
1065
|
for (const entry of entries) {
|
|
1041
|
-
if (!matchesLogIndexEntryFilters(entry, sessionId, undefined)) continue;
|
|
1066
|
+
if (!matchesLogIndexEntryFilters(entry, sessionId, undefined, undefined)) continue;
|
|
1042
1067
|
if (entry.summary === undefined) continue;
|
|
1043
1068
|
summaries.push(buildSessionLogSummary(compactLogFromIndexSummary(entry.summary)));
|
|
1044
1069
|
}
|