@tonyclaw/agent-inspector 2.1.13 → 2.1.15
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-BcEd6V-V.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +106 -0
- package/.output/public/assets/ReplayDialog-BNpC0548.js +1 -0
- package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
- package/.output/public/assets/ResponseView-7KPVqKl5.js +3 -0
- package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BHQT261s.js} +1 -1
- package/.output/public/assets/_sessionId-DWePGjnS.js +1 -0
- package/.output/public/assets/index-CI1-G8ua.js +1 -0
- package/.output/public/assets/index-DdhFqPsI.css +1 -0
- package/.output/public/assets/index-DjKt8XKe.js +14 -0
- package/.output/public/assets/json-viewer-CkCu-rka.js +1 -0
- package/.output/public/assets/{main-Dtspb4Ui.js → main-DpD1N0S8.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +194 -200
- package/.output/server/_libs/react-markdown.mjs +90 -2
- package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-DF9Sy8cP.mjs} +6 -57
- package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-BoxztaO7.mjs} +50 -74
- package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +1221 -799
- package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-Cc1dyDuK.mjs} +18 -62
- package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-CMGSsz5Z.mjs} +6 -57
- package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-Cp10DM1D.mjs} +139 -61
- package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +13 -58
- package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-47XVPghS.mjs} +6 -57
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-zDE2rrmJ.mjs +478 -0
- package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-DVeuZFqI.mjs} +680 -174
- package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
- package/.output/server/index.mjs +72 -58
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +28 -2
- package/src/components/ProxyViewerContainer.tsx +25 -17
- package/src/components/clients/ClientLogo.tsx +132 -0
- package/src/components/groups/GroupsDialog.tsx +18 -13
- package/src/components/providers/ImportWizardDialog.tsx +7 -1
- package/src/components/providers/ProviderCard.tsx +10 -1
- package/src/components/providers/ProviderForm.tsx +157 -41
- package/src/components/providers/ProvidersPanel.tsx +5 -1
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +56 -23
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
- package/src/components/proxy-viewer/CompareDrawer.tsx +58 -14
- package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
- package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
- package/src/components/proxy-viewer/LogEntry.tsx +41 -12
- package/src/components/proxy-viewer/LogEntryHeader.tsx +122 -51
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
- package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
- package/src/components/proxy-viewer/TurnGroup.tsx +67 -32
- package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
- package/src/components/proxy-viewer/formats/index.tsx +10 -3
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
- package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
- package/src/components/proxy-viewer/logFocus.ts +15 -2
- package/src/components/proxy-viewer/requestTools.ts +7 -3
- package/src/components/proxy-viewer/viewerState.ts +77 -5
- package/src/components/ui/json-viewer-bulk.ts +41 -6
- package/src/components/ui/json-viewer.tsx +9 -8
- package/src/contracts/index.ts +15 -1
- package/src/contracts/openai.ts +94 -0
- package/src/lib/providerContract.ts +1 -0
- package/src/lib/providerModelMetadata.ts +7 -1
- package/src/lib/sessionInfoContract.ts +1 -0
- package/src/lib/stopReason.ts +59 -15
- package/src/lib/upstreamUrl.ts +46 -0
- package/src/mcp/previewExtractor.ts +73 -3
- package/src/mcp/server.ts +2 -0
- package/src/mcp/toolHandlers.ts +2 -0
- package/src/proxy/constants.ts +2 -0
- package/src/proxy/formats/openai/handler.ts +40 -9
- package/src/proxy/formats/openai/index.ts +7 -0
- package/src/proxy/formats/openai/schemas.ts +15 -1
- package/src/proxy/formats/openai/stream.ts +529 -135
- package/src/proxy/formats/registry.ts +9 -1
- package/src/proxy/providerImporters.ts +45 -5
- package/src/proxy/providers.ts +31 -11
- package/src/proxy/schemas.ts +6 -0
- package/src/proxy/sessionInfo.ts +5 -1
- package/src/proxy/toolSchemaWarnings.ts +25 -3
- package/src/proxy/upstream.ts +18 -37
- package/src/routes/api/providers.$providerId.ts +1 -0
- package/src/routes/api/providers.ts +2 -0
- package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
- package/.output/public/assets/ReplayDialog-sBA1KAYD.js +0 -1
- package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
- package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
- package/.output/public/assets/index-B-QQLbpz.js +0 -1
- package/.output/public/assets/index-CmtfjQPv.css +0 -1
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { StopCircle, Terminal, Zap } from "lucide-react";
|
|
2
|
-
import { memo, useState, type JSX } from "react";
|
|
3
|
-
import type {
|
|
2
|
+
import { memo, Suspense, useState, type JSX } from "react";
|
|
3
|
+
import type {
|
|
4
|
+
OpenAIResponse,
|
|
5
|
+
OpenAIResponsesResponse,
|
|
6
|
+
OpenAIToolCall,
|
|
7
|
+
} from "../../../../contracts/openai";
|
|
8
|
+
import { safeGetOwnProperty } from "../../../../lib/objectUtils";
|
|
4
9
|
import { formatTokens } from "../../../../lib/utils";
|
|
5
10
|
import { AnswerMarkdown } from "../../AnswerMarkdown";
|
|
6
11
|
import { Badge } from "../../../ui/badge";
|
|
7
|
-
import { JsonViewer } from "../../../ui/json-viewer";
|
|
8
12
|
import { safeJsonValue } from "../../../ui/json-viewer-bulk";
|
|
9
13
|
import { ScrollArea } from "../../../ui/scroll-area";
|
|
10
14
|
import { Separator } from "../../../ui/separator";
|
|
@@ -12,6 +16,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui
|
|
|
12
16
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
13
17
|
import { ThinkingBlock } from "../anthropic/ContentBlocks";
|
|
14
18
|
import { extractThinkingFromContent } from "../anthropic/thinkingExtract";
|
|
19
|
+
import { LazyJsonViewer } from "../../lazy";
|
|
15
20
|
|
|
16
21
|
// Re-export for use in other components
|
|
17
22
|
export { extractThinkingFromContent } from "../anthropic/thinkingExtract";
|
|
@@ -34,7 +39,7 @@ function parseToolArguments(raw: string | undefined): unknown {
|
|
|
34
39
|
function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
|
|
35
40
|
const [open, setOpen] = useState(false);
|
|
36
41
|
const name = call.function.name ?? "(unnamed tool)";
|
|
37
|
-
const parsed = parseToolArguments(call.function.arguments);
|
|
42
|
+
const parsed = open ? parseToolArguments(call.function.arguments) : {};
|
|
38
43
|
|
|
39
44
|
return (
|
|
40
45
|
<Collapsible open={open} onOpenChange={setOpen}>
|
|
@@ -57,19 +62,128 @@ function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
|
|
|
57
62
|
)}
|
|
58
63
|
</CollapsibleTrigger>
|
|
59
64
|
<CollapsibleContent>
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
{open && (
|
|
66
|
+
<div className="px-3 pb-2">
|
|
67
|
+
<ScrollArea className="max-h-[60vh]">
|
|
68
|
+
{parsed === null ? (
|
|
69
|
+
// JSON.parse failed — show the raw string so the user can
|
|
70
|
+
// still see what the model tried to call.
|
|
71
|
+
<pre className="font-mono text-xs whitespace-pre-wrap break-words text-rose-300/90">
|
|
72
|
+
{call.function.arguments}
|
|
73
|
+
</pre>
|
|
74
|
+
) : (
|
|
75
|
+
<Suspense
|
|
76
|
+
fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
|
|
77
|
+
>
|
|
78
|
+
<LazyJsonViewer data={safeJsonValue(parsed)} defaultExpandDepth={0} />
|
|
79
|
+
</Suspense>
|
|
80
|
+
)}
|
|
81
|
+
</ScrollArea>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</CollapsibleContent>
|
|
85
|
+
</div>
|
|
86
|
+
</Collapsible>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type ResponsesFunctionCall = {
|
|
91
|
+
id: string | null;
|
|
92
|
+
callId: string | null;
|
|
93
|
+
name: string;
|
|
94
|
+
argumentsText: string;
|
|
95
|
+
status: string | null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function extractResponsesOutputText(response: OpenAIResponsesResponse): string {
|
|
99
|
+
if (response.output_text !== undefined && response.output_text.length > 0) {
|
|
100
|
+
return response.output_text;
|
|
101
|
+
}
|
|
102
|
+
const parts: string[] = [];
|
|
103
|
+
for (const outputItem of response.output ?? []) {
|
|
104
|
+
if (safeGetOwnProperty(outputItem, "type") !== "message") continue;
|
|
105
|
+
const content = safeGetOwnProperty(outputItem, "content");
|
|
106
|
+
if (!Array.isArray(content)) continue;
|
|
107
|
+
for (const contentPart of content) {
|
|
108
|
+
const type = safeGetOwnProperty(contentPart, "type");
|
|
109
|
+
const text = safeGetOwnProperty(contentPart, "text");
|
|
110
|
+
if ((type === "output_text" || type === "text") && typeof text === "string") {
|
|
111
|
+
parts.push(text);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return parts.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractResponsesFunctionCalls(response: OpenAIResponsesResponse): ResponsesFunctionCall[] {
|
|
119
|
+
const calls: ResponsesFunctionCall[] = [];
|
|
120
|
+
for (const outputItem of response.output ?? []) {
|
|
121
|
+
if (safeGetOwnProperty(outputItem, "type") !== "function_call") continue;
|
|
122
|
+
const name = safeGetOwnProperty(outputItem, "name");
|
|
123
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
124
|
+
const id = safeGetOwnProperty(outputItem, "id");
|
|
125
|
+
const callId = safeGetOwnProperty(outputItem, "call_id");
|
|
126
|
+
const args = safeGetOwnProperty(outputItem, "arguments");
|
|
127
|
+
const status = safeGetOwnProperty(outputItem, "status");
|
|
128
|
+
calls.push({
|
|
129
|
+
id: typeof id === "string" && id.length > 0 ? id : null,
|
|
130
|
+
callId: typeof callId === "string" && callId.length > 0 ? callId : null,
|
|
131
|
+
name,
|
|
132
|
+
argumentsText: typeof args === "string" ? args : "",
|
|
133
|
+
status: typeof status === "string" && status.length > 0 ? status : null,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return calls;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function ResponsesFunctionCallBlock({ call }: { call: ResponsesFunctionCall }): JSX.Element {
|
|
140
|
+
const [open, setOpen] = useState(false);
|
|
141
|
+
const parsed = open ? parseToolArguments(call.argumentsText) : {};
|
|
142
|
+
const identity = call.callId ?? call.id;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Collapsible open={open} onOpenChange={setOpen}>
|
|
146
|
+
<div className="border-l-2 border-sky-400/25 my-1">
|
|
147
|
+
<CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group">
|
|
148
|
+
<Terminal className="size-3.5 text-sky-400/70 shrink-0" />
|
|
149
|
+
<Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
|
|
150
|
+
{call.name}
|
|
151
|
+
</Badge>
|
|
152
|
+
{identity !== null && (
|
|
153
|
+
<span className="text-[10px] font-mono text-muted-foreground/60 truncate">
|
|
154
|
+
{identity}
|
|
155
|
+
</span>
|
|
156
|
+
)}
|
|
157
|
+
{call.status !== null && (
|
|
158
|
+
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4 font-mono">
|
|
159
|
+
{call.status}
|
|
160
|
+
</Badge>
|
|
161
|
+
)}
|
|
162
|
+
<span className="flex-1" />
|
|
163
|
+
{open ? (
|
|
164
|
+
<ChevronDown className="size-3 text-muted-foreground" />
|
|
165
|
+
) : (
|
|
166
|
+
<ChevronRight className="size-3 text-muted-foreground" />
|
|
167
|
+
)}
|
|
168
|
+
</CollapsibleTrigger>
|
|
169
|
+
<CollapsibleContent>
|
|
170
|
+
{open && (
|
|
171
|
+
<div className="px-3 pb-2">
|
|
172
|
+
<ScrollArea className="max-h-[60vh]">
|
|
173
|
+
{parsed === null ? (
|
|
174
|
+
<pre className="font-mono text-xs whitespace-pre-wrap break-words text-rose-300/90">
|
|
175
|
+
{call.argumentsText}
|
|
176
|
+
</pre>
|
|
177
|
+
) : (
|
|
178
|
+
<Suspense
|
|
179
|
+
fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
|
|
180
|
+
>
|
|
181
|
+
<LazyJsonViewer data={safeJsonValue(parsed)} defaultExpandDepth={0} />
|
|
182
|
+
</Suspense>
|
|
183
|
+
)}
|
|
184
|
+
</ScrollArea>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
73
187
|
</CollapsibleContent>
|
|
74
188
|
</div>
|
|
75
189
|
</Collapsible>
|
|
@@ -164,3 +278,57 @@ export const OpenAIResponseView = memo(function OpenAIResponseView({
|
|
|
164
278
|
</div>
|
|
165
279
|
);
|
|
166
280
|
});
|
|
281
|
+
|
|
282
|
+
export const OpenAIResponsesResponseView = memo(function OpenAIResponsesResponseView({
|
|
283
|
+
response,
|
|
284
|
+
}: {
|
|
285
|
+
response: OpenAIResponsesResponse;
|
|
286
|
+
}): JSX.Element {
|
|
287
|
+
const text = extractResponsesOutputText(response);
|
|
288
|
+
const calls = extractResponsesFunctionCalls(response);
|
|
289
|
+
const status = response.status ?? null;
|
|
290
|
+
const inputTokens = response.usage?.input_tokens ?? 0;
|
|
291
|
+
const outputTokens = response.usage?.output_tokens ?? 0;
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div className="space-y-3">
|
|
295
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
296
|
+
{response.model !== undefined && response.model.length > 0 && (
|
|
297
|
+
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-mono">
|
|
298
|
+
{response.model}
|
|
299
|
+
</Badge>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{status !== null && (
|
|
303
|
+
<Badge
|
|
304
|
+
variant="outline"
|
|
305
|
+
className="text-[10px] px-1.5 py-0 h-5 font-mono flex items-center gap-1"
|
|
306
|
+
>
|
|
307
|
+
<StopCircle className="size-2.5" />
|
|
308
|
+
{status}
|
|
309
|
+
</Badge>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs">
|
|
313
|
+
<Zap className="size-3" />
|
|
314
|
+
<span className="font-mono tabular-nums">
|
|
315
|
+
{formatTokens(inputTokens)} in / {formatTokens(outputTokens)} out
|
|
316
|
+
</span>
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<Separator className="opacity-50" />
|
|
321
|
+
|
|
322
|
+
<div className="space-y-2">
|
|
323
|
+
{text.length > 0 && <AnswerMarkdown text={text} />}
|
|
324
|
+
{calls.map((call, i) => (
|
|
325
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: response output order is stable
|
|
326
|
+
<ResponsesFunctionCallBlock key={call.callId ?? call.id ?? `call-${i}`} call={call} />
|
|
327
|
+
))}
|
|
328
|
+
{text.length === 0 && calls.length === 0 && (
|
|
329
|
+
<p className="text-xs text-muted-foreground italic">Empty response content</p>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
OpenAIRequestSchema,
|
|
3
|
+
OpenAIResponsesRequestSchema,
|
|
4
|
+
parseOpenAIResponsesResponse,
|
|
5
|
+
parseOpenAIResponse,
|
|
6
|
+
} from "../../../contracts/openai";
|
|
7
|
+
import { safeGetOwnProperty } from "../../../lib/objectUtils";
|
|
2
8
|
import type { AnatomySegment } from "../anatomy/types";
|
|
3
9
|
import { countCharacters, estimateTokens } from "../anatomy/tokenEstimate";
|
|
4
10
|
import type { LogFormatAdapter } from "./types";
|
|
@@ -16,9 +22,12 @@ function contentToText(content: unknown): string {
|
|
|
16
22
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
17
23
|
const b = block as Record<string, unknown>;
|
|
18
24
|
const type = b.type;
|
|
19
|
-
if (
|
|
25
|
+
if (
|
|
26
|
+
(type === "text" || type === "input_text" || type === "output_text") &&
|
|
27
|
+
typeof b.text === "string"
|
|
28
|
+
) {
|
|
20
29
|
parts.push(b.text);
|
|
21
|
-
} else if (type === "image_url") {
|
|
30
|
+
} else if (type === "image_url" || type === "input_image") {
|
|
22
31
|
parts.push("[image]");
|
|
23
32
|
}
|
|
24
33
|
}
|
|
@@ -60,19 +69,61 @@ function toolsToText(tools: unknown): string {
|
|
|
60
69
|
const parts: string[] = [];
|
|
61
70
|
for (const tool of tools) {
|
|
62
71
|
if (tool === null || typeof tool !== "object") continue;
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (typeof
|
|
70
|
-
if (typeof
|
|
71
|
-
if (
|
|
72
|
+
const fn = safeGetOwnProperty(tool, "function");
|
|
73
|
+
const name = safeGetOwnProperty(fn, "name") ?? safeGetOwnProperty(tool, "name");
|
|
74
|
+
const description =
|
|
75
|
+
safeGetOwnProperty(fn, "description") ?? safeGetOwnProperty(tool, "description");
|
|
76
|
+
const parameters =
|
|
77
|
+
safeGetOwnProperty(fn, "parameters") ?? safeGetOwnProperty(tool, "parameters");
|
|
78
|
+
if (typeof name === "string") parts.push(name);
|
|
79
|
+
if (typeof description === "string") parts.push(description);
|
|
80
|
+
if (parameters !== undefined) parts.push(JSON.stringify(parameters));
|
|
72
81
|
}
|
|
73
82
|
return parts.join("\n");
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
function countResponsesInput(input: unknown): number | null {
|
|
86
|
+
if (typeof input === "string") return input.length > 0 ? 1 : 0;
|
|
87
|
+
if (Array.isArray(input)) return input.length;
|
|
88
|
+
if (input === undefined || input === null) return null;
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function responsesInputItemToText(input: unknown): string {
|
|
93
|
+
if (typeof input === "string") return input;
|
|
94
|
+
const directText = safeGetOwnProperty(input, "text");
|
|
95
|
+
if (typeof directText === "string") return directText;
|
|
96
|
+
const contentText = contentToText(safeGetOwnProperty(input, "content"));
|
|
97
|
+
if (contentText.length > 0) return contentText;
|
|
98
|
+
const outputText = contentToText(safeGetOwnProperty(input, "output"));
|
|
99
|
+
if (outputText.length > 0) return outputText;
|
|
100
|
+
const argumentsText = safeGetOwnProperty(input, "arguments");
|
|
101
|
+
return typeof argumentsText === "string" ? argumentsText : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function responsesInputRole(input: unknown): AnatomySegment["role"] {
|
|
105
|
+
const role = safeGetOwnProperty(input, "role");
|
|
106
|
+
if (role === "system" || role === "user" || role === "assistant" || role === "tool") {
|
|
107
|
+
return role;
|
|
108
|
+
}
|
|
109
|
+
const type = safeGetOwnProperty(input, "type");
|
|
110
|
+
if (type === "function_call_output") return "tool";
|
|
111
|
+
if (type === "message") return "user";
|
|
112
|
+
return "user";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function responsesToolNames(response: unknown): string[] {
|
|
116
|
+
const output = safeGetOwnProperty(response, "output");
|
|
117
|
+
if (!Array.isArray(output)) return [];
|
|
118
|
+
const names: string[] = [];
|
|
119
|
+
for (const item of output) {
|
|
120
|
+
if (safeGetOwnProperty(item, "type") !== "function_call") continue;
|
|
121
|
+
const name = safeGetOwnProperty(item, "name");
|
|
122
|
+
if (typeof name === "string" && name.length > 0) names.push(name);
|
|
123
|
+
}
|
|
124
|
+
return names;
|
|
125
|
+
}
|
|
126
|
+
|
|
76
127
|
function segment(
|
|
77
128
|
role: AnatomySegment["role"],
|
|
78
129
|
label: string,
|
|
@@ -96,7 +147,19 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
|
|
|
96
147
|
if (rawBody === null) return emptyRequestAnalysis(rawBody);
|
|
97
148
|
try {
|
|
98
149
|
const result = OpenAIRequestSchema.safeParse(JSON.parse(rawBody));
|
|
99
|
-
if (!result.success)
|
|
150
|
+
if (!result.success) {
|
|
151
|
+
const responsesResult = OpenAIResponsesRequestSchema.safeParse(JSON.parse(rawBody));
|
|
152
|
+
if (!responsesResult.success) return emptyRequestAnalysis(rawBody);
|
|
153
|
+
return {
|
|
154
|
+
parsed: responsesResult.data,
|
|
155
|
+
comparisonValue: responsesResult.data,
|
|
156
|
+
messageCount: countResponsesInput(responsesResult.data.input),
|
|
157
|
+
toolCount:
|
|
158
|
+
responsesResult.data.tools !== undefined && responsesResult.data.tools.length > 0
|
|
159
|
+
? responsesResult.data.tools.length
|
|
160
|
+
: null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
100
163
|
return {
|
|
101
164
|
parsed: result.data,
|
|
102
165
|
comparisonValue: result.data,
|
|
@@ -114,7 +177,15 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
|
|
|
114
177
|
analyzeResponse(responseText) {
|
|
115
178
|
if (responseText === null) return EMPTY_RESPONSE_ANALYSIS;
|
|
116
179
|
const parsed = parseOpenAIResponse(responseText);
|
|
117
|
-
if (parsed === null)
|
|
180
|
+
if (parsed === null) {
|
|
181
|
+
const responsesParsed = parseOpenAIResponsesResponse(responseText);
|
|
182
|
+
if (responsesParsed === null) return EMPTY_RESPONSE_ANALYSIS;
|
|
183
|
+
const toolNames = responsesToolNames(responsesParsed);
|
|
184
|
+
return {
|
|
185
|
+
parsed: responsesParsed,
|
|
186
|
+
toolNames: toolNames.length > 0 ? toolNames : null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
118
189
|
const toolNames =
|
|
119
190
|
parsed.choices[0]?.message?.tool_calls
|
|
120
191
|
?.map((toolCall) => toolCall.function?.name ?? "")
|
|
@@ -132,9 +203,18 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
|
|
|
132
203
|
// render even when the body shape is slightly off-schema. We only
|
|
133
204
|
// need the top-level `messages` and `tools` arrays.
|
|
134
205
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
135
|
-
const body = parsed as {
|
|
206
|
+
const body = parsed as {
|
|
207
|
+
messages?: unknown;
|
|
208
|
+
input?: unknown;
|
|
209
|
+
instructions?: unknown;
|
|
210
|
+
tools?: unknown;
|
|
211
|
+
};
|
|
136
212
|
const segments: AnatomySegment[] = [];
|
|
137
213
|
|
|
214
|
+
if (typeof body.instructions === "string" && body.instructions.length > 0) {
|
|
215
|
+
segments.push(segment("system", "instructions", body.instructions, "/instructions"));
|
|
216
|
+
}
|
|
217
|
+
|
|
138
218
|
// Promote a leading role: "system" message into a "system" segment.
|
|
139
219
|
// Spec: derive system role from a leading role: "system" message, then
|
|
140
220
|
// a segment per remaining message. We still keep the system message in
|
|
@@ -157,6 +237,18 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
|
|
|
157
237
|
});
|
|
158
238
|
}
|
|
159
239
|
|
|
240
|
+
if (!Array.isArray(body.messages)) {
|
|
241
|
+
if (typeof body.input === "string" && body.input.length > 0) {
|
|
242
|
+
segments.push(segment("user", "input", body.input, "/input"));
|
|
243
|
+
} else if (Array.isArray(body.input)) {
|
|
244
|
+
body.input.forEach((item, index) => {
|
|
245
|
+
const role = responsesInputRole(item);
|
|
246
|
+
const text = responsesInputItemToText(item);
|
|
247
|
+
segments.push(segment(role, `[${index}] ${role}`, text, `/input/${index}`));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
160
252
|
if (Array.isArray(body.tools) && body.tools.length > 0) {
|
|
161
253
|
const text = toolsToText(body.tools);
|
|
162
254
|
segments.push(segment("tools", "tools", text, "/tools"));
|
|
@@ -4,9 +4,12 @@ export const LOG_FOCUS_REQUEST_EVENT = "agent-inspector:focus-log";
|
|
|
4
4
|
|
|
5
5
|
export type LogFocusTab = "anatomy" | "request";
|
|
6
6
|
|
|
7
|
+
export type LogFocusSource = "virtualizer";
|
|
8
|
+
|
|
7
9
|
export type LogFocusRequest = {
|
|
8
10
|
logId: number;
|
|
9
11
|
tab: LogFocusTab;
|
|
12
|
+
source?: LogFocusSource;
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export function dispatchLogFocusRequest(request: LogFocusRequest): void {
|
|
@@ -24,9 +27,19 @@ export function readLogFocusRequest(event: Event): LogFocusRequest | null {
|
|
|
24
27
|
switch (tab) {
|
|
25
28
|
case "anatomy":
|
|
26
29
|
case "request":
|
|
27
|
-
|
|
30
|
+
break;
|
|
31
|
+
case undefined:
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const source = safeGetOwnProperty(detail, "source");
|
|
38
|
+
switch (source) {
|
|
39
|
+
case "virtualizer":
|
|
40
|
+
return { logId, tab: tab ?? "request", source };
|
|
28
41
|
case undefined:
|
|
29
|
-
return { logId, tab: "request" };
|
|
42
|
+
return { logId, tab: tab ?? "request" };
|
|
30
43
|
default:
|
|
31
44
|
return null;
|
|
32
45
|
}
|
|
@@ -144,10 +144,14 @@ function parseAnthropicTool(tool: unknown): RequestToolDefinition | null {
|
|
|
144
144
|
|
|
145
145
|
function parseOpenAITool(tool: unknown): RequestToolDefinition | null {
|
|
146
146
|
const fn = safeGetOwnProperty(tool, "function");
|
|
147
|
-
const
|
|
147
|
+
const toolType = safeGetOwnProperty(tool, "type");
|
|
148
|
+
if (fn === undefined && toolType !== "function") return null;
|
|
149
|
+
const name = safeGetOwnProperty(fn, "name") ?? safeGetOwnProperty(tool, "name");
|
|
148
150
|
if (typeof name !== "string" || name.length === 0) return null;
|
|
149
|
-
const description =
|
|
150
|
-
|
|
151
|
+
const description =
|
|
152
|
+
safeGetOwnProperty(fn, "description") ?? safeGetOwnProperty(tool, "description");
|
|
153
|
+
const schema =
|
|
154
|
+
safeGetOwnProperty(fn, "parameters") ?? safeGetOwnProperty(tool, "parameters") ?? null;
|
|
151
155
|
const normalizedDescription = typeof description === "string" ? description : null;
|
|
152
156
|
return {
|
|
153
157
|
name,
|
|
@@ -6,6 +6,7 @@ import { resolveLogFormat } from "./log-formats";
|
|
|
6
6
|
export type TurnEntry = {
|
|
7
7
|
log: CapturedLog;
|
|
8
8
|
stopReason: StopReason;
|
|
9
|
+
sessionLogNumber: number;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
export type TurnGroupData = {
|
|
@@ -44,7 +45,29 @@ export type ToolTraceEvent = {
|
|
|
44
45
|
argumentsPreview: string | null;
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
type ToolTraceCacheEntry = {
|
|
49
|
+
apiFormat: CapturedLog["apiFormat"];
|
|
50
|
+
responseText: string | null;
|
|
51
|
+
events: ToolTraceEvent[];
|
|
52
|
+
};
|
|
53
|
+
|
|
47
54
|
const PREVIEW_LIMIT = 180;
|
|
55
|
+
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
|
+
}
|
|
48
71
|
|
|
49
72
|
export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
|
|
50
73
|
return standalone || expanded;
|
|
@@ -63,8 +86,10 @@ export function buildTurnGroups(logs: CapturedLog[]): TurnGroupData[] {
|
|
|
63
86
|
let entries: TurnEntry[] = [];
|
|
64
87
|
let turnIndex = 0;
|
|
65
88
|
|
|
66
|
-
for (
|
|
67
|
-
|
|
89
|
+
for (let index = 0; index < logs.length; index += 1) {
|
|
90
|
+
const log = logs[index];
|
|
91
|
+
if (log === undefined) continue;
|
|
92
|
+
entries.push({ log, stopReason: extractStopReason(log), sessionLogNumber: index + 1 });
|
|
68
93
|
const current = entries[entries.length - 1];
|
|
69
94
|
if (current !== undefined && isTurnBoundary(current.stopReason)) {
|
|
70
95
|
groups.push({ entries, turnIndex });
|
|
@@ -177,19 +202,66 @@ function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
|
177
202
|
});
|
|
178
203
|
}
|
|
179
204
|
}
|
|
205
|
+
const output = safeGetOwnProperty(parsed, "output");
|
|
206
|
+
if (Array.isArray(output)) {
|
|
207
|
+
for (const item of output) {
|
|
208
|
+
if (safeGetOwnProperty(item, "type") !== "function_call") continue;
|
|
209
|
+
const name = safeGetOwnProperty(item, "name");
|
|
210
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
211
|
+
const args = safeGetOwnProperty(item, "arguments");
|
|
212
|
+
events.push({
|
|
213
|
+
id: `${String(log.id)}-openai-tool-${String(events.length)}`,
|
|
214
|
+
logId: log.id,
|
|
215
|
+
index: events.length,
|
|
216
|
+
provider: "openai",
|
|
217
|
+
name,
|
|
218
|
+
argumentsText: copyValue(args),
|
|
219
|
+
argumentsPreview: previewValue(args),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
180
223
|
return events;
|
|
181
224
|
}
|
|
182
225
|
|
|
183
226
|
export function extractToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
227
|
+
const cached = toolTraceCache.get(log);
|
|
228
|
+
if (
|
|
229
|
+
cached !== undefined &&
|
|
230
|
+
cached.apiFormat === log.apiFormat &&
|
|
231
|
+
cached.responseText === log.responseText
|
|
232
|
+
) {
|
|
233
|
+
return cached.events;
|
|
234
|
+
}
|
|
235
|
+
|
|
184
236
|
const format = resolveLogFormat(log);
|
|
237
|
+
if (!responseMayContainToolTrace(log, format)) {
|
|
238
|
+
const events: ToolTraceEvent[] = [];
|
|
239
|
+
toolTraceCache.set(log, {
|
|
240
|
+
apiFormat: log.apiFormat,
|
|
241
|
+
responseText: log.responseText,
|
|
242
|
+
events,
|
|
243
|
+
});
|
|
244
|
+
return events;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let events: ToolTraceEvent[];
|
|
185
248
|
switch (format) {
|
|
186
249
|
case "anthropic":
|
|
187
|
-
|
|
250
|
+
events = extractAnthropicToolTraceEvents(log);
|
|
251
|
+
break;
|
|
188
252
|
case "openai":
|
|
189
|
-
|
|
253
|
+
events = extractOpenAIToolTraceEvents(log);
|
|
254
|
+
break;
|
|
190
255
|
case "unknown":
|
|
191
|
-
|
|
256
|
+
events = [];
|
|
257
|
+
break;
|
|
192
258
|
}
|
|
259
|
+
toolTraceCache.set(log, {
|
|
260
|
+
apiFormat: log.apiFormat,
|
|
261
|
+
responseText: log.responseText,
|
|
262
|
+
events,
|
|
263
|
+
});
|
|
264
|
+
return events;
|
|
193
265
|
}
|
|
194
266
|
|
|
195
267
|
export function buildTraceSummary(
|
|
@@ -1,17 +1,52 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState, useTransition } from "react";
|
|
2
2
|
|
|
3
3
|
type JsonPrimitive = string | number | boolean | null;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
| ReadonlyArray<JsonValue>
|
|
7
|
-
| Readonly<{ [key: string]: JsonValue }>;
|
|
4
|
+
type JsonObject = Readonly<{ [key: string]: JsonValue }>;
|
|
5
|
+
export type JsonValue = JsonPrimitive | ReadonlyArray<JsonValue> | JsonObject;
|
|
8
6
|
|
|
9
7
|
export type JsonExpansionPolicy = {
|
|
10
8
|
depth: number;
|
|
11
9
|
};
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const FULL_EXPAND_NODE_LIMIT = 400;
|
|
12
|
+
const STRUCTURE_EXPAND_NODE_LIMIT = 2_000;
|
|
13
|
+
|
|
14
|
+
function isJsonArray(value: JsonValue): value is ReadonlyArray<JsonValue> {
|
|
15
|
+
return Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function countJsonNodesUpTo(value: JsonValue, limit: number): number {
|
|
19
|
+
const stack: JsonValue[] = [value];
|
|
20
|
+
let count = 0;
|
|
21
|
+
|
|
22
|
+
while (stack.length > 0) {
|
|
23
|
+
const current = stack.pop();
|
|
24
|
+
if (current === undefined) continue;
|
|
25
|
+
count += 1;
|
|
26
|
+
if (count > limit) return count;
|
|
27
|
+
|
|
28
|
+
if (current === null || typeof current !== "object") continue;
|
|
29
|
+
if (isJsonArray(current)) {
|
|
30
|
+
for (const child of current) {
|
|
31
|
+
stack.push(child);
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const key of Object.keys(current)) {
|
|
37
|
+
const child = current[key];
|
|
38
|
+
if (child !== undefined) stack.push(child);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getJsonExpansionPolicy(value: JsonValue): JsonExpansionPolicy {
|
|
46
|
+
const nodeCount = countJsonNodesUpTo(value, STRUCTURE_EXPAND_NODE_LIMIT + 1);
|
|
47
|
+
if (nodeCount <= FULL_EXPAND_NODE_LIMIT) return { depth: Number.POSITIVE_INFINITY };
|
|
48
|
+
if (nodeCount <= STRUCTURE_EXPAND_NODE_LIMIT) return { depth: 3 };
|
|
49
|
+
return { depth: 2 };
|
|
15
50
|
}
|
|
16
51
|
|
|
17
52
|
export type JsonBulkExpansion = {
|