@tonyclaw/agent-inspector 2.0.10 → 2.0.12
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-BWMipEjS.js → CompareDrawer-ClM_uVRy.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CTNNzXSa.js +114 -0
- package/.output/public/assets/{ReplayDialog-DaSzZsvM.js → ReplayDialog-7rp7N_KJ.js} +1 -1
- package/.output/public/assets/RequestAnatomy-sL2FFo_N.js +1 -0
- package/.output/public/assets/ResponseView-Cjlf3b6-.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-B4atnL99.js → StreamingChunkSequence-Ctx3sT3r.js} +1 -1
- package/.output/public/assets/_sessionId-DEWzDQ8-.js +1 -0
- package/.output/public/assets/index-DGIIhZTn.js +1 -0
- package/.output/public/assets/index-D_nZj9Vt.css +1 -0
- package/.output/public/assets/{main-DE6Y4-wV.js → main-FvIQopAy.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +51 -39
- package/.output/server/{_sessionId-61HO9rUI.mjs → _sessionId-BDSgtrsI.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CSPqneYm.mjs → CompareDrawer-Cqvv0I3x.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-DrSMa5O7.mjs → ProxyViewerContainer-C0YEoG8N.mjs} +1611 -183
- package/.output/server/_ssr/{ReplayDialog-Dn3naENv.mjs → ReplayDialog-CrsynUKI.mjs} +4 -4
- package/.output/server/_ssr/RequestAnatomy-CR_JUkcL.mjs +688 -0
- package/.output/server/_ssr/{ResponseView-CwXAWOoE.mjs → ResponseView-Dba2Et69.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DRSR2FfN.mjs → StreamingChunkSequence-BIMHLstI.mjs} +2 -2
- package/.output/server/_ssr/{index-D4zn5CAg.mjs → index-DveIUTm5.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CSRaa_tu.mjs → router-D0yXVG0Q.mjs} +85 -22
- package/.output/server/{_tanstack-start-manifest_v-CfvVUYYj.mjs → _tanstack-start-manifest_v-pe0xJFuQ.mjs} +1 -1
- package/.output/server/index.mjs +70 -70
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +453 -52
- package/src/components/providers/ProviderCard.tsx +45 -2
- package/src/components/providers/ProviderForm.tsx +245 -80
- package/src/components/providers/ProvidersPanel.tsx +7 -1
- package/src/components/proxy-viewer/ConversationGroup.tsx +18 -1
- package/src/components/proxy-viewer/LogEntry.tsx +25 -0
- package/src/components/proxy-viewer/TurnGroup.tsx +15 -0
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +144 -80
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +70 -5
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +196 -0
- package/src/components/proxy-viewer/logFocus.ts +33 -0
- package/src/lib/providerModelMetadata.ts +64 -12
- package/src/lib/utils.ts +7 -0
- package/src/proxy/providers.ts +25 -0
- package/src/routes/api/providers.ts +3 -0
- package/.output/public/assets/ProxyViewerContainer-DIAgurux.js +0 -114
- package/.output/public/assets/RequestAnatomy-BfQmPHCd.js +0 -1
- package/.output/public/assets/ResponseView-CblEWDMx.js +0 -1
- package/.output/public/assets/_sessionId-DUzYnZXV.js +0 -1
- package/.output/public/assets/index-CxhRsekH.css +0 -1
- package/.output/public/assets/index-jagRsQaV.js +0 -1
- package/.output/server/_ssr/RequestAnatomy-D1X3f3AX.mjs +0 -1322
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Info } from "lucide-react";
|
|
1
|
+
import { ChevronDown, ChevronRight, Info } from "lucide-react";
|
|
2
2
|
import { type JSX, useMemo, useState } from "react";
|
|
3
|
-
import { cn, formatTokens } from "../../../lib/utils";
|
|
3
|
+
import { cn, formatContextWindowTokens, formatTokens } from "../../../lib/utils";
|
|
4
4
|
import { useProviders } from "../../../lib/useProviders";
|
|
5
5
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/tooltip";
|
|
6
6
|
import {
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type ContextDiagnostic,
|
|
10
10
|
type DuplicateContextGroup,
|
|
11
11
|
type DuplicateContextSegment,
|
|
12
|
+
type ContextHealth,
|
|
13
|
+
type ContextHealthLevel,
|
|
12
14
|
type ContextIntelligence,
|
|
13
15
|
type ContextRiskLevel,
|
|
14
16
|
} from "./contextIntelligence";
|
|
@@ -63,6 +65,18 @@ const RISK_TEXT_CLASS: Record<ContextRiskLevel, string> = {
|
|
|
63
65
|
danger: "text-red-300",
|
|
64
66
|
};
|
|
65
67
|
|
|
68
|
+
const HEALTH_BADGE_CLASS: Record<ContextHealthLevel, string> = {
|
|
69
|
+
ok: "border-emerald-400/30 bg-emerald-400/8 text-emerald-200",
|
|
70
|
+
optimizable: "border-amber-400/30 bg-amber-400/8 text-amber-100",
|
|
71
|
+
risk: "border-red-400/35 bg-red-500/10 text-red-100",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const HEALTH_LABEL: Record<ContextHealthLevel, string> = {
|
|
75
|
+
ok: "OK",
|
|
76
|
+
optimizable: "Optimizable",
|
|
77
|
+
risk: "Risk",
|
|
78
|
+
};
|
|
79
|
+
|
|
66
80
|
function formatPercent(value: number): string {
|
|
67
81
|
if (value >= 10) return `${value.toFixed(0)}%`;
|
|
68
82
|
if (value >= 1) return `${value.toFixed(1)}%`;
|
|
@@ -83,7 +97,7 @@ function formatRemainingTokens(value: number | null): string {
|
|
|
83
97
|
|
|
84
98
|
function formatContextLimit(intelligence: ContextIntelligence): string {
|
|
85
99
|
if (intelligence.contextWindow.tokens === null) return "Unknown";
|
|
86
|
-
return
|
|
100
|
+
return formatContextWindowTokens(intelligence.contextWindow.tokens);
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
function formatContextSource(intelligence: ContextIntelligence): string {
|
|
@@ -110,6 +124,33 @@ function diagnosticTone(diagnostic: ContextDiagnostic): string {
|
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
function formatContextHealth(health: ContextHealth): string {
|
|
128
|
+
const label = HEALTH_LABEL[health.level];
|
|
129
|
+
if (health.level === "ok") return label;
|
|
130
|
+
const countLabel =
|
|
131
|
+
health.level === "risk"
|
|
132
|
+
? `${String(health.opportunityCount)} signal${health.opportunityCount === 1 ? "" : "s"}`
|
|
133
|
+
: `${String(health.opportunityCount)} opportunit${
|
|
134
|
+
health.opportunityCount === 1 ? "y" : "ies"
|
|
135
|
+
}`;
|
|
136
|
+
if (health.reclaimableTokens <= 0) return `${label} | ${countLabel}`;
|
|
137
|
+
return `${label} | ${countLabel} | ~${formatTokens(health.reclaimableTokens)} tokens`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ContextHealthBadge({ health }: { health: ContextHealth }): JSX.Element {
|
|
141
|
+
return (
|
|
142
|
+
<span
|
|
143
|
+
className={cn(
|
|
144
|
+
"inline-flex h-5 max-w-full items-center rounded border px-1.5 font-mono text-[10px]",
|
|
145
|
+
HEALTH_BADGE_CLASS[health.level],
|
|
146
|
+
)}
|
|
147
|
+
aria-label={`Context health ${formatContextHealth(health)}`}
|
|
148
|
+
>
|
|
149
|
+
<span className="truncate">{formatContextHealth(health)}</span>
|
|
150
|
+
</span>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
113
154
|
function ContextIntelligenceStrip({
|
|
114
155
|
intelligence,
|
|
115
156
|
}: {
|
|
@@ -243,6 +284,8 @@ function DuplicateContentPanel({
|
|
|
243
284
|
segments: readonly AnatomySegment[];
|
|
244
285
|
onSegmentActivate?: (segment: AnatomySegment) => void;
|
|
245
286
|
}): JSX.Element | null {
|
|
287
|
+
const [expanded, setExpanded] = useState(false);
|
|
288
|
+
|
|
246
289
|
if (groups.length === 0) return null;
|
|
247
290
|
|
|
248
291
|
const visibleGroups = groups.slice(0, DUPLICATE_GROUP_DISPLAY_COUNT);
|
|
@@ -251,87 +294,105 @@ function DuplicateContentPanel({
|
|
|
251
294
|
|
|
252
295
|
return (
|
|
253
296
|
<section className="space-y-2 rounded border border-border/70 bg-muted/15 p-2">
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
297
|
+
<button
|
|
298
|
+
type="button"
|
|
299
|
+
onClick={() => setExpanded((current) => !current)}
|
|
300
|
+
aria-expanded={expanded}
|
|
301
|
+
className="flex w-full flex-wrap items-center justify-between gap-2 rounded px-1 py-0.5 text-left transition-colors hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
302
|
+
>
|
|
303
|
+
<span className="inline-flex min-w-0 items-center gap-1.5">
|
|
304
|
+
{expanded ? (
|
|
305
|
+
<ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
|
|
306
|
+
) : (
|
|
307
|
+
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
|
|
308
|
+
)}
|
|
309
|
+
<span className="truncate text-xs font-medium text-foreground">Duplicate Content</span>
|
|
310
|
+
</span>
|
|
311
|
+
<span className="font-mono text-[11px] text-muted-foreground">
|
|
257
312
|
{String(groups.length)} group{groups.length === 1 ? "" : "s"} | ~
|
|
258
313
|
{formatTokens(totalRepeatedTokens)} repeated
|
|
259
|
-
</
|
|
260
|
-
</
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
314
|
+
</span>
|
|
315
|
+
</button>
|
|
316
|
+
|
|
317
|
+
{expanded && (
|
|
318
|
+
<>
|
|
319
|
+
<div className="space-y-2">
|
|
320
|
+
{visibleGroups.map((group, groupIndex) => {
|
|
321
|
+
const visibleSegments = group.segments.slice(0, DUPLICATE_SEGMENT_DISPLAY_COUNT);
|
|
322
|
+
const hiddenSegmentCount = group.segments.length - visibleSegments.length;
|
|
323
|
+
return (
|
|
324
|
+
<div
|
|
325
|
+
key={group.key}
|
|
326
|
+
className="space-y-1.5 border-t border-border/50 pt-2 first:border-t-0 first:pt-0"
|
|
327
|
+
>
|
|
328
|
+
<div className="flex min-w-0 items-center gap-2 text-xs">
|
|
329
|
+
<span className="w-5 shrink-0 text-right font-mono text-muted-foreground/70">
|
|
330
|
+
{String(groupIndex + 1)}
|
|
331
|
+
</span>
|
|
332
|
+
<span className="min-w-0 flex-1 truncate text-foreground">
|
|
333
|
+
{group.firstLabel}
|
|
334
|
+
</span>
|
|
335
|
+
<span className="shrink-0 font-mono text-muted-foreground">
|
|
336
|
+
x{String(group.count)} | ~{formatTokens(group.repeatedTokens)} saved
|
|
337
|
+
</span>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{group.preview.length > 0 && (
|
|
341
|
+
<div className="ml-7 line-clamp-2 break-words rounded bg-background/40 px-2 py-1 font-mono text-[11px] leading-4 text-muted-foreground">
|
|
342
|
+
{group.preview}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
280
345
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
346
|
+
<div className="ml-7 flex flex-wrap gap-1">
|
|
347
|
+
{visibleSegments.map((duplicateSegment, segmentIndex) => {
|
|
348
|
+
const target = resolveDuplicateSegment(duplicateSegment, segments);
|
|
349
|
+
const activate =
|
|
350
|
+
target === null || onSegmentActivate === undefined
|
|
351
|
+
? undefined
|
|
352
|
+
: onSegmentActivate;
|
|
353
|
+
const label = `${duplicateSegment.label} ~${formatTokens(
|
|
354
|
+
duplicateSegment.tokens,
|
|
355
|
+
)}`;
|
|
356
|
+
if (activate === undefined || target === null) {
|
|
357
|
+
return (
|
|
358
|
+
<span
|
|
359
|
+
key={`${duplicateSegment.path}-${segmentIndex}`}
|
|
360
|
+
className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground"
|
|
361
|
+
title={label}
|
|
362
|
+
>
|
|
363
|
+
<span className="truncate">{label}</span>
|
|
364
|
+
</span>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return (
|
|
368
|
+
<button
|
|
369
|
+
key={`${duplicateSegment.path}-${segmentIndex}`}
|
|
370
|
+
type="button"
|
|
371
|
+
onClick={() => activate(target)}
|
|
372
|
+
className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground hover:border-border hover:bg-muted/40 hover:text-foreground"
|
|
373
|
+
title="Jump to this duplicate block"
|
|
374
|
+
>
|
|
375
|
+
<span className="truncate">{label}</span>
|
|
376
|
+
</button>
|
|
377
|
+
);
|
|
378
|
+
})}
|
|
379
|
+
{hiddenSegmentCount > 0 && (
|
|
380
|
+
<span className="inline-flex h-6 items-center rounded border border-border/40 px-1.5 font-mono text-[11px] text-muted-foreground/70">
|
|
381
|
+
+{String(hiddenSegmentCount)}
|
|
305
382
|
</span>
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
onClick={() => activate(target)}
|
|
313
|
-
className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground hover:border-border hover:bg-muted/40 hover:text-foreground"
|
|
314
|
-
title="Jump to this duplicate block"
|
|
315
|
-
>
|
|
316
|
-
<span className="truncate">{label}</span>
|
|
317
|
-
</button>
|
|
318
|
-
);
|
|
319
|
-
})}
|
|
320
|
-
{hiddenSegmentCount > 0 && (
|
|
321
|
-
<span className="inline-flex h-6 items-center rounded border border-border/40 px-1.5 font-mono text-[11px] text-muted-foreground/70">
|
|
322
|
-
+{String(hiddenSegmentCount)}
|
|
323
|
-
</span>
|
|
324
|
-
)}
|
|
325
|
-
</div>
|
|
326
|
-
</div>
|
|
327
|
-
);
|
|
328
|
-
})}
|
|
329
|
-
</div>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
})}
|
|
388
|
+
</div>
|
|
330
389
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
390
|
+
{hiddenGroupCount > 0 && (
|
|
391
|
+
<div className="border-t border-border/50 pt-2 font-mono text-[11px] text-muted-foreground">
|
|
392
|
+
+{String(hiddenGroupCount)} more duplicate group{hiddenGroupCount === 1 ? "" : "s"}
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</>
|
|
335
396
|
)}
|
|
336
397
|
</section>
|
|
337
398
|
);
|
|
@@ -437,7 +498,10 @@ export function RequestAnatomy({
|
|
|
437
498
|
<div className="px-4 py-3 space-y-3" data-testid="anatomy-root">
|
|
438
499
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
439
500
|
<div className="min-w-0">
|
|
440
|
-
<div className="
|
|
501
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
502
|
+
<div className="text-sm font-semibold text-foreground">Request Context</div>
|
|
503
|
+
{intelligence !== null && <ContextHealthBadge health={intelligence.health} />}
|
|
504
|
+
</div>
|
|
441
505
|
<div className={cn("mt-0.5 text-xs font-mono tabular-nums", summaryColorClass)}>
|
|
442
506
|
Estimated ~{formatTokens(total)} tokens | {providerInputLabel} | {contextLimitLabel} |{" "}
|
|
443
507
|
{usageLabel} | {segmentCountLabel}
|
|
@@ -8,6 +8,8 @@ export type ContextRiskLevel = "unknown" | "ok" | "watch" | "danger";
|
|
|
8
8
|
|
|
9
9
|
export type ContextWindowSource = "request" | "provider" | "model-rule" | "unknown";
|
|
10
10
|
|
|
11
|
+
export type ContextHealthLevel = "ok" | "optimizable" | "risk";
|
|
12
|
+
|
|
11
13
|
export type ContextWindow = {
|
|
12
14
|
tokens: number | null;
|
|
13
15
|
label: string;
|
|
@@ -46,6 +48,12 @@ export type DuplicateContextGroup = {
|
|
|
46
48
|
segments: DuplicateContextSegment[];
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
export type ContextHealth = {
|
|
52
|
+
level: ContextHealthLevel;
|
|
53
|
+
opportunityCount: number;
|
|
54
|
+
reclaimableTokens: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
49
57
|
export type ContextIntelligence = {
|
|
50
58
|
model: string | null;
|
|
51
59
|
contextWindow: ContextWindow;
|
|
@@ -60,6 +68,7 @@ export type ContextIntelligence = {
|
|
|
60
68
|
largestRole: RoleUsage | null;
|
|
61
69
|
diagnostics: ContextDiagnostic[];
|
|
62
70
|
duplicateGroups: DuplicateContextGroup[];
|
|
71
|
+
health: ContextHealth;
|
|
63
72
|
};
|
|
64
73
|
|
|
65
74
|
type MatchMode = "prefix" | "includes";
|
|
@@ -158,7 +167,7 @@ const MODEL_CONTEXT_RULES: readonly ModelContextRule[] = [
|
|
|
158
167
|
},
|
|
159
168
|
{
|
|
160
169
|
family: "DeepSeek V4",
|
|
161
|
-
tokens:
|
|
170
|
+
tokens: 1_048_576,
|
|
162
171
|
mode: "prefix",
|
|
163
172
|
patterns: ["deepseek-v4-pro", "deepseek-v4-flash"],
|
|
164
173
|
},
|
|
@@ -170,15 +179,33 @@ const MODEL_CONTEXT_RULES: readonly ModelContextRule[] = [
|
|
|
170
179
|
},
|
|
171
180
|
{
|
|
172
181
|
family: "MiniMax M3",
|
|
173
|
-
tokens:
|
|
182
|
+
tokens: 1_048_576,
|
|
174
183
|
mode: "includes",
|
|
175
184
|
patterns: ["minimax-m3"],
|
|
176
185
|
},
|
|
177
186
|
{
|
|
178
|
-
family: "MiniMax M2",
|
|
187
|
+
family: "MiniMax M2.7",
|
|
179
188
|
tokens: 204_800,
|
|
180
189
|
mode: "includes",
|
|
181
|
-
patterns: ["minimax-m2.7"
|
|
190
|
+
patterns: ["minimax-m2.7"],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
family: "MiniMax M2.5/M2.1",
|
|
194
|
+
tokens: 196_608,
|
|
195
|
+
mode: "includes",
|
|
196
|
+
patterns: ["minimax-m2.5", "minimax-m2.1"],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
family: "GLM-5.2",
|
|
200
|
+
tokens: 1_048_576,
|
|
201
|
+
mode: "prefix",
|
|
202
|
+
patterns: ["glm-5.2"],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
family: "GLM-5",
|
|
206
|
+
tokens: 202_752,
|
|
207
|
+
mode: "prefix",
|
|
208
|
+
patterns: ["glm-5", "glm-5.1"],
|
|
182
209
|
},
|
|
183
210
|
];
|
|
184
211
|
|
|
@@ -551,6 +578,10 @@ function duplicateDiagnostic(
|
|
|
551
578
|
};
|
|
552
579
|
}
|
|
553
580
|
|
|
581
|
+
function duplicateReclaimableTokens(duplicateGroups: readonly DuplicateContextGroup[]): number {
|
|
582
|
+
return duplicateGroups.reduce((sum, group) => sum + group.repeatedTokens, 0);
|
|
583
|
+
}
|
|
584
|
+
|
|
554
585
|
function diagnosticsForRolePressure(
|
|
555
586
|
roleUsages: readonly RoleUsage[],
|
|
556
587
|
total: number,
|
|
@@ -606,6 +637,33 @@ function diagnosticsForRolePressure(
|
|
|
606
637
|
return diagnostics;
|
|
607
638
|
}
|
|
608
639
|
|
|
640
|
+
function contextHealth({
|
|
641
|
+
risk,
|
|
642
|
+
contextWindow,
|
|
643
|
+
diagnostics,
|
|
644
|
+
duplicateGroups,
|
|
645
|
+
}: {
|
|
646
|
+
risk: ContextRiskLevel;
|
|
647
|
+
contextWindow: ContextWindow;
|
|
648
|
+
diagnostics: readonly ContextDiagnostic[];
|
|
649
|
+
duplicateGroups: readonly DuplicateContextGroup[];
|
|
650
|
+
}): ContextHealth {
|
|
651
|
+
const diagnosticKinds = new Set(diagnostics.map((diagnostic) => diagnostic.kind));
|
|
652
|
+
let opportunityCount = diagnosticKinds.size;
|
|
653
|
+
if (risk === "watch" || risk === "danger") opportunityCount += 1;
|
|
654
|
+
if (contextWindow.source === "unknown") opportunityCount += 1;
|
|
655
|
+
|
|
656
|
+
const hasDangerDiagnostic = diagnostics.some((diagnostic) => diagnostic.severity === "danger");
|
|
657
|
+
const level: ContextHealthLevel =
|
|
658
|
+
risk === "danger" || hasDangerDiagnostic ? "risk" : opportunityCount > 0 ? "optimizable" : "ok";
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
level,
|
|
662
|
+
opportunityCount,
|
|
663
|
+
reclaimableTokens: duplicateReclaimableTokens(duplicateGroups),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
609
667
|
export function analyzeContextIntelligence({
|
|
610
668
|
segments,
|
|
611
669
|
inputTokens,
|
|
@@ -648,6 +706,7 @@ export function analyzeContextIntelligence({
|
|
|
648
706
|
messageSegmentCount,
|
|
649
707
|
);
|
|
650
708
|
if (duplicate !== null) diagnostics.unshift(duplicate);
|
|
709
|
+
const risk = riskLevel(usagePercent);
|
|
651
710
|
|
|
652
711
|
return {
|
|
653
712
|
model: requestModel,
|
|
@@ -658,11 +717,17 @@ export function analyzeContextIntelligence({
|
|
|
658
717
|
remainingInputTokens,
|
|
659
718
|
remainingAfterReserveTokens,
|
|
660
719
|
usagePercent,
|
|
661
|
-
riskLevel:
|
|
720
|
+
riskLevel: risk,
|
|
662
721
|
roleUsages,
|
|
663
722
|
largestRole,
|
|
664
723
|
diagnostics,
|
|
665
724
|
duplicateGroups,
|
|
725
|
+
health: contextHealth({
|
|
726
|
+
risk,
|
|
727
|
+
contextWindow,
|
|
728
|
+
diagnostics,
|
|
729
|
+
duplicateGroups,
|
|
730
|
+
}),
|
|
666
731
|
};
|
|
667
732
|
}
|
|
668
733
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { ProviderConfig } from "../../../lib/providerContract";
|
|
2
|
+
import type { CapturedLog } from "../../../proxy/schemas";
|
|
3
|
+
import { getLogFormatAdapter, resolveLogFormat } from "../log-formats";
|
|
4
|
+
import {
|
|
5
|
+
analyzeContextIntelligence,
|
|
6
|
+
type ContextHealth,
|
|
7
|
+
type ContextRiskLevel,
|
|
8
|
+
type ContextWindow,
|
|
9
|
+
type RoleUsage,
|
|
10
|
+
} from "./contextIntelligence";
|
|
11
|
+
import type { AnatomySegment } from "./types";
|
|
12
|
+
|
|
13
|
+
export type SessionContextSnapshot = {
|
|
14
|
+
logId: number;
|
|
15
|
+
model: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
contextWindow: ContextWindow;
|
|
18
|
+
estimatedInputTokens: number;
|
|
19
|
+
windowUsedTokens: number | null;
|
|
20
|
+
usagePercent: number | null;
|
|
21
|
+
riskLevel: ContextRiskLevel;
|
|
22
|
+
health: ContextHealth;
|
|
23
|
+
diagnostics: string[];
|
|
24
|
+
roleSegments: AnatomySegment[];
|
|
25
|
+
roleTotalTokens: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type SessionModelContextSummary = {
|
|
29
|
+
model: string;
|
|
30
|
+
requestCount: number;
|
|
31
|
+
first: SessionContextSnapshot;
|
|
32
|
+
latest: SessionContextSnapshot;
|
|
33
|
+
peak: SessionContextSnapshot;
|
|
34
|
+
trendPercent: number | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type SessionContextSummary = {
|
|
38
|
+
models: SessionModelContextSummary[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type SummaryAccumulator = {
|
|
42
|
+
model: string;
|
|
43
|
+
requestCount: number;
|
|
44
|
+
first: SessionContextSnapshot;
|
|
45
|
+
latest: SessionContextSnapshot;
|
|
46
|
+
peak: SessionContextSnapshot;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function parseRequestBody(rawBody: string | null): unknown | null {
|
|
50
|
+
if (rawBody === null) return null;
|
|
51
|
+
try {
|
|
52
|
+
const parsed: unknown = JSON.parse(rawBody);
|
|
53
|
+
return parsed;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function roleUsageSegment(usage: RoleUsage): AnatomySegment {
|
|
60
|
+
return {
|
|
61
|
+
role: usage.role,
|
|
62
|
+
label: usage.label,
|
|
63
|
+
size: usage.tokens,
|
|
64
|
+
characters: usage.tokens * 4,
|
|
65
|
+
text: "",
|
|
66
|
+
path: `role:${usage.role}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function snapshotFromLog(
|
|
71
|
+
log: CapturedLog,
|
|
72
|
+
providers: readonly ProviderConfig[],
|
|
73
|
+
): SessionContextSnapshot | null {
|
|
74
|
+
const parsed = parseRequestBody(log.rawRequestBody);
|
|
75
|
+
if (parsed === null) return null;
|
|
76
|
+
|
|
77
|
+
const adapter = getLogFormatAdapter(resolveLogFormat(log));
|
|
78
|
+
const segments = adapter.anatomySegments(parsed);
|
|
79
|
+
if (segments === null) return null;
|
|
80
|
+
|
|
81
|
+
const intelligence = analyzeContextIntelligence({
|
|
82
|
+
segments,
|
|
83
|
+
inputTokens: log.inputTokens ?? null,
|
|
84
|
+
parsed,
|
|
85
|
+
model: log.model,
|
|
86
|
+
providers,
|
|
87
|
+
});
|
|
88
|
+
const model = intelligence.model ?? log.model ?? "Unknown model";
|
|
89
|
+
const roleSegments = intelligence.roleUsages.map(roleUsageSegment);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
logId: log.id,
|
|
93
|
+
model,
|
|
94
|
+
timestamp: log.timestamp,
|
|
95
|
+
contextWindow: intelligence.contextWindow,
|
|
96
|
+
estimatedInputTokens: intelligence.estimatedInputTokens,
|
|
97
|
+
windowUsedTokens: intelligence.windowUsedTokens,
|
|
98
|
+
usagePercent: intelligence.usagePercent,
|
|
99
|
+
riskLevel: intelligence.riskLevel,
|
|
100
|
+
health: intelligence.health,
|
|
101
|
+
diagnostics: intelligence.diagnostics.map((diagnostic) => diagnostic.title),
|
|
102
|
+
roleSegments,
|
|
103
|
+
roleTotalTokens: roleSegments.reduce((sum, segment) => sum + segment.size, 0),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isHigherPeak(candidate: SessionContextSnapshot, current: SessionContextSnapshot): boolean {
|
|
108
|
+
if (candidate.usagePercent !== null && current.usagePercent !== null) {
|
|
109
|
+
return candidate.usagePercent > current.usagePercent;
|
|
110
|
+
}
|
|
111
|
+
if (candidate.usagePercent !== null && current.usagePercent === null) return true;
|
|
112
|
+
if (candidate.usagePercent === null && current.usagePercent !== null) return false;
|
|
113
|
+
return candidate.estimatedInputTokens > current.estimatedInputTokens;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function riskRank(risk: ContextRiskLevel): number {
|
|
117
|
+
switch (risk) {
|
|
118
|
+
case "danger":
|
|
119
|
+
return 4;
|
|
120
|
+
case "watch":
|
|
121
|
+
return 3;
|
|
122
|
+
case "unknown":
|
|
123
|
+
return 2;
|
|
124
|
+
case "ok":
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function compareModelSummary(
|
|
130
|
+
left: SessionModelContextSummary,
|
|
131
|
+
right: SessionModelContextSummary,
|
|
132
|
+
): number {
|
|
133
|
+
const riskDelta = riskRank(right.latest.riskLevel) - riskRank(left.latest.riskLevel);
|
|
134
|
+
if (riskDelta !== 0) return riskDelta;
|
|
135
|
+
|
|
136
|
+
const leftPeak = left.peak.usagePercent ?? -1;
|
|
137
|
+
const rightPeak = right.peak.usagePercent ?? -1;
|
|
138
|
+
const peakDelta = rightPeak - leftPeak;
|
|
139
|
+
if (peakDelta !== 0) return peakDelta;
|
|
140
|
+
|
|
141
|
+
return right.latest.logId - left.latest.logId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function trendPercent(
|
|
145
|
+
first: SessionContextSnapshot,
|
|
146
|
+
latest: SessionContextSnapshot,
|
|
147
|
+
): number | null {
|
|
148
|
+
if (first.usagePercent === null || latest.usagePercent === null) return null;
|
|
149
|
+
return (latest.usagePercent - first.usagePercent) * 100;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function buildSessionContextSummary(
|
|
153
|
+
logs: readonly CapturedLog[],
|
|
154
|
+
providers: readonly ProviderConfig[] = [],
|
|
155
|
+
): SessionContextSummary {
|
|
156
|
+
const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
|
|
157
|
+
const byModel = new Map<string, SummaryAccumulator>();
|
|
158
|
+
|
|
159
|
+
for (const log of sortedLogs) {
|
|
160
|
+
const snapshot = snapshotFromLog(log, providers);
|
|
161
|
+
if (snapshot === null) continue;
|
|
162
|
+
|
|
163
|
+
const existing = byModel.get(snapshot.model);
|
|
164
|
+
if (existing === undefined) {
|
|
165
|
+
byModel.set(snapshot.model, {
|
|
166
|
+
model: snapshot.model,
|
|
167
|
+
requestCount: 1,
|
|
168
|
+
first: snapshot,
|
|
169
|
+
latest: snapshot,
|
|
170
|
+
peak: snapshot,
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
byModel.set(snapshot.model, {
|
|
176
|
+
model: existing.model,
|
|
177
|
+
requestCount: existing.requestCount + 1,
|
|
178
|
+
first: existing.first,
|
|
179
|
+
latest: snapshot,
|
|
180
|
+
peak: isHigherPeak(snapshot, existing.peak) ? snapshot : existing.peak,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const models = [...byModel.values()]
|
|
185
|
+
.map((entry) => ({
|
|
186
|
+
model: entry.model,
|
|
187
|
+
requestCount: entry.requestCount,
|
|
188
|
+
first: entry.first,
|
|
189
|
+
latest: entry.latest,
|
|
190
|
+
peak: entry.peak,
|
|
191
|
+
trendPercent: trendPercent(entry.first, entry.latest),
|
|
192
|
+
}))
|
|
193
|
+
.sort(compareModelSummary);
|
|
194
|
+
|
|
195
|
+
return { models };
|
|
196
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { safeGetOwnProperty } from "../../lib/objectUtils";
|
|
2
|
+
|
|
3
|
+
export const LOG_FOCUS_REQUEST_EVENT = "agent-inspector:focus-log";
|
|
4
|
+
|
|
5
|
+
export type LogFocusTab = "anatomy" | "request";
|
|
6
|
+
|
|
7
|
+
export type LogFocusRequest = {
|
|
8
|
+
logId: number;
|
|
9
|
+
tab: LogFocusTab;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function dispatchLogFocusRequest(request: LogFocusRequest): void {
|
|
13
|
+
window.dispatchEvent(new CustomEvent(LOG_FOCUS_REQUEST_EVENT, { detail: request }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readLogFocusRequest(event: Event): LogFocusRequest | null {
|
|
17
|
+
if (typeof CustomEvent === "undefined" || !(event instanceof CustomEvent)) return null;
|
|
18
|
+
|
|
19
|
+
const detail: unknown = event.detail;
|
|
20
|
+
const logId = safeGetOwnProperty(detail, "logId");
|
|
21
|
+
if (typeof logId !== "number" || !Number.isInteger(logId) || logId < 0) return null;
|
|
22
|
+
|
|
23
|
+
const tab = safeGetOwnProperty(detail, "tab");
|
|
24
|
+
switch (tab) {
|
|
25
|
+
case "anatomy":
|
|
26
|
+
case "request":
|
|
27
|
+
return { logId, tab };
|
|
28
|
+
case undefined:
|
|
29
|
+
return { logId, tab: "request" };
|
|
30
|
+
default:
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|