@tonyclaw/agent-inspector 3.1.8 → 3.1.10
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/backend/nitro.json +1 -1
- package/.output/cli.js +14 -4
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-CHcOqlej.mjs → router-sfMEKM3b.mjs} +270 -3
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-D6p2D3aR.js → CompareDrawer-dLJBRd6W.js} +1 -1
- package/.output/ui/assets/{InspectorPet-DteVBsK4.js → InspectorPet-DOP6QLyU.js} +1 -1
- package/.output/ui/assets/ProxyViewerContainer-BR0cxniH.js +59 -0
- package/.output/ui/assets/{ReplayDialog-BxfD3Ni8.js → ReplayDialog-Bfdzueeg.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-Bc-HEy3u.js → RequestAnatomy-CvVFHUCZ.js} +1 -1
- package/.output/ui/assets/ResponseView-BOeVZfR4.js +2 -0
- package/.output/ui/assets/StreamingChunkSequence-BX4KDLqt.js +1 -0
- package/.output/ui/assets/{_sessionId-Dm2fmwl6.js → _sessionId-BD6Scv6x.js} +1 -1
- package/.output/ui/assets/{_sessionId-MXVCdKA6.js → _sessionId-BlQOMPaG.js} +1 -1
- package/.output/ui/assets/{index-r-7cZ1UL.js → index-BwV0b3yu.js} +2 -2
- package/.output/ui/assets/{index-aah_H3hN.js → index-CVlN4MFT.js} +1 -1
- package/.output/ui/assets/index-IBEDJoV_.css +1 -0
- package/.output/ui/assets/{index-BuF4rMwu.js → index-UvtnFHf_.js} +1 -1
- package/.output/ui/assets/{index-Dr_GO75d.js → index-gUFWZr2M.js} +1 -1
- package/.output/ui/assets/{json-viewer-Dikr00zS.js → json-viewer-BLx7UxTA.js} +1 -1
- package/.output/ui/assets/{jszip.min-zWpW-aD7.js → jszip.min-BvxDqjej.js} +1 -1
- package/.output/ui/index.html +2 -2
- package/.output/workers/logFinalizer.worker.js +10 -0
- package/.output/workers/sessionWorkerEntry.js +10 -0
- package/package.json +4 -4
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +37 -7
- package/src/components/proxy-viewer/LogEntry.tsx +61 -11
- package/src/components/proxy-viewer/ResponseView.tsx +33 -3
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +60 -3
- package/src/components/proxy-viewer/viewerState.ts +11 -169
- package/src/contracts/index.ts +2 -0
- package/src/contracts/log.ts +13 -0
- package/src/lib/toolTrace.ts +290 -0
- package/src/proxy/logIndex.ts +4 -0
- package/src/proxy/schemas.ts +2 -0
- package/src/proxy/sqliteLogIndex.ts +24 -2
- package/src/proxy/store.ts +3 -0
- package/.output/ui/assets/ProxyViewerContainer-Dw723CdA.js +0 -59
- package/.output/ui/assets/ResponseView-Be5xdsIm.js +0 -2
- package/.output/ui/assets/StreamingChunkSequence-Cd5G2xRQ.js +0 -1
- package/.output/ui/assets/index-BvXp42al.css +0 -1
|
@@ -29,6 +29,10 @@ const StreamingChunksResponseSchema = z.object({
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
const CHUNK_FETCH_TIMEOUT_MS = 10_000;
|
|
32
|
+
const INITIAL_VISIBLE_GROUPS = 80;
|
|
33
|
+
const GROUP_PAGE_SIZE = 80;
|
|
34
|
+
const INITIAL_VISIBLE_CHUNKS_PER_GROUP = 40;
|
|
35
|
+
const CHUNK_PAGE_SIZE = 40;
|
|
32
36
|
|
|
33
37
|
// eslint-disable-next-line @typescript-eslint/no-shadow -- named function gives React DevTools a readable displayName
|
|
34
38
|
export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
@@ -38,8 +42,18 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
|
38
42
|
const [containerExpanded, setContainerExpanded] = useState(false);
|
|
39
43
|
const [chunkState, setChunkState] = useState<ChunkState>({ status: "idle" });
|
|
40
44
|
const [expandedIndices, setExpandedIndices] = useState<Set<number>>(new Set());
|
|
45
|
+
const [visibleGroupCount, setVisibleGroupCount] = useState(INITIAL_VISIBLE_GROUPS);
|
|
46
|
+
const [visibleChunksByGroup, setVisibleChunksByGroup] = useState<Map<number, number>>(new Map());
|
|
41
47
|
const loadedLogIdRef = useRef<number | null>(null);
|
|
42
48
|
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setExpandedIndices(new Set());
|
|
51
|
+
setVisibleGroupCount(INITIAL_VISIBLE_GROUPS);
|
|
52
|
+
setVisibleChunksByGroup(new Map());
|
|
53
|
+
setChunkState({ status: "idle" });
|
|
54
|
+
loadedLogIdRef.current = null;
|
|
55
|
+
}, [logId]);
|
|
56
|
+
|
|
43
57
|
useEffect(() => {
|
|
44
58
|
if (!containerExpanded) return;
|
|
45
59
|
if (loadedLogIdRef.current === logId) return;
|
|
@@ -90,6 +104,9 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
|
90
104
|
.sort((a, b) => a.index - b.index);
|
|
91
105
|
}, [chunkState]);
|
|
92
106
|
|
|
107
|
+
const visibleGroups = groups.slice(0, visibleGroupCount);
|
|
108
|
+
const hiddenGroupCount = Math.max(groups.length - visibleGroups.length, 0);
|
|
109
|
+
|
|
93
110
|
const toggleIndex = (index: number) => {
|
|
94
111
|
setExpandedIndices((prev) => {
|
|
95
112
|
const next = new Set(prev);
|
|
@@ -102,6 +119,19 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
|
102
119
|
});
|
|
103
120
|
};
|
|
104
121
|
|
|
122
|
+
const showMoreGroups = () => {
|
|
123
|
+
setVisibleGroupCount((count) => Math.min(count + GROUP_PAGE_SIZE, groups.length));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const showMoreChunks = (group: ChunkGroup) => {
|
|
127
|
+
setVisibleChunksByGroup((prev) => {
|
|
128
|
+
const next = new Map(prev);
|
|
129
|
+
const current = next.get(group.index) ?? INITIAL_VISIBLE_CHUNKS_PER_GROUP;
|
|
130
|
+
next.set(group.index, Math.min(current + CHUNK_PAGE_SIZE, group.chunks.length));
|
|
131
|
+
return next;
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
105
135
|
function renderBody(): JSX.Element {
|
|
106
136
|
if (chunkState.status === "idle" || chunkState.status === "loading") {
|
|
107
137
|
return (
|
|
@@ -121,8 +151,12 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
|
121
151
|
<div className="text-[10px] text-muted-foreground font-mono mb-2">
|
|
122
152
|
{groups.length} index group{groups.length !== 1 ? "s" : ""} available
|
|
123
153
|
</div>
|
|
124
|
-
{
|
|
154
|
+
{visibleGroups.map((group) => {
|
|
125
155
|
const isExpanded = expandedIndices.has(group.index);
|
|
156
|
+
const visibleChunkCount =
|
|
157
|
+
visibleChunksByGroup.get(group.index) ?? INITIAL_VISIBLE_CHUNKS_PER_GROUP;
|
|
158
|
+
const visibleChunks = group.chunks.slice(0, visibleChunkCount);
|
|
159
|
+
const hiddenChunkCount = Math.max(group.chunks.length - visibleChunks.length, 0);
|
|
126
160
|
return (
|
|
127
161
|
<div key={group.index} className="rounded border border-border bg-background">
|
|
128
162
|
<button
|
|
@@ -144,8 +178,11 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
|
144
178
|
</button>
|
|
145
179
|
{isExpanded && (
|
|
146
180
|
<div className="px-2 pb-2 space-y-1">
|
|
147
|
-
{
|
|
148
|
-
<div
|
|
181
|
+
{visibleChunks.map((chunk, chunkOrdinal) => (
|
|
182
|
+
<div
|
|
183
|
+
key={`${chunk.index}:${chunk.timestamp}:${chunk.type}:${chunkOrdinal}`}
|
|
184
|
+
className="rounded border border-border bg-muted/20 p-2"
|
|
185
|
+
>
|
|
149
186
|
<div className="flex items-center gap-2 mb-1">
|
|
150
187
|
<span className="text-[10px] text-muted-foreground font-mono">
|
|
151
188
|
+{chunk.timestamp}ms
|
|
@@ -163,11 +200,31 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
|
163
200
|
</Suspense>
|
|
164
201
|
</div>
|
|
165
202
|
))}
|
|
203
|
+
{hiddenChunkCount > 0 && (
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
className="mt-1 rounded-md border border-border bg-background px-2 py-1 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
207
|
+
onClick={() => showMoreChunks(group)}
|
|
208
|
+
>
|
|
209
|
+
Show {Math.min(CHUNK_PAGE_SIZE, hiddenChunkCount)} more chunk
|
|
210
|
+
{Math.min(CHUNK_PAGE_SIZE, hiddenChunkCount) !== 1 ? "s" : ""}
|
|
211
|
+
</button>
|
|
212
|
+
)}
|
|
166
213
|
</div>
|
|
167
214
|
)}
|
|
168
215
|
</div>
|
|
169
216
|
);
|
|
170
217
|
})}
|
|
218
|
+
{hiddenGroupCount > 0 && (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
className="rounded-md border border-border bg-background px-2 py-1 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
222
|
+
onClick={showMoreGroups}
|
|
223
|
+
>
|
|
224
|
+
Show {Math.min(GROUP_PAGE_SIZE, hiddenGroupCount)} more SSE group
|
|
225
|
+
{Math.min(GROUP_PAGE_SIZE, hiddenGroupCount) !== 1 ? "s" : ""}
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
171
228
|
</div>
|
|
172
229
|
);
|
|
173
230
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { extractStopReason, isTurnBoundary, type StopReason } from "../../lib/stopReason";
|
|
2
|
-
import { safeGetOwnProperty } from "../../lib/objectUtils";
|
|
3
2
|
import type { CapturedLog } from "../../contracts";
|
|
3
|
+
import {
|
|
4
|
+
extractToolTraceEvents as extractSharedToolTraceEvents,
|
|
5
|
+
type ToolTraceEvent,
|
|
6
|
+
} from "../../lib/toolTrace";
|
|
4
7
|
import { resolveLogFormat } from "./log-formats";
|
|
5
8
|
|
|
9
|
+
export type { ToolTraceEvent };
|
|
10
|
+
|
|
6
11
|
export type TurnEntry = {
|
|
7
12
|
log: CapturedLog;
|
|
8
13
|
stopReason: StopReason;
|
|
@@ -35,39 +40,14 @@ export type TraceSummary = {
|
|
|
35
40
|
knowledgeCandidateCount: number;
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
export type ToolTraceEvent = {
|
|
39
|
-
id: string;
|
|
40
|
-
logId: number;
|
|
41
|
-
index: number;
|
|
42
|
-
provider: "anthropic" | "openai";
|
|
43
|
-
name: string;
|
|
44
|
-
argumentsText: string | null;
|
|
45
|
-
argumentsPreview: string | null;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
43
|
type ToolTraceCacheEntry = {
|
|
49
44
|
apiFormat: CapturedLog["apiFormat"];
|
|
50
45
|
responseText: string | null;
|
|
46
|
+
toolTraceEvents: CapturedLog["toolTraceEvents"];
|
|
51
47
|
events: ToolTraceEvent[];
|
|
52
48
|
};
|
|
53
49
|
|
|
54
|
-
const PREVIEW_LIMIT = 180;
|
|
55
50
|
const toolTraceCache = new WeakMap<CapturedLog, ToolTraceCacheEntry>();
|
|
56
|
-
type ResolvedLogFormat = ReturnType<typeof resolveLogFormat>;
|
|
57
|
-
|
|
58
|
-
function responseMayContainToolTrace(log: CapturedLog, format: ResolvedLogFormat): boolean {
|
|
59
|
-
const responseText = log.responseText;
|
|
60
|
-
if (responseText === null) return false;
|
|
61
|
-
|
|
62
|
-
switch (format) {
|
|
63
|
-
case "anthropic":
|
|
64
|
-
return responseText.includes("tool_use");
|
|
65
|
-
case "openai":
|
|
66
|
-
return responseText.includes("tool_calls") || responseText.includes("function_call");
|
|
67
|
-
case "unknown":
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
51
|
|
|
72
52
|
export function isTurnCollapsible(entryCount: number): boolean {
|
|
73
53
|
return entryCount > 1;
|
|
@@ -112,160 +92,22 @@ export function buildValidPredecessors(groups: ConversationLike[]): Map<number,
|
|
|
112
92
|
return predecessors;
|
|
113
93
|
}
|
|
114
94
|
|
|
115
|
-
function parseJsonResponse(responseText: string | null): unknown {
|
|
116
|
-
if (responseText === null) return null;
|
|
117
|
-
try {
|
|
118
|
-
const parsed: unknown = JSON.parse(responseText);
|
|
119
|
-
if (typeof parsed === "string") {
|
|
120
|
-
return JSON.parse(parsed);
|
|
121
|
-
}
|
|
122
|
-
return parsed;
|
|
123
|
-
} catch {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function previewValue(value: unknown): string | null {
|
|
129
|
-
if (value === undefined || value === null) return null;
|
|
130
|
-
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
131
|
-
if (raw === undefined) return null;
|
|
132
|
-
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
133
|
-
if (normalized.length === 0) return null;
|
|
134
|
-
return normalized.length > PREVIEW_LIMIT
|
|
135
|
-
? `${normalized.slice(0, PREVIEW_LIMIT - 1)}...`
|
|
136
|
-
: normalized;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function copyValue(value: unknown): string | null {
|
|
140
|
-
if (value === undefined || value === null) return null;
|
|
141
|
-
if (typeof value === "string") return value.length > 0 ? value : null;
|
|
142
|
-
const raw = JSON.stringify(value, null, 2);
|
|
143
|
-
return raw === undefined || raw.length === 0 ? null : raw;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
147
|
-
const parsed = parseJsonResponse(log.responseText);
|
|
148
|
-
const content = safeGetOwnProperty(parsed, "content");
|
|
149
|
-
if (!Array.isArray(content)) return [];
|
|
150
|
-
|
|
151
|
-
const events: ToolTraceEvent[] = [];
|
|
152
|
-
for (const block of content) {
|
|
153
|
-
const type = safeGetOwnProperty(block, "type");
|
|
154
|
-
if (type !== "tool_use") continue;
|
|
155
|
-
const name = safeGetOwnProperty(block, "name");
|
|
156
|
-
if (typeof name !== "string" || name.length === 0) continue;
|
|
157
|
-
const input = safeGetOwnProperty(block, "input");
|
|
158
|
-
events.push({
|
|
159
|
-
id: `${String(log.id)}-anthropic-tool-${String(events.length)}`,
|
|
160
|
-
logId: log.id,
|
|
161
|
-
index: events.length,
|
|
162
|
-
provider: "anthropic",
|
|
163
|
-
name,
|
|
164
|
-
argumentsText: copyValue(input),
|
|
165
|
-
argumentsPreview: previewValue(input),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return events;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
172
|
-
const parsed = parseJsonResponse(log.responseText);
|
|
173
|
-
const choices = safeGetOwnProperty(parsed, "choices");
|
|
174
|
-
if (!Array.isArray(choices)) return [];
|
|
175
|
-
|
|
176
|
-
const events: ToolTraceEvent[] = [];
|
|
177
|
-
for (const choice of choices) {
|
|
178
|
-
const message = safeGetOwnProperty(choice, "message");
|
|
179
|
-
const toolCalls = safeGetOwnProperty(message, "tool_calls");
|
|
180
|
-
if (Array.isArray(toolCalls)) {
|
|
181
|
-
for (const call of toolCalls) {
|
|
182
|
-
const fn = safeGetOwnProperty(call, "function");
|
|
183
|
-
const name = safeGetOwnProperty(fn, "name");
|
|
184
|
-
if (typeof name !== "string" || name.length === 0) continue;
|
|
185
|
-
const args = safeGetOwnProperty(fn, "arguments");
|
|
186
|
-
events.push({
|
|
187
|
-
id: `${String(log.id)}-openai-tool-${String(events.length)}`,
|
|
188
|
-
logId: log.id,
|
|
189
|
-
index: events.length,
|
|
190
|
-
provider: "openai",
|
|
191
|
-
name,
|
|
192
|
-
argumentsText: copyValue(args),
|
|
193
|
-
argumentsPreview: previewValue(args),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const functionCall = safeGetOwnProperty(message, "function_call");
|
|
198
|
-
const functionCallName = safeGetOwnProperty(functionCall, "name");
|
|
199
|
-
if (typeof functionCallName === "string" && functionCallName.length > 0) {
|
|
200
|
-
const args = safeGetOwnProperty(functionCall, "arguments");
|
|
201
|
-
events.push({
|
|
202
|
-
id: `${String(log.id)}-openai-tool-${String(events.length)}`,
|
|
203
|
-
logId: log.id,
|
|
204
|
-
index: events.length,
|
|
205
|
-
provider: "openai",
|
|
206
|
-
name: functionCallName,
|
|
207
|
-
argumentsText: copyValue(args),
|
|
208
|
-
argumentsPreview: previewValue(args),
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const output = safeGetOwnProperty(parsed, "output");
|
|
213
|
-
if (Array.isArray(output)) {
|
|
214
|
-
for (const item of output) {
|
|
215
|
-
if (safeGetOwnProperty(item, "type") !== "function_call") continue;
|
|
216
|
-
const name = safeGetOwnProperty(item, "name");
|
|
217
|
-
if (typeof name !== "string" || name.length === 0) continue;
|
|
218
|
-
const args = safeGetOwnProperty(item, "arguments");
|
|
219
|
-
events.push({
|
|
220
|
-
id: `${String(log.id)}-openai-tool-${String(events.length)}`,
|
|
221
|
-
logId: log.id,
|
|
222
|
-
index: events.length,
|
|
223
|
-
provider: "openai",
|
|
224
|
-
name,
|
|
225
|
-
argumentsText: copyValue(args),
|
|
226
|
-
argumentsPreview: previewValue(args),
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return events;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
95
|
export function extractToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
234
96
|
const cached = toolTraceCache.get(log);
|
|
235
97
|
if (
|
|
236
98
|
cached !== undefined &&
|
|
237
99
|
cached.apiFormat === log.apiFormat &&
|
|
238
|
-
cached.responseText === log.responseText
|
|
100
|
+
cached.responseText === log.responseText &&
|
|
101
|
+
cached.toolTraceEvents === log.toolTraceEvents
|
|
239
102
|
) {
|
|
240
103
|
return cached.events;
|
|
241
104
|
}
|
|
242
105
|
|
|
243
|
-
const
|
|
244
|
-
if (!responseMayContainToolTrace(log, format)) {
|
|
245
|
-
const events: ToolTraceEvent[] = [];
|
|
246
|
-
toolTraceCache.set(log, {
|
|
247
|
-
apiFormat: log.apiFormat,
|
|
248
|
-
responseText: log.responseText,
|
|
249
|
-
events,
|
|
250
|
-
});
|
|
251
|
-
return events;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
let events: ToolTraceEvent[];
|
|
255
|
-
switch (format) {
|
|
256
|
-
case "anthropic":
|
|
257
|
-
events = extractAnthropicToolTraceEvents(log);
|
|
258
|
-
break;
|
|
259
|
-
case "openai":
|
|
260
|
-
events = extractOpenAIToolTraceEvents(log);
|
|
261
|
-
break;
|
|
262
|
-
case "unknown":
|
|
263
|
-
events = [];
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
106
|
+
const events = extractSharedToolTraceEvents(log);
|
|
266
107
|
toolTraceCache.set(log, {
|
|
267
108
|
apiFormat: log.apiFormat,
|
|
268
109
|
responseText: log.responseText,
|
|
110
|
+
toolTraceEvents: log.toolTraceEvents,
|
|
269
111
|
events,
|
|
270
112
|
});
|
|
271
113
|
return events;
|
package/src/contracts/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
LogBodyPartSchema,
|
|
10
10
|
parseLogId,
|
|
11
11
|
StreamingChunkSchema,
|
|
12
|
+
ToolTraceEventSchema,
|
|
12
13
|
} from "./log";
|
|
13
14
|
export type {
|
|
14
15
|
ApiFormat,
|
|
@@ -18,6 +19,7 @@ export type {
|
|
|
18
19
|
LogId,
|
|
19
20
|
LogBodyPart,
|
|
20
21
|
StreamingChunk,
|
|
22
|
+
ToolTraceEvent,
|
|
21
23
|
TokenUsage,
|
|
22
24
|
} from "./log";
|
|
23
25
|
|
package/src/contracts/log.ts
CHANGED
|
@@ -42,6 +42,18 @@ export const StreamingChunkSchema = z.object({
|
|
|
42
42
|
|
|
43
43
|
export type StreamingChunk = z.infer<typeof StreamingChunkSchema>;
|
|
44
44
|
|
|
45
|
+
export const ToolTraceEventSchema = z.object({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
logId: LogIdSchema,
|
|
48
|
+
index: z.number().int().nonnegative(),
|
|
49
|
+
provider: z.enum(["anthropic", "openai"]),
|
|
50
|
+
name: z.string(),
|
|
51
|
+
argumentsText: z.string().nullable(),
|
|
52
|
+
argumentsPreview: z.string().nullable(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type ToolTraceEvent = z.infer<typeof ToolTraceEventSchema>;
|
|
56
|
+
|
|
45
57
|
const StreamingChunksArraySchema = z.object({
|
|
46
58
|
chunks: z.array(StreamingChunkSchema),
|
|
47
59
|
truncated: z.boolean().optional().default(false),
|
|
@@ -107,6 +119,7 @@ export const CapturedLogSchema = z.object({
|
|
|
107
119
|
droppedBytes: z.number().int().nonnegative().optional(),
|
|
108
120
|
captureIncompleteReason: CaptureIncompleteReasonSchema.nullable().optional(),
|
|
109
121
|
warnings: z.array(z.string()).optional(),
|
|
122
|
+
toolTraceEvents: z.array(ToolTraceEventSchema).optional(),
|
|
110
123
|
/** Error message from streaming response (e.g., SSE error event) */
|
|
111
124
|
error: z.string().nullable().optional(),
|
|
112
125
|
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { ApiFormat, CapturedLog, StreamingChunk, ToolTraceEvent } from "../contracts";
|
|
2
|
+
import { apiFormatForPath } from "./apiFormat";
|
|
3
|
+
import { safeGetOwnProperty } from "./objectUtils";
|
|
4
|
+
|
|
5
|
+
export type { ToolTraceEvent };
|
|
6
|
+
|
|
7
|
+
const PREVIEW_LIMIT = 180;
|
|
8
|
+
|
|
9
|
+
type ResolvedLogFormat = ApiFormat;
|
|
10
|
+
|
|
11
|
+
function resolveLogFormat(log: Pick<CapturedLog, "path" | "apiFormat">): ResolvedLogFormat {
|
|
12
|
+
const pathFormat = apiFormatForPath(log.path);
|
|
13
|
+
return pathFormat === "unknown" ? log.apiFormat : pathFormat;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function responseMayContainToolTrace(log: CapturedLog, format: ResolvedLogFormat): boolean {
|
|
17
|
+
const responseText = log.responseText;
|
|
18
|
+
if (responseText === null) return false;
|
|
19
|
+
|
|
20
|
+
switch (format) {
|
|
21
|
+
case "anthropic":
|
|
22
|
+
return responseText.includes("tool_use");
|
|
23
|
+
case "openai":
|
|
24
|
+
return responseText.includes("tool_calls") || responseText.includes("function_call");
|
|
25
|
+
case "unknown":
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseJsonResponse(responseText: string | null): unknown {
|
|
31
|
+
if (responseText === null) return null;
|
|
32
|
+
try {
|
|
33
|
+
const parsed: unknown = JSON.parse(responseText);
|
|
34
|
+
if (typeof parsed === "string") {
|
|
35
|
+
return JSON.parse(parsed);
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function previewValue(value: unknown): string | null {
|
|
44
|
+
if (value === undefined || value === null) return null;
|
|
45
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
46
|
+
if (raw === undefined) return null;
|
|
47
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
48
|
+
if (normalized.length === 0) return null;
|
|
49
|
+
return normalized.length > PREVIEW_LIMIT
|
|
50
|
+
? `${normalized.slice(0, PREVIEW_LIMIT - 1)}...`
|
|
51
|
+
: normalized;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function copyValue(value: unknown): string | null {
|
|
55
|
+
if (value === undefined || value === null) return null;
|
|
56
|
+
if (typeof value === "string") return value.length > 0 ? value : null;
|
|
57
|
+
const raw = JSON.stringify(value, null, 2);
|
|
58
|
+
return raw === undefined || raw.length === 0 ? null : raw;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeEvent({
|
|
62
|
+
log,
|
|
63
|
+
provider,
|
|
64
|
+
index,
|
|
65
|
+
name,
|
|
66
|
+
input,
|
|
67
|
+
}: {
|
|
68
|
+
log: Pick<CapturedLog, "id">;
|
|
69
|
+
provider: "anthropic" | "openai";
|
|
70
|
+
index: number;
|
|
71
|
+
name: string;
|
|
72
|
+
input: unknown;
|
|
73
|
+
}): ToolTraceEvent {
|
|
74
|
+
return {
|
|
75
|
+
id: `${String(log.id)}-${provider}-tool-${String(index)}`,
|
|
76
|
+
logId: log.id,
|
|
77
|
+
index,
|
|
78
|
+
provider,
|
|
79
|
+
name,
|
|
80
|
+
argumentsText: copyValue(input),
|
|
81
|
+
argumentsPreview: previewValue(input),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractAnthropicResponseToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
86
|
+
const parsed = parseJsonResponse(log.responseText);
|
|
87
|
+
const content = safeGetOwnProperty(parsed, "content");
|
|
88
|
+
if (!Array.isArray(content)) return [];
|
|
89
|
+
|
|
90
|
+
const events: ToolTraceEvent[] = [];
|
|
91
|
+
for (const block of content) {
|
|
92
|
+
const type = safeGetOwnProperty(block, "type");
|
|
93
|
+
if (type !== "tool_use") continue;
|
|
94
|
+
const name = safeGetOwnProperty(block, "name");
|
|
95
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
96
|
+
const input = safeGetOwnProperty(block, "input");
|
|
97
|
+
events.push(makeEvent({ log, provider: "anthropic", index: events.length, name, input }));
|
|
98
|
+
}
|
|
99
|
+
return events;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractOpenAIResponseToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
103
|
+
const parsed = parseJsonResponse(log.responseText);
|
|
104
|
+
const choices = safeGetOwnProperty(parsed, "choices");
|
|
105
|
+
const events: ToolTraceEvent[] = [];
|
|
106
|
+
|
|
107
|
+
if (Array.isArray(choices)) {
|
|
108
|
+
for (const choice of choices) {
|
|
109
|
+
const message = safeGetOwnProperty(choice, "message");
|
|
110
|
+
const toolCalls = safeGetOwnProperty(message, "tool_calls");
|
|
111
|
+
if (Array.isArray(toolCalls)) {
|
|
112
|
+
for (const call of toolCalls) {
|
|
113
|
+
const fn = safeGetOwnProperty(call, "function");
|
|
114
|
+
const name = safeGetOwnProperty(fn, "name");
|
|
115
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
116
|
+
const args = safeGetOwnProperty(fn, "arguments");
|
|
117
|
+
events.push(
|
|
118
|
+
makeEvent({ log, provider: "openai", index: events.length, name, input: args }),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const functionCall = safeGetOwnProperty(message, "function_call");
|
|
123
|
+
const functionCallName = safeGetOwnProperty(functionCall, "name");
|
|
124
|
+
if (typeof functionCallName === "string" && functionCallName.length > 0) {
|
|
125
|
+
const args = safeGetOwnProperty(functionCall, "arguments");
|
|
126
|
+
events.push(
|
|
127
|
+
makeEvent({
|
|
128
|
+
log,
|
|
129
|
+
provider: "openai",
|
|
130
|
+
index: events.length,
|
|
131
|
+
name: functionCallName,
|
|
132
|
+
input: args,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const output = safeGetOwnProperty(parsed, "output");
|
|
140
|
+
if (Array.isArray(output)) {
|
|
141
|
+
for (const item of output) {
|
|
142
|
+
if (safeGetOwnProperty(item, "type") !== "function_call") continue;
|
|
143
|
+
const name = safeGetOwnProperty(item, "name");
|
|
144
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
145
|
+
const args = safeGetOwnProperty(item, "arguments");
|
|
146
|
+
events.push(makeEvent({ log, provider: "openai", index: events.length, name, input: args }));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return events;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function chunkData(chunk: StreamingChunk): unknown {
|
|
153
|
+
if (typeof chunk.data === "string") {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(chunk.data);
|
|
156
|
+
} catch {
|
|
157
|
+
return chunk.data;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return chunk.data;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function appendUniqueEvent(
|
|
164
|
+
events: ToolTraceEvent[],
|
|
165
|
+
seen: Set<string>,
|
|
166
|
+
event: ToolTraceEvent,
|
|
167
|
+
): void {
|
|
168
|
+
const key = `${event.provider}:${event.name}:${event.index}`;
|
|
169
|
+
if (seen.has(key)) return;
|
|
170
|
+
seen.add(key);
|
|
171
|
+
events.push(event);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function extractAnthropicChunkToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
175
|
+
const chunks = log.streamingChunks?.chunks ?? [];
|
|
176
|
+
const events: ToolTraceEvent[] = [];
|
|
177
|
+
const seen = new Set<string>();
|
|
178
|
+
|
|
179
|
+
for (const chunk of chunks) {
|
|
180
|
+
const data = chunkData(chunk);
|
|
181
|
+
const contentBlock = safeGetOwnProperty(data, "content_block");
|
|
182
|
+
if (safeGetOwnProperty(contentBlock, "type") !== "tool_use") continue;
|
|
183
|
+
const name = safeGetOwnProperty(contentBlock, "name");
|
|
184
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
185
|
+
const input = safeGetOwnProperty(contentBlock, "input");
|
|
186
|
+
appendUniqueEvent(
|
|
187
|
+
events,
|
|
188
|
+
seen,
|
|
189
|
+
makeEvent({ log, provider: "anthropic", index: chunk.index, name, input }),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return events.map((event, index) => ({
|
|
194
|
+
...event,
|
|
195
|
+
id: `${String(log.id)}-anthropic-tool-${String(index)}`,
|
|
196
|
+
index,
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function extractOpenAIChunkToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
201
|
+
const chunks = log.streamingChunks?.chunks ?? [];
|
|
202
|
+
const events: ToolTraceEvent[] = [];
|
|
203
|
+
const seen = new Set<string>();
|
|
204
|
+
|
|
205
|
+
for (const chunk of chunks) {
|
|
206
|
+
const data = chunkData(chunk);
|
|
207
|
+
const choices = safeGetOwnProperty(data, "choices");
|
|
208
|
+
if (Array.isArray(choices)) {
|
|
209
|
+
for (const choice of choices) {
|
|
210
|
+
const delta = safeGetOwnProperty(choice, "delta");
|
|
211
|
+
const toolCalls = safeGetOwnProperty(delta, "tool_calls");
|
|
212
|
+
if (Array.isArray(toolCalls)) {
|
|
213
|
+
for (const call of toolCalls) {
|
|
214
|
+
const fn = safeGetOwnProperty(call, "function");
|
|
215
|
+
const name = safeGetOwnProperty(fn, "name");
|
|
216
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
217
|
+
const args = safeGetOwnProperty(fn, "arguments");
|
|
218
|
+
appendUniqueEvent(
|
|
219
|
+
events,
|
|
220
|
+
seen,
|
|
221
|
+
makeEvent({ log, provider: "openai", index: events.length, name, input: args }),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const functionCall = safeGetOwnProperty(delta, "function_call");
|
|
226
|
+
const functionCallName = safeGetOwnProperty(functionCall, "name");
|
|
227
|
+
if (typeof functionCallName === "string" && functionCallName.length > 0) {
|
|
228
|
+
const args = safeGetOwnProperty(functionCall, "arguments");
|
|
229
|
+
appendUniqueEvent(
|
|
230
|
+
events,
|
|
231
|
+
seen,
|
|
232
|
+
makeEvent({
|
|
233
|
+
log,
|
|
234
|
+
provider: "openai",
|
|
235
|
+
index: events.length,
|
|
236
|
+
name: functionCallName,
|
|
237
|
+
input: args,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const item = safeGetOwnProperty(data, "item");
|
|
245
|
+
if (safeGetOwnProperty(item, "type") === "function_call") {
|
|
246
|
+
const name = safeGetOwnProperty(item, "name");
|
|
247
|
+
if (typeof name === "string" && name.length > 0) {
|
|
248
|
+
const args = safeGetOwnProperty(item, "arguments");
|
|
249
|
+
appendUniqueEvent(
|
|
250
|
+
events,
|
|
251
|
+
seen,
|
|
252
|
+
makeEvent({ log, provider: "openai", index: events.length, name, input: args }),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return events.map((event, index) => ({
|
|
259
|
+
...event,
|
|
260
|
+
id: `${String(log.id)}-openai-tool-${String(index)}`,
|
|
261
|
+
index,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function extractToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
266
|
+
if (log.toolTraceEvents !== undefined && log.toolTraceEvents.length > 0) {
|
|
267
|
+
return log.toolTraceEvents;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const format = resolveLogFormat(log);
|
|
271
|
+
if (responseMayContainToolTrace(log, format)) {
|
|
272
|
+
switch (format) {
|
|
273
|
+
case "anthropic":
|
|
274
|
+
return extractAnthropicResponseToolTraceEvents(log);
|
|
275
|
+
case "openai":
|
|
276
|
+
return extractOpenAIResponseToolTraceEvents(log);
|
|
277
|
+
case "unknown":
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
switch (format) {
|
|
283
|
+
case "anthropic":
|
|
284
|
+
return extractAnthropicChunkToolTraceEvents(log);
|
|
285
|
+
case "openai":
|
|
286
|
+
return extractOpenAIChunkToolTraceEvents(log);
|
|
287
|
+
case "unknown":
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
}
|
package/src/proxy/logIndex.ts
CHANGED
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
type ApiFormat,
|
|
10
10
|
type CaptureIncompleteReason,
|
|
11
11
|
type CapturedLog,
|
|
12
|
+
type ToolTraceEvent,
|
|
12
13
|
} from "./schemas";
|
|
13
14
|
import { textByteLength } from "../lib/unknown";
|
|
15
|
+
import { extractToolTraceEvents } from "../lib/toolTrace";
|
|
14
16
|
import {
|
|
15
17
|
getSqliteLogIndexMaxId,
|
|
16
18
|
replaceSqliteLogIndexEntries,
|
|
@@ -60,6 +62,7 @@ export type LogIndexSummary = {
|
|
|
60
62
|
droppedBytes: number;
|
|
61
63
|
captureIncompleteReason: CaptureIncompleteReason | null;
|
|
62
64
|
warnings?: string[];
|
|
65
|
+
toolTraceEvents?: ToolTraceEvent[];
|
|
63
66
|
error: string | null;
|
|
64
67
|
};
|
|
65
68
|
|
|
@@ -138,6 +141,7 @@ export function createLogIndexEntryMetadata(log: CapturedLog): LogIndexEntryMeta
|
|
|
138
141
|
droppedBytes: log.droppedBytes ?? 0,
|
|
139
142
|
captureIncompleteReason: log.captureIncompleteReason ?? null,
|
|
140
143
|
warnings: log.warnings,
|
|
144
|
+
toolTraceEvents: extractToolTraceEvents(log),
|
|
141
145
|
error: log.error ?? null,
|
|
142
146
|
},
|
|
143
147
|
};
|