@tonyclaw/agent-inspector 3.1.7 → 3.1.9

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.
Files changed (41) hide show
  1. package/.output/backend/nitro.json +1 -1
  2. package/.output/cli.js +14 -4
  3. package/.output/server/_ssr/index.mjs +1 -1
  4. package/.output/server/_ssr/{router-6IijwoYv.mjs → router-Lc1aOs3g.mjs} +270 -3
  5. package/.output/server/index.mjs +1 -1
  6. package/.output/ui/assets/{CompareDrawer-CBqIDEv5.js → CompareDrawer-YdfnAMiC.js} +1 -1
  7. package/.output/ui/assets/{InspectorPet-Cx21mu-r.js → InspectorPet-Bt0vxGL8.js} +1 -1
  8. package/.output/ui/assets/ProxyViewerContainer-D4_GM20S.js +59 -0
  9. package/.output/ui/assets/{ReplayDialog-5h3GQEg_.js → ReplayDialog-9BWotxhQ.js} +1 -1
  10. package/.output/ui/assets/{RequestAnatomy-CAlwQcTB.js → RequestAnatomy-0VrsjZmK.js} +1 -1
  11. package/.output/ui/assets/ResponseView-mmW2Z9sl.js +2 -0
  12. package/.output/ui/assets/StreamingChunkSequence-ZC90PIBc.js +1 -0
  13. package/.output/ui/assets/{_sessionId-1G3bJYx7.js → _sessionId-C84un6eF.js} +1 -1
  14. package/.output/ui/assets/{_sessionId-BtmyX4Vb.js → _sessionId-CB7Eytuo.js} +1 -1
  15. package/.output/ui/assets/{index--SNPe17S.js → index-2feCKPew.js} +2 -2
  16. package/.output/ui/assets/{index-CtSb9B-7.js → index-7oLdSt17.js} +1 -1
  17. package/.output/ui/assets/{index-CfAo7qLu.js → index-BswExFrv.js} +1 -1
  18. package/.output/ui/assets/index-IBEDJoV_.css +1 -0
  19. package/.output/ui/assets/{index-DPCSM3Ig.js → index-YEgoJRUl.js} +1 -1
  20. package/.output/ui/assets/{json-viewer-BC3R5htL.js → json-viewer-CJ96a0P_.js} +1 -1
  21. package/.output/ui/assets/{jszip.min-DEdQuE_J.js → jszip.min-BDO1ePnu.js} +1 -1
  22. package/.output/ui/index.html +2 -2
  23. package/.output/workers/logFinalizer.worker.js +10 -0
  24. package/.output/workers/sessionWorkerEntry.js +10 -0
  25. package/package.json +4 -4
  26. package/src/components/proxy-viewer/AnswerMarkdown.tsx +37 -7
  27. package/src/components/proxy-viewer/LogEntry.tsx +82 -32
  28. package/src/components/proxy-viewer/ResponseView.tsx +33 -3
  29. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +60 -3
  30. package/src/components/proxy-viewer/viewerState.ts +11 -169
  31. package/src/contracts/index.ts +2 -0
  32. package/src/contracts/log.ts +13 -0
  33. package/src/lib/toolTrace.ts +290 -0
  34. package/src/proxy/logIndex.ts +4 -0
  35. package/src/proxy/schemas.ts +2 -0
  36. package/src/proxy/sqliteLogIndex.ts +24 -2
  37. package/src/proxy/store.ts +3 -0
  38. package/.output/ui/assets/ProxyViewerContainer-Btek1Lgk.js +0 -59
  39. package/.output/ui/assets/ResponseView-B0QKCR9M.js +0 -2
  40. package/.output/ui/assets/StreamingChunkSequence-DXbDSW9D.js +0 -1
  41. package/.output/ui/assets/index-BvXp42al.css +0 -1
@@ -1,6 +1,6 @@
1
1
  import { AlertTriangle, Zap } from "lucide-react";
2
2
  import type { JSX } from "react";
3
- import { memo } from "react";
3
+ import { memo, useEffect, useState } from "react";
4
4
  import { useMemo } from "react";
5
5
  import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../lib/utils";
6
6
  import type { ApiFormat } from "../../contracts";
@@ -66,6 +66,8 @@ function MarkdownFallbackView({ text }: { text: string }): JSX.Element {
66
66
  return <AnswerMarkdown text={text} />;
67
67
  }
68
68
 
69
+ const RESPONSE_AUTO_PARSE_MAX_CHARS = 1024 * 1024;
70
+
69
71
  // eslint-disable-next-line @typescript-eslint/no-shadow -- named function gives React DevTools a readable displayName
70
72
  export const ResponseView = memo(function ResponseView({
71
73
  responseText,
@@ -81,11 +83,23 @@ export const ResponseView = memo(function ResponseView({
81
83
  toolFocusNonce = 0,
82
84
  }: ResponseViewProps): JSX.Element {
83
85
  const resolvedFormat = apiFormat ?? "unknown";
86
+ const [parseLargeResponse, setParseLargeResponse] = useState(false);
87
+ const shouldDeferStructuredParse =
88
+ responseText !== null && responseText.length > RESPONSE_AUTO_PARSE_MAX_CHARS;
89
+ const responseTextForStructuredParse =
90
+ responseText === null || (shouldDeferStructuredParse && !parseLargeResponse)
91
+ ? null
92
+ : responseText;
84
93
  const parsed = useMemo(
85
- () => getLogFormatAdapter(resolvedFormat).analyzeResponse(responseText).parsed,
86
- [resolvedFormat, responseText],
94
+ () =>
95
+ getLogFormatAdapter(resolvedFormat).analyzeResponse(responseTextForStructuredParse).parsed,
96
+ [resolvedFormat, responseTextForStructuredParse],
87
97
  );
88
98
 
99
+ useEffect(() => {
100
+ setParseLargeResponse(false);
101
+ }, [responseText]);
102
+
89
103
  if (responseText === null && error === undefined) {
90
104
  return (
91
105
  <div className="flex items-center gap-2 py-3">
@@ -166,6 +180,22 @@ export const ResponseView = memo(function ResponseView({
166
180
  </span>
167
181
  )}
168
182
  </div>
183
+ {shouldDeferStructuredParse && !parseLargeResponse && (
184
+ <div className="rounded-md border border-amber-300/20 bg-amber-300/[0.06] px-3 py-2 text-xs text-muted-foreground">
185
+ <div className="mb-2">
186
+ This response is {responseText.length.toLocaleString()} characters, so structured
187
+ parsing is deferred to keep the UI responsive. Response and SSE remain separate; open
188
+ SSE for the event stream.
189
+ </div>
190
+ <button
191
+ type="button"
192
+ className="rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
193
+ onClick={() => setParseLargeResponse(true)}
194
+ >
195
+ Parse full structured response
196
+ </button>
197
+ </div>
198
+ )}
169
199
  <MarkdownFallbackView text={responseText ?? ""} />
170
200
  </div>
171
201
  );
@@ -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
- {groups.map((group) => {
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
- {group.chunks.map((chunk) => (
148
- <div key={chunk.index} className="rounded border border-border bg-muted/20 p-2">
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 format = resolveLogFormat(log);
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;
@@ -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
 
@@ -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
  });