@tonyclaw/agent-inspector 2.0.2 → 2.0.3
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-Bp7_x-5N.js → CompareDrawer-D5A4bTfV.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-Da0jpBkp.js +101 -0
- package/.output/public/assets/{ReplayDialog-DFHCd0yx.js → ReplayDialog-CxUk_TF0.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-ehyrskxt.js → RequestAnatomy-DIlzjgjJ.js} +1 -1
- package/.output/public/assets/ResponseView-DQCuKJ1G.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-Bjs4Lqwn.js → StreamingChunkSequence-DHk4SGGL.js} +1 -1
- package/.output/public/assets/_sessionId-dY1TTl7N.js +1 -0
- package/.output/public/assets/index-D7wwbwly.css +1 -0
- package/.output/public/assets/index-FqQZbfl2.js +1 -0
- package/.output/public/assets/{json-viewer-6uV_YXws.js → json-viewer-BbU0n8eM.js} +1 -1
- package/.output/public/assets/{main-FSGUGtEL.js → main-CZT_F-gu.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +8 -8
- package/.output/server/{_sessionId-_bf9vUww.mjs → _sessionId-B-s9P7fJ.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-DIth2DQM.mjs → CompareDrawer-C08L3UOO.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-249bTH-T.mjs → ProxyViewerContainer-CMWl3Ijy.mjs} +399 -49
- package/.output/server/_ssr/{ReplayDialog-C1aGx0y1.mjs → ReplayDialog-CPDo9_G5.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-D2bCiEJn.mjs → RequestAnatomy-D9wt_K1E.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-DP6k4Xs_.mjs → ResponseView-DXaL7nY3.mjs} +4 -4
- package/.output/server/_ssr/{StreamingChunkSequence-HyXZV-b5.mjs → StreamingChunkSequence-B_hudZyb.mjs} +3 -3
- package/.output/server/_ssr/{index-Bt47f9pn.mjs → index-CuE_BN86.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Co-YRwUP.mjs → json-viewer-Ci6kkjde.mjs} +2 -2
- package/.output/server/_ssr/{router-to_OJirX.mjs → router-BemxgIg7.mjs} +24 -93
- package/.output/server/{_tanstack-start-manifest_v-Bd-2YRWo.mjs → _tanstack-start-manifest_v--L1_b4sd.mjs} +1 -1
- package/.output/server/index.mjs +67 -67
- package/README.md +5 -2
- package/package.json +1 -1
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +218 -0
- package/src/components/proxy-viewer/ConversationGroup.tsx +6 -0
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +33 -0
- package/src/components/proxy-viewer/TurnGroup.tsx +11 -1
- package/src/components/proxy-viewer/viewerState.ts +177 -0
- package/src/proxy/chunkStorage.ts +3 -4
- package/src/proxy/logger.ts +8 -15
- package/src/proxy/store.ts +8 -16
- package/src/routes/api/providers.$providerId.test.log.ts +0 -79
- package/.output/public/assets/ProxyViewerContainer-USuxPy-K.js +0 -101
- package/.output/public/assets/ResponseView-BNGyc8e_.js +0 -1
- package/.output/public/assets/_sessionId-D_SeK_qp.js +0 -1
- package/.output/public/assets/index-BGGOWR7A.js +0 -1
- package/.output/public/assets/index-CIL46Z2y.css +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState, type JSX } from "react";
|
|
2
|
+
import { AlertTriangle, Brain, Clock, Loader2, MessageSquare, Wrench, Zap } from "lucide-react";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { formatTokens } from "../../lib/utils";
|
|
5
|
+
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
6
|
+
import { KnowledgeCandidateSchema, type KnowledgeCandidate } from "../../knowledge/types";
|
|
7
|
+
import type { CapturedLog } from "../../proxy/schemas";
|
|
8
|
+
import { Badge } from "../ui/badge";
|
|
9
|
+
import { Button } from "../ui/button";
|
|
10
|
+
import { buildTraceSummary } from "./viewerState";
|
|
11
|
+
|
|
12
|
+
const CandidateResponseSchema = z.object({
|
|
13
|
+
candidates: z.array(KnowledgeCandidateSchema),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
type CandidateLoadState =
|
|
17
|
+
| { status: "idle"; error: null }
|
|
18
|
+
| { status: "loading"; error: null }
|
|
19
|
+
| { status: "ready"; error: null }
|
|
20
|
+
| { status: "failed"; error: string };
|
|
21
|
+
|
|
22
|
+
type AgentTraceSummaryProps = {
|
|
23
|
+
logs: CapturedLog[];
|
|
24
|
+
scopeId: string;
|
|
25
|
+
slowResponseThresholdSeconds: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function formatElapsed(ms: number | null): string {
|
|
29
|
+
if (ms === null) return "-";
|
|
30
|
+
if (ms < 1000) return `${String(ms)}ms`;
|
|
31
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatTimeRange(startedAt: string | null, endedAt: string | null): string | null {
|
|
35
|
+
if (startedAt === null || endedAt === null) return null;
|
|
36
|
+
const format = (iso: string): string =>
|
|
37
|
+
new Date(iso).toLocaleTimeString([], {
|
|
38
|
+
hour: "2-digit",
|
|
39
|
+
minute: "2-digit",
|
|
40
|
+
second: "2-digit",
|
|
41
|
+
});
|
|
42
|
+
return `${format(startedAt)} - ${format(endedAt)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scrollToLog(logId: number): void {
|
|
46
|
+
const target = document.getElementById(`log-${String(logId)}`);
|
|
47
|
+
if (!(target instanceof HTMLElement)) return;
|
|
48
|
+
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
49
|
+
target.focus({ preventScroll: true });
|
|
50
|
+
if (target.getAttribute("data-nav-action") === "expand") {
|
|
51
|
+
target.click();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function CandidateList({ candidates }: { candidates: KnowledgeCandidate[] }): JSX.Element | null {
|
|
56
|
+
if (candidates.length === 0) return null;
|
|
57
|
+
return (
|
|
58
|
+
<div className="mt-2 grid gap-1.5">
|
|
59
|
+
{candidates.map((candidate) => (
|
|
60
|
+
<div
|
|
61
|
+
key={candidate.id}
|
|
62
|
+
className="rounded-md border border-border/80 bg-background/60 px-2.5 py-2"
|
|
63
|
+
>
|
|
64
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
65
|
+
<Badge variant="outline" className="h-5 px-1.5 text-[10px] font-mono">
|
|
66
|
+
{candidate.type}
|
|
67
|
+
</Badge>
|
|
68
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium" title={candidate.title}>
|
|
69
|
+
{candidate.title}
|
|
70
|
+
</span>
|
|
71
|
+
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">
|
|
72
|
+
{candidate.status}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
76
|
+
{candidate.logIds.map((logId) => (
|
|
77
|
+
<button
|
|
78
|
+
key={logId}
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => scrollToLog(logId)}
|
|
81
|
+
className="rounded border border-border px-1.5 py-0.5 font-mono text-[10px] text-blue-400 transition-colors hover:bg-muted hover:text-blue-300"
|
|
82
|
+
>
|
|
83
|
+
#{logId}
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function AgentTraceSummary({
|
|
94
|
+
logs,
|
|
95
|
+
scopeId,
|
|
96
|
+
slowResponseThresholdSeconds,
|
|
97
|
+
}: AgentTraceSummaryProps): JSX.Element | null {
|
|
98
|
+
const [candidates, setCandidates] = useState<KnowledgeCandidate[]>([]);
|
|
99
|
+
const [candidateState, setCandidateState] = useState<CandidateLoadState>({
|
|
100
|
+
status: "idle",
|
|
101
|
+
error: null,
|
|
102
|
+
});
|
|
103
|
+
const summary = useMemo(
|
|
104
|
+
() => buildTraceSummary(logs, slowResponseThresholdSeconds, candidates.length),
|
|
105
|
+
[candidates.length, logs, slowResponseThresholdSeconds],
|
|
106
|
+
);
|
|
107
|
+
const timeRange = useMemo(
|
|
108
|
+
() => formatTimeRange(summary.startedAt, summary.endedAt),
|
|
109
|
+
[summary.endedAt, summary.startedAt],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const createCandidates = useCallback(() => {
|
|
113
|
+
if (logs.length === 0 || candidateState.status === "loading") return;
|
|
114
|
+
setCandidateState({ status: "loading", error: null });
|
|
115
|
+
void (async () => {
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(
|
|
118
|
+
`/api/knowledge/sessions/${encodeURIComponent(scopeId)}/candidates`,
|
|
119
|
+
{ method: "POST" },
|
|
120
|
+
);
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
const message = await readApiError(
|
|
123
|
+
response,
|
|
124
|
+
`Candidate generation failed with ${String(response.status)}`,
|
|
125
|
+
);
|
|
126
|
+
setCandidateState({ status: "failed", error: message });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const parsed = await parseJsonResponse(response, CandidateResponseSchema);
|
|
130
|
+
setCandidates(parsed.candidates);
|
|
131
|
+
setCandidateState({ status: "ready", error: null });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
setCandidateState({
|
|
134
|
+
status: "failed",
|
|
135
|
+
error: error instanceof Error ? error.message : "Candidate response was invalid",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
}, [candidateState.status, logs.length, scopeId]);
|
|
140
|
+
|
|
141
|
+
if (logs.length === 0) return null;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<section className="mb-2 rounded-lg border border-border bg-muted/10 px-3 py-2">
|
|
145
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-xs">
|
|
146
|
+
<span className="inline-flex items-center gap-1.5 font-semibold text-foreground">
|
|
147
|
+
<MessageSquare className="size-3.5 text-blue-400" />
|
|
148
|
+
{summary.llmCallCount} LLM
|
|
149
|
+
</span>
|
|
150
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
151
|
+
<Wrench className="size-3.5 text-amber-400" />
|
|
152
|
+
{summary.toolCallCount} tools
|
|
153
|
+
</span>
|
|
154
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
155
|
+
<Zap className="size-3.5 text-emerald-400" />
|
|
156
|
+
<span className="font-mono">
|
|
157
|
+
{formatTokens(summary.totalInputTokens)} / {formatTokens(summary.totalOutputTokens)}
|
|
158
|
+
</span>
|
|
159
|
+
</span>
|
|
160
|
+
{(summary.totalCacheCreationInputTokens > 0 || summary.totalCacheReadInputTokens > 0) && (
|
|
161
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
162
|
+
<Zap className="size-3.5 text-purple-400" />
|
|
163
|
+
<span className="font-mono">
|
|
164
|
+
+{formatTokens(summary.totalCacheCreationInputTokens)} / ~
|
|
165
|
+
{formatTokens(summary.totalCacheReadInputTokens)}
|
|
166
|
+
</span>
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
170
|
+
<Clock className="size-3.5" />
|
|
171
|
+
<span className="font-mono">{formatElapsed(summary.totalElapsedMs)}</span>
|
|
172
|
+
{summary.maxElapsedMs !== null && (
|
|
173
|
+
<span className="font-mono text-muted-foreground/70">
|
|
174
|
+
max {formatElapsed(summary.maxElapsedMs)}
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
</span>
|
|
178
|
+
{timeRange !== null && (
|
|
179
|
+
<span className="font-mono text-muted-foreground/70">{timeRange}</span>
|
|
180
|
+
)}
|
|
181
|
+
{(summary.failedCallCount > 0 ||
|
|
182
|
+
summary.pendingCallCount > 0 ||
|
|
183
|
+
summary.slowCallCount > 0) && (
|
|
184
|
+
<span className="inline-flex items-center gap-1.5 text-amber-400">
|
|
185
|
+
<AlertTriangle className="size-3.5" />
|
|
186
|
+
<span className="font-mono">
|
|
187
|
+
{summary.failedCallCount} fail / {summary.pendingCallCount} pending /{" "}
|
|
188
|
+
{summary.slowCallCount} slow
|
|
189
|
+
</span>
|
|
190
|
+
</span>
|
|
191
|
+
)}
|
|
192
|
+
<span className="flex-1" />
|
|
193
|
+
<Badge variant="outline" className="h-6 px-2 text-[10px] font-mono">
|
|
194
|
+
{summary.knowledgeCandidateCount} memory
|
|
195
|
+
</Badge>
|
|
196
|
+
<Button
|
|
197
|
+
type="button"
|
|
198
|
+
variant="outline"
|
|
199
|
+
size="sm"
|
|
200
|
+
className="h-7 gap-1.5 px-2 text-xs"
|
|
201
|
+
onClick={createCandidates}
|
|
202
|
+
disabled={candidateState.status === "loading"}
|
|
203
|
+
>
|
|
204
|
+
{candidateState.status === "loading" ? (
|
|
205
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
206
|
+
) : (
|
|
207
|
+
<Brain className="size-3.5" />
|
|
208
|
+
)}
|
|
209
|
+
Candidate
|
|
210
|
+
</Button>
|
|
211
|
+
</div>
|
|
212
|
+
{candidateState.status === "failed" && (
|
|
213
|
+
<p className="mt-2 text-xs text-destructive">{candidateState.error}</p>
|
|
214
|
+
)}
|
|
215
|
+
<CandidateList candidates={candidates} />
|
|
216
|
+
</section>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ConversationGroupData,
|
|
9
9
|
} from "./ConversationHeader";
|
|
10
10
|
import { TurnGroup } from "./TurnGroup";
|
|
11
|
+
import { AgentTraceSummary } from "./AgentTraceSummary";
|
|
11
12
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
12
13
|
import { buildTurnGroups, shouldRenderConversationContent } from "./viewerState";
|
|
13
14
|
|
|
@@ -87,6 +88,11 @@ export const ConversationGroup = memo(function ({
|
|
|
87
88
|
|
|
88
89
|
{shouldRenderConversationContent(standalone, expanded) && (
|
|
89
90
|
<div>
|
|
91
|
+
<AgentTraceSummary
|
|
92
|
+
logs={group.logs}
|
|
93
|
+
scopeId={group.conversationId}
|
|
94
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
95
|
+
/>
|
|
90
96
|
{turnGroups.map((tg) => (
|
|
91
97
|
<TurnGroup
|
|
92
98
|
key={tg.turnIndex}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type JSX } from "react";
|
|
2
|
+
import { ChevronRight, Wrench } from "lucide-react";
|
|
3
|
+
import type { ToolTraceEvent } from "./viewerState";
|
|
4
|
+
|
|
5
|
+
type ToolTraceEventsProps = {
|
|
6
|
+
events: ToolTraceEvent[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function ToolTraceEvents({ events }: ToolTraceEventsProps): JSX.Element | null {
|
|
10
|
+
if (events.length === 0) return null;
|
|
11
|
+
return (
|
|
12
|
+
<div className="mx-3 mb-2 grid gap-1.5">
|
|
13
|
+
{events.map((event) => (
|
|
14
|
+
<div
|
|
15
|
+
key={event.id}
|
|
16
|
+
className="flex min-w-0 items-center gap-2 rounded-md border border-amber-500/20 bg-amber-500/5 px-2.5 py-1.5 text-xs"
|
|
17
|
+
>
|
|
18
|
+
<Wrench className="size-3.5 shrink-0 text-amber-400" />
|
|
19
|
+
<span className="font-mono font-semibold text-amber-300">{event.name}</span>
|
|
20
|
+
<span className="font-mono text-muted-foreground">#{event.logId}</span>
|
|
21
|
+
{event.argumentsPreview !== null && (
|
|
22
|
+
<>
|
|
23
|
+
<ChevronRight className="size-3 shrink-0 text-muted-foreground/60" />
|
|
24
|
+
<span className="min-w-0 truncate font-mono text-muted-foreground">
|
|
25
|
+
{event.argumentsPreview}
|
|
26
|
+
</span>
|
|
27
|
+
</>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -9,7 +9,8 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/
|
|
|
9
9
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
10
10
|
import { LogEntry } from "./LogEntry";
|
|
11
11
|
import { ThreadConnector } from "./ThreadConnector";
|
|
12
|
-
import {
|
|
12
|
+
import { ToolTraceEvents } from "./ToolTraceEvents";
|
|
13
|
+
import { extractToolTraceEvents, isTurnCollapsible, type TurnEntry } from "./viewerState";
|
|
13
14
|
|
|
14
15
|
function formatElapsed(ms: number): string {
|
|
15
16
|
if (ms < 1000) return `${ms}ms`;
|
|
@@ -106,6 +107,14 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
106
107
|
aggregate.maxElapsed !== null &&
|
|
107
108
|
slowResponseThresholdSeconds > 0 &&
|
|
108
109
|
aggregate.maxElapsed > slowResponseThresholdSeconds * 1000;
|
|
110
|
+
const toolEventsByLogId = useMemo(() => {
|
|
111
|
+
const events = new Map<number, ReturnType<typeof extractToolTraceEvents>>();
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const extracted = extractToolTraceEvents(entry.log);
|
|
114
|
+
if (extracted.length > 0) events.set(entry.log.id, extracted);
|
|
115
|
+
}
|
|
116
|
+
return events;
|
|
117
|
+
}, [entries]);
|
|
109
118
|
|
|
110
119
|
// ResizeObserver → re-render connectors when any LogEntry height changes
|
|
111
120
|
const [layoutVersion, setLayoutVersion] = useState(0);
|
|
@@ -327,6 +336,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
327
336
|
comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
|
|
328
337
|
}
|
|
329
338
|
/>
|
|
339
|
+
<ToolTraceEvents events={toolEventsByLogId.get(log.id) ?? []} />
|
|
330
340
|
</div>
|
|
331
341
|
</div>
|
|
332
342
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extractStopReason, isTurnBoundary, type StopReason } from "../../lib/stopReason";
|
|
2
|
+
import { safeGetOwnProperty } from "../../lib/objectUtils";
|
|
2
3
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
3
4
|
import { resolveLogFormat } from "./log-formats";
|
|
4
5
|
|
|
@@ -16,6 +17,34 @@ type ConversationLike = {
|
|
|
16
17
|
logs: CapturedLog[];
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
export type TraceSummary = {
|
|
21
|
+
llmCallCount: number;
|
|
22
|
+
toolCallCount: number;
|
|
23
|
+
failedCallCount: number;
|
|
24
|
+
pendingCallCount: number;
|
|
25
|
+
slowCallCount: number;
|
|
26
|
+
totalInputTokens: number;
|
|
27
|
+
totalOutputTokens: number;
|
|
28
|
+
totalCacheCreationInputTokens: number;
|
|
29
|
+
totalCacheReadInputTokens: number;
|
|
30
|
+
totalElapsedMs: number;
|
|
31
|
+
maxElapsedMs: number | null;
|
|
32
|
+
startedAt: string | null;
|
|
33
|
+
endedAt: string | null;
|
|
34
|
+
knowledgeCandidateCount: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ToolTraceEvent = {
|
|
38
|
+
id: string;
|
|
39
|
+
logId: number;
|
|
40
|
+
index: number;
|
|
41
|
+
provider: "anthropic" | "openai";
|
|
42
|
+
name: string;
|
|
43
|
+
argumentsPreview: string | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const PREVIEW_LIMIT = 180;
|
|
47
|
+
|
|
19
48
|
export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
|
|
20
49
|
return standalone || expanded;
|
|
21
50
|
}
|
|
@@ -64,3 +93,151 @@ export function buildValidPredecessors(groups: ConversationLike[]): Map<number,
|
|
|
64
93
|
|
|
65
94
|
return predecessors;
|
|
66
95
|
}
|
|
96
|
+
|
|
97
|
+
function parseJsonResponse(responseText: string | null): unknown {
|
|
98
|
+
if (responseText === null) return null;
|
|
99
|
+
try {
|
|
100
|
+
const parsed: unknown = JSON.parse(responseText);
|
|
101
|
+
if (typeof parsed === "string") {
|
|
102
|
+
return JSON.parse(parsed);
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function previewValue(value: unknown): string | null {
|
|
111
|
+
if (value === undefined || value === null) return null;
|
|
112
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
113
|
+
if (raw === undefined) return null;
|
|
114
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
115
|
+
if (normalized.length === 0) return null;
|
|
116
|
+
return normalized.length > PREVIEW_LIMIT
|
|
117
|
+
? `${normalized.slice(0, PREVIEW_LIMIT - 1)}...`
|
|
118
|
+
: normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
122
|
+
const parsed = parseJsonResponse(log.responseText);
|
|
123
|
+
const content = safeGetOwnProperty(parsed, "content");
|
|
124
|
+
if (!Array.isArray(content)) return [];
|
|
125
|
+
|
|
126
|
+
const events: ToolTraceEvent[] = [];
|
|
127
|
+
for (const block of content) {
|
|
128
|
+
const type = safeGetOwnProperty(block, "type");
|
|
129
|
+
if (type !== "tool_use") continue;
|
|
130
|
+
const name = safeGetOwnProperty(block, "name");
|
|
131
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
132
|
+
events.push({
|
|
133
|
+
id: `${String(log.id)}-anthropic-tool-${String(events.length)}`,
|
|
134
|
+
logId: log.id,
|
|
135
|
+
index: events.length,
|
|
136
|
+
provider: "anthropic",
|
|
137
|
+
name,
|
|
138
|
+
argumentsPreview: previewValue(safeGetOwnProperty(block, "input")),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return events;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
145
|
+
const parsed = parseJsonResponse(log.responseText);
|
|
146
|
+
const choices = safeGetOwnProperty(parsed, "choices");
|
|
147
|
+
if (!Array.isArray(choices)) return [];
|
|
148
|
+
|
|
149
|
+
const events: ToolTraceEvent[] = [];
|
|
150
|
+
for (const choice of choices) {
|
|
151
|
+
const message = safeGetOwnProperty(choice, "message");
|
|
152
|
+
const toolCalls = safeGetOwnProperty(message, "tool_calls");
|
|
153
|
+
if (!Array.isArray(toolCalls)) continue;
|
|
154
|
+
for (const call of toolCalls) {
|
|
155
|
+
const fn = safeGetOwnProperty(call, "function");
|
|
156
|
+
const name = safeGetOwnProperty(fn, "name");
|
|
157
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
158
|
+
events.push({
|
|
159
|
+
id: `${String(log.id)}-openai-tool-${String(events.length)}`,
|
|
160
|
+
logId: log.id,
|
|
161
|
+
index: events.length,
|
|
162
|
+
provider: "openai",
|
|
163
|
+
name,
|
|
164
|
+
argumentsPreview: previewValue(safeGetOwnProperty(fn, "arguments")),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return events;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function extractToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
172
|
+
const format = resolveLogFormat(log);
|
|
173
|
+
switch (format) {
|
|
174
|
+
case "anthropic":
|
|
175
|
+
return extractAnthropicToolTraceEvents(log);
|
|
176
|
+
case "openai":
|
|
177
|
+
return extractOpenAIToolTraceEvents(log);
|
|
178
|
+
case "unknown":
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildTraceSummary(
|
|
184
|
+
logs: CapturedLog[],
|
|
185
|
+
slowResponseThresholdSeconds: number,
|
|
186
|
+
knowledgeCandidateCount = 0,
|
|
187
|
+
): TraceSummary {
|
|
188
|
+
let failedCallCount = 0;
|
|
189
|
+
let pendingCallCount = 0;
|
|
190
|
+
let slowCallCount = 0;
|
|
191
|
+
let totalInputTokens = 0;
|
|
192
|
+
let totalOutputTokens = 0;
|
|
193
|
+
let totalCacheCreationInputTokens = 0;
|
|
194
|
+
let totalCacheReadInputTokens = 0;
|
|
195
|
+
let totalElapsedMs = 0;
|
|
196
|
+
let maxElapsedMs: number | null = null;
|
|
197
|
+
let toolCallCount = 0;
|
|
198
|
+
|
|
199
|
+
for (const log of logs) {
|
|
200
|
+
if (log.responseStatus === null) {
|
|
201
|
+
pendingCallCount += 1;
|
|
202
|
+
} else if (log.responseStatus >= 400) {
|
|
203
|
+
failedCallCount += 1;
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
log.elapsedMs !== null &&
|
|
207
|
+
slowResponseThresholdSeconds > 0 &&
|
|
208
|
+
log.elapsedMs > slowResponseThresholdSeconds * 1000
|
|
209
|
+
) {
|
|
210
|
+
slowCallCount += 1;
|
|
211
|
+
}
|
|
212
|
+
if (log.inputTokens !== null) totalInputTokens += log.inputTokens;
|
|
213
|
+
if (log.outputTokens !== null) totalOutputTokens += log.outputTokens;
|
|
214
|
+
if (log.cacheCreationInputTokens !== null) {
|
|
215
|
+
totalCacheCreationInputTokens += log.cacheCreationInputTokens;
|
|
216
|
+
}
|
|
217
|
+
if (log.cacheReadInputTokens !== null) {
|
|
218
|
+
totalCacheReadInputTokens += log.cacheReadInputTokens;
|
|
219
|
+
}
|
|
220
|
+
if (log.elapsedMs !== null) {
|
|
221
|
+
totalElapsedMs += log.elapsedMs;
|
|
222
|
+
maxElapsedMs = maxElapsedMs === null ? log.elapsedMs : Math.max(maxElapsedMs, log.elapsedMs);
|
|
223
|
+
}
|
|
224
|
+
toolCallCount += extractToolTraceEvents(log).length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
llmCallCount: logs.length,
|
|
229
|
+
toolCallCount,
|
|
230
|
+
failedCallCount,
|
|
231
|
+
pendingCallCount,
|
|
232
|
+
slowCallCount,
|
|
233
|
+
totalInputTokens,
|
|
234
|
+
totalOutputTokens,
|
|
235
|
+
totalCacheCreationInputTokens,
|
|
236
|
+
totalCacheReadInputTokens,
|
|
237
|
+
totalElapsedMs,
|
|
238
|
+
maxElapsedMs,
|
|
239
|
+
startedAt: logs[0]?.timestamp ?? null,
|
|
240
|
+
endedAt: logs[logs.length - 1]?.timestamp ?? null,
|
|
241
|
+
knowledgeCandidateCount,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -30,11 +30,10 @@ const StreamingChunksDataSchema = z.object({
|
|
|
30
30
|
truncated: z.boolean().optional(),
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
const CHUNKS_DIR_ENV = process.env["CHUNKS_DIR"];
|
|
34
|
-
|
|
35
33
|
export function getChunksDir(): string {
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const chunksDirEnv = process.env["CHUNKS_DIR"];
|
|
35
|
+
if (chunksDirEnv !== undefined && chunksDirEnv !== "") {
|
|
36
|
+
return isAbsolute(chunksDirEnv) ? chunksDirEnv : join(getDataDir(), chunksDirEnv);
|
|
38
37
|
}
|
|
39
38
|
return join(getDataDir(), "chunks");
|
|
40
39
|
}
|
package/src/proxy/logger.ts
CHANGED
|
@@ -3,23 +3,14 @@ import { writeFileSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { getDataDir } from "./dataDir";
|
|
5
5
|
|
|
6
|
-
const LOG_DIR_ENV = process.env["LOG_DIR"];
|
|
7
6
|
const RETENTION_DAYS = Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
|
|
8
|
-
const LOG_FILE_ENV = process.env["AGENT_INSPECTOR_LOG_FILE"];
|
|
9
|
-
|
|
10
|
-
let resolvedLogDir: string | null = null;
|
|
11
7
|
|
|
12
8
|
export function resolveLogDir(): string {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
resolvedLogDir = path.isAbsolute(LOG_DIR_ENV)
|
|
17
|
-
? LOG_DIR_ENV
|
|
18
|
-
: path.join(getDataDir(), LOG_DIR_ENV);
|
|
19
|
-
} else {
|
|
20
|
-
resolvedLogDir = path.join(getDataDir(), "logs");
|
|
9
|
+
const logDirEnv = process.env["LOG_DIR"];
|
|
10
|
+
if (logDirEnv !== undefined && logDirEnv !== "") {
|
|
11
|
+
return path.isAbsolute(logDirEnv) ? logDirEnv : path.join(getDataDir(), logDirEnv);
|
|
21
12
|
}
|
|
22
|
-
return
|
|
13
|
+
return path.join(getDataDir(), "logs");
|
|
23
14
|
}
|
|
24
15
|
|
|
25
16
|
export function getLogFilePath(): string {
|
|
@@ -31,8 +22,9 @@ export function getLogFilePath(): string {
|
|
|
31
22
|
}
|
|
32
23
|
|
|
33
24
|
function getInspectorLogPath(): string {
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
const logFileEnv = process.env["AGENT_INSPECTOR_LOG_FILE"];
|
|
26
|
+
if (logFileEnv !== undefined && logFileEnv !== "") {
|
|
27
|
+
return logFileEnv;
|
|
36
28
|
}
|
|
37
29
|
return path.join(getDataDir(), "logs", "inspector.log");
|
|
38
30
|
}
|
|
@@ -43,6 +35,7 @@ export async function initLogger(): Promise<void> {
|
|
|
43
35
|
const cutoff = Date.now() - retentionMs;
|
|
44
36
|
|
|
45
37
|
try {
|
|
38
|
+
await mkdir(dir, { recursive: true });
|
|
46
39
|
const entries = await readdir(dir);
|
|
47
40
|
for (const entry of entries) {
|
|
48
41
|
if (!entry.endsWith(".jsonl")) continue;
|
package/src/proxy/store.ts
CHANGED
|
@@ -5,14 +5,7 @@ import { createInterface } from "node:readline";
|
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { appendLogEntry, resolveLogDir, logger } from "./logger";
|
|
8
|
-
import {
|
|
9
|
-
addToIndex,
|
|
10
|
-
findInIndex,
|
|
11
|
-
getNextLogId,
|
|
12
|
-
getCurrentLogFile,
|
|
13
|
-
saveIndex,
|
|
14
|
-
loadIndex,
|
|
15
|
-
} from "./logIndex";
|
|
8
|
+
import { addToIndex, findInIndex, getNextLogId, getCurrentLogFile } from "./logIndex";
|
|
16
9
|
import { writeChunks } from "./chunkStorage";
|
|
17
10
|
import type { CapturedLog } from "./schemas";
|
|
18
11
|
import { CapturedLogSchema } from "./schemas";
|
|
@@ -67,17 +60,11 @@ function removeFromCache(id: number): void {
|
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
/**
|
|
70
|
-
* Add a test log entry
|
|
71
|
-
* This is used by the provider test endpoint
|
|
63
|
+
* Add a test log entry to the in-memory store and persistent log file.
|
|
64
|
+
* This is used by the provider test endpoint to seed the provider-test session.
|
|
72
65
|
*/
|
|
73
66
|
export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<CapturedLog> {
|
|
74
67
|
const id = await getNextLogId();
|
|
75
|
-
// Update the index with the new maxId so subsequent calls get unique IDs
|
|
76
|
-
const index = await loadIndex();
|
|
77
|
-
if (id > index.maxId) {
|
|
78
|
-
index.maxId = id;
|
|
79
|
-
await saveIndex(index);
|
|
80
|
-
}
|
|
81
68
|
|
|
82
69
|
// Persist streaming chunks to disk if present
|
|
83
70
|
let streamingChunksPath: string | null = null;
|
|
@@ -100,6 +87,11 @@ export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<C
|
|
|
100
87
|
sessionId: session.id,
|
|
101
88
|
streamingChunksPath,
|
|
102
89
|
};
|
|
90
|
+
|
|
91
|
+
const logFile = getCurrentLogFile();
|
|
92
|
+
appendLogEntry(log);
|
|
93
|
+
await addToIndex(id, logFile, -1, -1);
|
|
94
|
+
|
|
103
95
|
addToCache(log);
|
|
104
96
|
observeSessionLog(log, session.source);
|
|
105
97
|
emitLogUpdate(log);
|