@tonyclaw/agent-inspector 2.0.3 → 2.0.5
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-D5A4bTfV.js → CompareDrawer-3nRwtk8J.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CbW5VRER.js +101 -0
- package/.output/public/assets/ReplayDialog-Cl62N9PI.js +1 -0
- package/.output/public/assets/RequestAnatomy-DgQWGvjs.js +1 -0
- package/.output/public/assets/ResponseView-Cvc-ct4E.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-BCQaCAIe.js +1 -0
- package/.output/public/assets/_sessionId-CcD_aLGq.js +1 -0
- package/.output/public/assets/index-B_dffD3u.js +1 -0
- package/.output/public/assets/index-CX796gvi.css +1 -0
- package/.output/public/assets/{json-viewer-BbU0n8eM.js → json-viewer-IXejqXB0.js} +1 -1
- package/.output/public/assets/{main-CZT_F-gu.js → main-2NlGzgOe.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +181 -114
- package/.output/server/{_sessionId-B-s9P7fJ.mjs → _sessionId-DWCTasJU.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-C08L3UOO.mjs → CompareDrawer-DhrN1uC2.mjs} +6 -6
- package/.output/server/_ssr/{ProxyViewerContainer-CMWl3Ijy.mjs → ProxyViewerContainer-DRl51s_n.mjs} +910 -186
- package/.output/server/_ssr/{ReplayDialog-CPDo9_G5.mjs → ReplayDialog-BQT_ygxC.mjs} +240 -14
- package/.output/server/_ssr/{RequestAnatomy-D9wt_K1E.mjs → RequestAnatomy-DS2tZOgq.mjs} +5 -5
- package/.output/server/_ssr/{ResponseView-DXaL7nY3.mjs → ResponseView-e0kL2C3x.mjs} +25 -21
- package/.output/server/_ssr/{StreamingChunkSequence-B_hudZyb.mjs → StreamingChunkSequence-BJG-m7xs.mjs} +3 -3
- package/.output/server/_ssr/{index-CuE_BN86.mjs → index-Dea3OeRw.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Ci6kkjde.mjs → json-viewer-DDU55MLK.mjs} +3 -3
- package/.output/server/_ssr/{router-BemxgIg7.mjs → router-Dl7oh0zx.mjs} +164 -82
- package/.output/server/_tanstack-start-manifest_v-m-FJNBVf.mjs +4 -0
- package/.output/server/index.mjs +70 -70
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +11 -19
- package/src/components/ProxyViewer.tsx +26 -16
- package/src/components/ProxyViewerContainer.tsx +2 -1
- package/src/components/providers/ProviderCard.tsx +6 -20
- package/src/components/providers/SettingsDialog.tsx +140 -3
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +731 -72
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +16 -0
- package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
- package/src/components/proxy-viewer/ConversationGroup.tsx +12 -0
- package/src/components/proxy-viewer/ConversationHeader.tsx +6 -6
- package/src/components/proxy-viewer/LogEntry.tsx +5 -5
- package/src/components/proxy-viewer/LogEntryHeader.tsx +21 -36
- package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
- package/src/components/proxy-viewer/ResponseView.tsx +4 -8
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -17
- package/src/components/proxy-viewer/TurnGroup.tsx +18 -2
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +2 -2
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +6 -12
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +10 -14
- package/src/components/proxy-viewer/replayComparison.ts +131 -0
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +64 -22
- package/src/components/proxy-viewer/viewerState.ts +14 -2
- package/src/knowledge/candidateStore.ts +32 -1
- package/src/lib/runtimeConfig.ts +6 -0
- package/src/lib/timeDisplay.ts +22 -0
- package/src/lib/useOnboarding.ts +2 -0
- package/src/lib/useStripConfig.ts +16 -0
- package/src/proxy/config.ts +3 -0
- package/src/routes/api/config.ts +5 -1
- package/src/routes/api/knowledge.candidates.$candidateId.ts +50 -0
- package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +12 -2
- package/.output/public/assets/ProxyViewerContainer-Da0jpBkp.js +0 -101
- package/.output/public/assets/ReplayDialog-CxUk_TF0.js +0 -1
- package/.output/public/assets/RequestAnatomy-DIlzjgjJ.js +0 -1
- package/.output/public/assets/ResponseView-DQCuKJ1G.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-DHk4SGGL.js +0 -1
- package/.output/public/assets/_sessionId-dY1TTl7N.js +0 -1
- package/.output/public/assets/index-D7wwbwly.css +0 -1
- package/.output/public/assets/index-FqQZbfl2.js +0 -1
- package/.output/server/_tanstack-start-manifest_v--L1_b4sd.mjs +0 -4
|
@@ -1,28 +1,90 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState, type JSX } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
Brain,
|
|
5
|
+
ChevronDown,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
CircleCheck,
|
|
8
|
+
Clock,
|
|
9
|
+
FileSearch,
|
|
10
|
+
Loader2,
|
|
11
|
+
MessageSquare,
|
|
12
|
+
Pencil,
|
|
13
|
+
RefreshCw,
|
|
14
|
+
Save,
|
|
15
|
+
ShieldCheck,
|
|
16
|
+
UploadCloud,
|
|
17
|
+
Wrench,
|
|
18
|
+
X,
|
|
19
|
+
XCircle,
|
|
20
|
+
Zap,
|
|
21
|
+
} from "lucide-react";
|
|
3
22
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
23
|
+
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
24
|
+
import { formatTimestampRange } from "../../lib/timeDisplay";
|
|
25
|
+
import { cn, formatTokens } from "../../lib/utils";
|
|
5
26
|
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
6
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
KnowledgeCandidateSchema,
|
|
29
|
+
type KnowledgeCandidate,
|
|
30
|
+
type KnowledgeCandidateStatus,
|
|
31
|
+
} from "../../knowledge/types";
|
|
7
32
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
8
33
|
import { Badge } from "../ui/badge";
|
|
9
34
|
import { Button } from "../ui/button";
|
|
10
|
-
import { buildTraceSummary } from "./viewerState";
|
|
35
|
+
import { buildTraceSummary, extractToolTraceEvents, type TraceSummary } from "./viewerState";
|
|
11
36
|
|
|
12
37
|
const CandidateResponseSchema = z.object({
|
|
13
38
|
candidates: z.array(KnowledgeCandidateSchema),
|
|
14
39
|
});
|
|
15
40
|
|
|
41
|
+
const CandidatePromotionResponseSchema = z.object({
|
|
42
|
+
candidate: KnowledgeCandidateSchema,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const CandidatePromotionFailureResponseSchema = z.object({
|
|
46
|
+
error: z.string().optional(),
|
|
47
|
+
candidate: KnowledgeCandidateSchema.optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const CandidateUpdateResponseSchema = z.object({
|
|
51
|
+
candidate: KnowledgeCandidateSchema,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
type CandidateDraftUpdate = {
|
|
55
|
+
type: KnowledgeCandidate["type"];
|
|
56
|
+
title: string;
|
|
57
|
+
content: string;
|
|
58
|
+
tags: string[];
|
|
59
|
+
};
|
|
60
|
+
|
|
16
61
|
type CandidateLoadState =
|
|
17
62
|
| { status: "idle"; error: null }
|
|
18
63
|
| { status: "loading"; error: null }
|
|
19
64
|
| { status: "ready"; error: null }
|
|
20
65
|
| { status: "failed"; error: string };
|
|
21
66
|
|
|
67
|
+
type TraceInsightKind = "session" | "tool" | "slow" | "failure" | "candidate";
|
|
68
|
+
|
|
69
|
+
type TraceInsight = {
|
|
70
|
+
kind: TraceInsightKind;
|
|
71
|
+
title: string;
|
|
72
|
+
detail: string;
|
|
73
|
+
logId: number | null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const CANDIDATE_STATUS_CLASSES: Record<KnowledgeCandidateStatus, string> = {
|
|
77
|
+
draft: "border-amber-500/30 bg-amber-500/10 text-amber-500",
|
|
78
|
+
promoted: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500",
|
|
79
|
+
failed: "border-destructive/30 bg-destructive/10 text-destructive",
|
|
80
|
+
};
|
|
81
|
+
|
|
22
82
|
type AgentTraceSummaryProps = {
|
|
23
83
|
logs: CapturedLog[];
|
|
24
84
|
scopeId: string;
|
|
25
85
|
slowResponseThresholdSeconds: number;
|
|
86
|
+
showRollupMetrics: boolean;
|
|
87
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
26
88
|
};
|
|
27
89
|
|
|
28
90
|
function formatElapsed(ms: number | null): string {
|
|
@@ -31,60 +93,492 @@ function formatElapsed(ms: number | null): string {
|
|
|
31
93
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
32
94
|
}
|
|
33
95
|
|
|
34
|
-
function formatTimeRange(
|
|
96
|
+
function formatTimeRange(
|
|
97
|
+
startedAt: string | null,
|
|
98
|
+
endedAt: string | null,
|
|
99
|
+
timeDisplayFormat: TimeDisplayFormat,
|
|
100
|
+
): string | null {
|
|
35
101
|
if (startedAt === null || endedAt === null) return null;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
102
|
+
return formatTimestampRange(startedAt, endedAt, timeDisplayFormat);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatCandidateCount(count: number): string {
|
|
106
|
+
return `${String(count)} candidate${count === 1 ? "" : "s"}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getLogAnchor(logId: number): string {
|
|
110
|
+
return `log-${String(logId)}`;
|
|
43
111
|
}
|
|
44
112
|
|
|
45
|
-
function
|
|
46
|
-
const
|
|
113
|
+
function jumpToLog(logId: number): void {
|
|
114
|
+
const anchor = getLogAnchor(logId);
|
|
115
|
+
const target = document.getElementById(anchor);
|
|
116
|
+
window.history.replaceState(null, "", `#${anchor}`);
|
|
47
117
|
if (!(target instanceof HTMLElement)) return;
|
|
48
118
|
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
49
119
|
target.focus({ preventScroll: true });
|
|
50
|
-
|
|
51
|
-
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function firstFailureLog(logs: CapturedLog[]): CapturedLog | null {
|
|
123
|
+
for (const log of logs) {
|
|
124
|
+
if (log.responseStatus !== null && log.responseStatus >= 400) return log;
|
|
52
125
|
}
|
|
126
|
+
return null;
|
|
53
127
|
}
|
|
54
128
|
|
|
55
|
-
function
|
|
56
|
-
|
|
129
|
+
function slowestLog(logs: CapturedLog[]): CapturedLog | null {
|
|
130
|
+
let current: CapturedLog | null = null;
|
|
131
|
+
let currentElapsed = -1;
|
|
132
|
+
for (const log of logs) {
|
|
133
|
+
if (log.elapsedMs === null) continue;
|
|
134
|
+
if (log.elapsedMs > currentElapsed) {
|
|
135
|
+
current = log;
|
|
136
|
+
currentElapsed = log.elapsedMs;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return current;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function firstToolLog(logs: CapturedLog[]): CapturedLog | null {
|
|
143
|
+
for (const log of logs) {
|
|
144
|
+
if (extractToolTraceEvents(log).length > 0) return log;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildTraceInsights(input: {
|
|
150
|
+
logs: CapturedLog[];
|
|
151
|
+
scopeId: string;
|
|
152
|
+
summary: TraceSummary;
|
|
153
|
+
candidates: KnowledgeCandidate[];
|
|
154
|
+
}): TraceInsight[] {
|
|
155
|
+
const insights: TraceInsight[] = [
|
|
156
|
+
{
|
|
157
|
+
kind: "session",
|
|
158
|
+
title: `${String(input.summary.llmCallCount)} LLM call${
|
|
159
|
+
input.summary.llmCallCount === 1 ? "" : "s"
|
|
160
|
+
}`,
|
|
161
|
+
detail: `Scope ${input.scopeId}`,
|
|
162
|
+
logId: input.logs[0]?.id ?? null,
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const toolLog = firstToolLog(input.logs);
|
|
167
|
+
if (input.summary.toolCallCount > 0 && toolLog !== null) {
|
|
168
|
+
insights.push({
|
|
169
|
+
kind: "tool",
|
|
170
|
+
title: `${String(input.summary.toolCallCount)} tool call${
|
|
171
|
+
input.summary.toolCallCount === 1 ? "" : "s"
|
|
172
|
+
}`,
|
|
173
|
+
detail: `First tool evidence at #${String(toolLog.id)}`,
|
|
174
|
+
logId: toolLog.id,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const failure = firstFailureLog(input.logs);
|
|
179
|
+
if (failure !== null) {
|
|
180
|
+
insights.push({
|
|
181
|
+
kind: "failure",
|
|
182
|
+
title: `Failure #${String(failure.responseStatus)}`,
|
|
183
|
+
detail: `First failed request is #${String(failure.id)}`,
|
|
184
|
+
logId: failure.id,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const slowest = slowestLog(input.logs);
|
|
189
|
+
if (slowest !== null && input.summary.maxElapsedMs !== null) {
|
|
190
|
+
insights.push({
|
|
191
|
+
kind: "slow",
|
|
192
|
+
title: `Slowest ${formatElapsed(input.summary.maxElapsedMs)}`,
|
|
193
|
+
detail: `Max latency observed at #${String(slowest.id)}`,
|
|
194
|
+
logId: slowest.id,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (input.candidates.length > 0) {
|
|
199
|
+
const draftCount = input.candidates.filter((candidate) => candidate.status === "draft").length;
|
|
200
|
+
const promotedCount = input.candidates.filter(
|
|
201
|
+
(candidate) => candidate.status === "promoted",
|
|
202
|
+
).length;
|
|
203
|
+
insights.push({
|
|
204
|
+
kind: "candidate",
|
|
205
|
+
title: `${String(input.candidates.length)} memory candidate${
|
|
206
|
+
input.candidates.length === 1 ? "" : "s"
|
|
207
|
+
}`,
|
|
208
|
+
detail: `${String(draftCount)} draft / ${String(promotedCount)} promoted`,
|
|
209
|
+
logId: input.candidates[0]?.logIds[0] ?? null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return insights;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function insightIcon(kind: TraceInsightKind): JSX.Element {
|
|
217
|
+
switch (kind) {
|
|
218
|
+
case "session":
|
|
219
|
+
return <MessageSquare className="size-3.5 text-blue-400" />;
|
|
220
|
+
case "tool":
|
|
221
|
+
return <Wrench className="size-3.5 text-sky-400/70" />;
|
|
222
|
+
case "slow":
|
|
223
|
+
return <Clock className="size-3.5 text-amber-400" />;
|
|
224
|
+
case "failure":
|
|
225
|
+
return <XCircle className="size-3.5 text-destructive" />;
|
|
226
|
+
case "candidate":
|
|
227
|
+
return <Brain className="size-3.5 text-emerald-400" />;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function TraceInsights({ insights }: { insights: TraceInsight[] }): JSX.Element | null {
|
|
232
|
+
if (insights.length === 0) return null;
|
|
57
233
|
return (
|
|
58
|
-
<div className="mt-2 grid gap-1.5">
|
|
59
|
-
{
|
|
60
|
-
<
|
|
61
|
-
key={
|
|
62
|
-
|
|
234
|
+
<div className="mt-2 grid gap-1.5 border-t border-border/70 pt-2 md:grid-cols-2 xl:grid-cols-3">
|
|
235
|
+
{insights.map((insight) => (
|
|
236
|
+
<button
|
|
237
|
+
key={`${insight.kind}-${insight.title}`}
|
|
238
|
+
type="button"
|
|
239
|
+
className={cn(
|
|
240
|
+
"flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs",
|
|
241
|
+
"text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground",
|
|
242
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
243
|
+
)}
|
|
244
|
+
onClick={() => {
|
|
245
|
+
if (insight.logId !== null) jumpToLog(insight.logId);
|
|
246
|
+
}}
|
|
247
|
+
disabled={insight.logId === null}
|
|
63
248
|
>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
</
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
249
|
+
{insightIcon(insight.kind)}
|
|
250
|
+
<span className="min-w-0">
|
|
251
|
+
<span className="block truncate font-medium text-foreground/90">{insight.title}</span>
|
|
252
|
+
<span className="block truncate font-mono text-[10px]">{insight.detail}</span>
|
|
253
|
+
</span>
|
|
254
|
+
</button>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function candidateStatusLabel(status: KnowledgeCandidateStatus): string {
|
|
261
|
+
switch (status) {
|
|
262
|
+
case "draft":
|
|
263
|
+
return "Draft";
|
|
264
|
+
case "promoted":
|
|
265
|
+
return "Promoted";
|
|
266
|
+
case "failed":
|
|
267
|
+
return "Failed";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function candidatePromoteLabel(candidate: KnowledgeCandidate, promoting: boolean): string {
|
|
272
|
+
if (promoting) return "Promoting";
|
|
273
|
+
switch (candidate.status) {
|
|
274
|
+
case "draft":
|
|
275
|
+
return "Promote";
|
|
276
|
+
case "failed":
|
|
277
|
+
return "Retry";
|
|
278
|
+
case "promoted":
|
|
279
|
+
return "Promoted";
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function redactionLabel(candidate: KnowledgeCandidate): string {
|
|
284
|
+
if (!candidate.redaction.redacted) return "No sensitive pattern matched";
|
|
285
|
+
return `Redacted ${candidate.redaction.patterns.join(", ")}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function previewText(value: string, maxLength: number): string {
|
|
289
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
290
|
+
if (normalized.length <= maxLength) return normalized;
|
|
291
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function logRangeLabel(logIds: number[]): string {
|
|
295
|
+
const first = logIds[0];
|
|
296
|
+
const last = logIds[logIds.length - 1];
|
|
297
|
+
if (first === undefined) return "No evidence logs";
|
|
298
|
+
if (last === undefined || first === last) return `#${String(first)}`;
|
|
299
|
+
return `#${String(first)}-#${String(last)}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function tagsToText(tags: string[]): string {
|
|
303
|
+
return tags.join(", ");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function textToTags(value: string): string[] {
|
|
307
|
+
return value
|
|
308
|
+
.split(",")
|
|
309
|
+
.map((tag) => tag.trim())
|
|
310
|
+
.filter((tag) => tag.length > 0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function CandidateItem({
|
|
314
|
+
candidate,
|
|
315
|
+
isPromoting,
|
|
316
|
+
isUpdating,
|
|
317
|
+
onPromoteCandidate,
|
|
318
|
+
onUpdateCandidate,
|
|
319
|
+
}: {
|
|
320
|
+
candidate: KnowledgeCandidate;
|
|
321
|
+
isPromoting: boolean;
|
|
322
|
+
isUpdating: boolean;
|
|
323
|
+
onPromoteCandidate: (candidateId: string) => void;
|
|
324
|
+
onUpdateCandidate: (candidateId: string, update: CandidateDraftUpdate) => Promise<boolean>;
|
|
325
|
+
}): JSX.Element {
|
|
326
|
+
const [editing, setEditing] = useState(false);
|
|
327
|
+
const [draftType, setDraftType] = useState<KnowledgeCandidate["type"]>(candidate.type);
|
|
328
|
+
const [draftTitle, setDraftTitle] = useState(candidate.title);
|
|
329
|
+
const [draftContent, setDraftContent] = useState(candidate.content);
|
|
330
|
+
const [draftTags, setDraftTags] = useState(tagsToText(candidate.tags));
|
|
331
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
332
|
+
const canPromote = candidate.status !== "promoted";
|
|
333
|
+
const canEdit = candidate.status !== "promoted";
|
|
334
|
+
|
|
335
|
+
const resetDraft = useCallback(() => {
|
|
336
|
+
setDraftType(candidate.type);
|
|
337
|
+
setDraftTitle(candidate.title);
|
|
338
|
+
setDraftContent(candidate.content);
|
|
339
|
+
setDraftTags(tagsToText(candidate.tags));
|
|
340
|
+
setLocalError(null);
|
|
341
|
+
}, [candidate.content, candidate.tags, candidate.title, candidate.type]);
|
|
342
|
+
|
|
343
|
+
const saveDraft = useCallback(async () => {
|
|
344
|
+
const title = draftTitle.trim();
|
|
345
|
+
const content = draftContent.trim();
|
|
346
|
+
const tags = textToTags(draftTags);
|
|
347
|
+
if (title.length === 0) {
|
|
348
|
+
setLocalError("Title is required.");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (content.length === 0) {
|
|
352
|
+
setLocalError("Content is required.");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (tags.length === 0) {
|
|
356
|
+
setLocalError("At least one tag is required.");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
setLocalError(null);
|
|
360
|
+
const saved = await onUpdateCandidate(candidate.id, {
|
|
361
|
+
type: draftType,
|
|
362
|
+
title,
|
|
363
|
+
content,
|
|
364
|
+
tags,
|
|
365
|
+
});
|
|
366
|
+
if (saved) setEditing(false);
|
|
367
|
+
}, [candidate.id, draftContent, draftTags, draftTitle, draftType, onUpdateCandidate]);
|
|
368
|
+
|
|
369
|
+
if (editing) {
|
|
370
|
+
return (
|
|
371
|
+
<div className="rounded-md border border-border/80 bg-background/60 px-2.5 py-2">
|
|
372
|
+
<div className="grid gap-2">
|
|
373
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
374
|
+
<select
|
|
375
|
+
value={draftType}
|
|
376
|
+
disabled={isUpdating}
|
|
377
|
+
onChange={(event) => {
|
|
378
|
+
const parsed = KnowledgeCandidateSchema.shape.type.safeParse(
|
|
379
|
+
event.currentTarget.value,
|
|
380
|
+
);
|
|
381
|
+
if (parsed.success) setDraftType(parsed.data);
|
|
382
|
+
}}
|
|
383
|
+
className="h-7 rounded-md border border-input bg-background px-2 text-xs"
|
|
384
|
+
aria-label="Candidate type"
|
|
385
|
+
>
|
|
386
|
+
<option value="episode">episode</option>
|
|
387
|
+
<option value="procedure">procedure</option>
|
|
388
|
+
<option value="preference">preference</option>
|
|
389
|
+
<option value="project-fact">project-fact</option>
|
|
390
|
+
</select>
|
|
391
|
+
<input
|
|
392
|
+
value={draftTitle}
|
|
393
|
+
disabled={isUpdating}
|
|
394
|
+
onChange={(event) => setDraftTitle(event.currentTarget.value)}
|
|
395
|
+
className="h-7 min-w-[220px] flex-1 rounded-md border border-input bg-background px-2 text-xs"
|
|
396
|
+
aria-label="Candidate title"
|
|
397
|
+
/>
|
|
74
398
|
</div>
|
|
75
|
-
<
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
))}
|
|
399
|
+
<textarea
|
|
400
|
+
value={draftContent}
|
|
401
|
+
disabled={isUpdating}
|
|
402
|
+
onChange={(event) => setDraftContent(event.currentTarget.value)}
|
|
403
|
+
className="min-h-28 rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed"
|
|
404
|
+
aria-label="Candidate content"
|
|
405
|
+
/>
|
|
406
|
+
<input
|
|
407
|
+
value={draftTags}
|
|
408
|
+
disabled={isUpdating}
|
|
409
|
+
onChange={(event) => setDraftTags(event.currentTarget.value)}
|
|
410
|
+
className="h-7 rounded-md border border-input bg-background px-2 text-xs"
|
|
411
|
+
aria-label="Candidate tags"
|
|
412
|
+
/>
|
|
413
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
414
|
+
<span className="inline-flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
415
|
+
<ShieldCheck className="size-3" />
|
|
416
|
+
Saving reruns Inspector redaction before promotion.
|
|
417
|
+
</span>
|
|
418
|
+
<span className="flex-1" />
|
|
419
|
+
{localError !== null && (
|
|
420
|
+
<span className="text-[10px] text-destructive">{localError}</span>
|
|
421
|
+
)}
|
|
422
|
+
<Button
|
|
423
|
+
type="button"
|
|
424
|
+
variant="ghost"
|
|
425
|
+
size="sm"
|
|
426
|
+
className="h-7 gap-1.5 px-2 text-xs"
|
|
427
|
+
disabled={isUpdating}
|
|
428
|
+
onClick={() => {
|
|
429
|
+
resetDraft();
|
|
430
|
+
setEditing(false);
|
|
431
|
+
}}
|
|
432
|
+
>
|
|
433
|
+
<X className="size-3.5" />
|
|
434
|
+
Cancel
|
|
435
|
+
</Button>
|
|
436
|
+
<Button
|
|
437
|
+
type="button"
|
|
438
|
+
variant="outline"
|
|
439
|
+
size="sm"
|
|
440
|
+
className="h-7 gap-1.5 px-2 text-xs"
|
|
441
|
+
disabled={isUpdating}
|
|
442
|
+
onClick={() => {
|
|
443
|
+
void saveDraft();
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
{isUpdating ? (
|
|
447
|
+
<RefreshCw className="size-3.5 animate-spin" />
|
|
448
|
+
) : (
|
|
449
|
+
<Save className="size-3.5" />
|
|
450
|
+
)}
|
|
451
|
+
Save
|
|
452
|
+
</Button>
|
|
86
453
|
</div>
|
|
87
454
|
</div>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<div className="rounded-md border border-border/80 bg-background/60 px-2.5 py-2">
|
|
461
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
462
|
+
<Badge variant="outline" className="h-5 px-1.5 text-[10px] font-mono">
|
|
463
|
+
{candidate.type}
|
|
464
|
+
</Badge>
|
|
465
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium" title={candidate.title}>
|
|
466
|
+
{candidate.title}
|
|
467
|
+
</span>
|
|
468
|
+
<Badge
|
|
469
|
+
variant="outline"
|
|
470
|
+
className={cn(
|
|
471
|
+
"h-5 shrink-0 px-1.5 text-[10px] font-mono",
|
|
472
|
+
CANDIDATE_STATUS_CLASSES[candidate.status],
|
|
473
|
+
)}
|
|
474
|
+
>
|
|
475
|
+
{candidateStatusLabel(candidate.status)}
|
|
476
|
+
</Badge>
|
|
477
|
+
</div>
|
|
478
|
+
<p
|
|
479
|
+
className="mt-1 text-[11px] leading-relaxed text-muted-foreground"
|
|
480
|
+
title={candidate.content}
|
|
481
|
+
>
|
|
482
|
+
{previewText(candidate.content, 360)}
|
|
483
|
+
</p>
|
|
484
|
+
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-muted-foreground">
|
|
485
|
+
<span className="inline-flex items-center gap-1">
|
|
486
|
+
<FileSearch className="size-3" />
|
|
487
|
+
{logRangeLabel(candidate.logIds)}
|
|
488
|
+
</span>
|
|
489
|
+
<span className="inline-flex items-center gap-1">
|
|
490
|
+
<ShieldCheck className="size-3" />
|
|
491
|
+
{redactionLabel(candidate)}
|
|
492
|
+
</span>
|
|
493
|
+
{candidate.status === "promoted" && (
|
|
494
|
+
<span className="inline-flex items-center gap-1 text-emerald-500">
|
|
495
|
+
<CircleCheck className="size-3" />
|
|
496
|
+
{candidate.openClawMemoryId ?? "OpenClaw"}
|
|
497
|
+
</span>
|
|
498
|
+
)}
|
|
499
|
+
{candidate.status === "failed" && candidate.error !== null && (
|
|
500
|
+
<span className="inline-flex items-center gap-1 text-destructive">
|
|
501
|
+
<XCircle className="size-3" />
|
|
502
|
+
{candidate.error}
|
|
503
|
+
</span>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
507
|
+
{candidate.logIds.map((logId) => (
|
|
508
|
+
<a
|
|
509
|
+
key={logId}
|
|
510
|
+
href={`#${getLogAnchor(logId)}`}
|
|
511
|
+
onClick={(event) => {
|
|
512
|
+
event.preventDefault();
|
|
513
|
+
jumpToLog(logId);
|
|
514
|
+
}}
|
|
515
|
+
className="rounded border border-blue-400/25 px-1.5 py-0.5 font-mono text-[10px] text-blue-400 underline-offset-2 transition-colors hover:bg-blue-400/10 hover:text-blue-300 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
516
|
+
aria-label={`Jump to evidence log ${String(logId)}`}
|
|
517
|
+
>
|
|
518
|
+
#{logId}
|
|
519
|
+
</a>
|
|
520
|
+
))}
|
|
521
|
+
<span className="flex-1" />
|
|
522
|
+
{canEdit && (
|
|
523
|
+
<Button
|
|
524
|
+
type="button"
|
|
525
|
+
variant="ghost"
|
|
526
|
+
size="sm"
|
|
527
|
+
className="h-6 gap-1.5 px-2 text-[10px]"
|
|
528
|
+
disabled={isUpdating || isPromoting}
|
|
529
|
+
onClick={() => setEditing(true)}
|
|
530
|
+
>
|
|
531
|
+
<Pencil className="size-3" />
|
|
532
|
+
Review
|
|
533
|
+
</Button>
|
|
534
|
+
)}
|
|
535
|
+
<Button
|
|
536
|
+
type="button"
|
|
537
|
+
variant={canPromote ? "outline" : "ghost"}
|
|
538
|
+
size="sm"
|
|
539
|
+
className="h-6 gap-1.5 px-2 text-[10px]"
|
|
540
|
+
onClick={() => onPromoteCandidate(candidate.id)}
|
|
541
|
+
disabled={!canPromote || isPromoting || isUpdating}
|
|
542
|
+
>
|
|
543
|
+
{isPromoting ? (
|
|
544
|
+
<RefreshCw className="size-3 animate-spin" />
|
|
545
|
+
) : canPromote ? (
|
|
546
|
+
<UploadCloud className="size-3" />
|
|
547
|
+
) : (
|
|
548
|
+
<CircleCheck className="size-3" />
|
|
549
|
+
)}
|
|
550
|
+
{candidatePromoteLabel(candidate, isPromoting)}
|
|
551
|
+
</Button>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function CandidateList({
|
|
558
|
+
candidates,
|
|
559
|
+
promotingCandidateIds,
|
|
560
|
+
updatingCandidateIds,
|
|
561
|
+
onPromoteCandidate,
|
|
562
|
+
onUpdateCandidate,
|
|
563
|
+
}: {
|
|
564
|
+
candidates: KnowledgeCandidate[];
|
|
565
|
+
promotingCandidateIds: ReadonlySet<string>;
|
|
566
|
+
updatingCandidateIds: ReadonlySet<string>;
|
|
567
|
+
onPromoteCandidate: (candidateId: string) => void;
|
|
568
|
+
onUpdateCandidate: (candidateId: string, update: CandidateDraftUpdate) => Promise<boolean>;
|
|
569
|
+
}): JSX.Element | null {
|
|
570
|
+
if (candidates.length === 0) return null;
|
|
571
|
+
return (
|
|
572
|
+
<div className="mt-2 grid gap-1.5">
|
|
573
|
+
{candidates.map((candidate) => (
|
|
574
|
+
<CandidateItem
|
|
575
|
+
key={candidate.id}
|
|
576
|
+
candidate={candidate}
|
|
577
|
+
isPromoting={promotingCandidateIds.has(candidate.id)}
|
|
578
|
+
isUpdating={updatingCandidateIds.has(candidate.id)}
|
|
579
|
+
onPromoteCandidate={onPromoteCandidate}
|
|
580
|
+
onUpdateCandidate={onUpdateCandidate}
|
|
581
|
+
/>
|
|
88
582
|
))}
|
|
89
583
|
</div>
|
|
90
584
|
);
|
|
@@ -94,19 +588,34 @@ export function AgentTraceSummary({
|
|
|
94
588
|
logs,
|
|
95
589
|
scopeId,
|
|
96
590
|
slowResponseThresholdSeconds,
|
|
591
|
+
showRollupMetrics,
|
|
592
|
+
timeDisplayFormat,
|
|
97
593
|
}: AgentTraceSummaryProps): JSX.Element | null {
|
|
98
594
|
const [candidates, setCandidates] = useState<KnowledgeCandidate[]>([]);
|
|
595
|
+
const [candidatesExpanded, setCandidatesExpanded] = useState(true);
|
|
99
596
|
const [candidateState, setCandidateState] = useState<CandidateLoadState>({
|
|
100
597
|
status: "idle",
|
|
101
598
|
error: null,
|
|
102
599
|
});
|
|
600
|
+
const [promotingCandidateIds, setPromotingCandidateIds] = useState<ReadonlySet<string>>(
|
|
601
|
+
() => new Set(),
|
|
602
|
+
);
|
|
603
|
+
const [updatingCandidateIds, setUpdatingCandidateIds] = useState<ReadonlySet<string>>(
|
|
604
|
+
() => new Set(),
|
|
605
|
+
);
|
|
606
|
+
const hasCandidates = candidates.length > 0;
|
|
103
607
|
const summary = useMemo(
|
|
104
608
|
() => buildTraceSummary(logs, slowResponseThresholdSeconds, candidates.length),
|
|
105
609
|
[candidates.length, logs, slowResponseThresholdSeconds],
|
|
106
610
|
);
|
|
611
|
+
const traceInsights = useMemo(
|
|
612
|
+
() => buildTraceInsights({ logs, scopeId, summary, candidates }),
|
|
613
|
+
[candidates, logs, scopeId, summary],
|
|
614
|
+
);
|
|
615
|
+
const showElapsedSummary = showRollupMetrics || summary.maxElapsedMs !== null;
|
|
107
616
|
const timeRange = useMemo(
|
|
108
|
-
() => formatTimeRange(summary.startedAt, summary.endedAt),
|
|
109
|
-
[summary.endedAt, summary.startedAt],
|
|
617
|
+
() => formatTimeRange(summary.startedAt, summary.endedAt, timeDisplayFormat),
|
|
618
|
+
[summary.endedAt, summary.startedAt, timeDisplayFormat],
|
|
110
619
|
);
|
|
111
620
|
|
|
112
621
|
const createCandidates = useCallback(() => {
|
|
@@ -128,7 +637,12 @@ export function AgentTraceSummary({
|
|
|
128
637
|
}
|
|
129
638
|
const parsed = await parseJsonResponse(response, CandidateResponseSchema);
|
|
130
639
|
setCandidates(parsed.candidates);
|
|
131
|
-
|
|
640
|
+
setCandidatesExpanded(parsed.candidates.length > 0);
|
|
641
|
+
setCandidateState(
|
|
642
|
+
parsed.candidates.length > 0
|
|
643
|
+
? { status: "ready", error: null }
|
|
644
|
+
: { status: "failed", error: "No candidate was generated for this trace scope." },
|
|
645
|
+
);
|
|
132
646
|
} catch (error) {
|
|
133
647
|
setCandidateState({
|
|
134
648
|
status: "failed",
|
|
@@ -138,44 +652,159 @@ export function AgentTraceSummary({
|
|
|
138
652
|
})();
|
|
139
653
|
}, [candidateState.status, logs.length, scopeId]);
|
|
140
654
|
|
|
655
|
+
const promoteCandidate = useCallback((candidateId: string) => {
|
|
656
|
+
setPromotingCandidateIds((current) => {
|
|
657
|
+
if (current.has(candidateId)) return current;
|
|
658
|
+
const next = new Set(current);
|
|
659
|
+
next.add(candidateId);
|
|
660
|
+
return next;
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
void (async () => {
|
|
664
|
+
try {
|
|
665
|
+
const response = await fetch(
|
|
666
|
+
`/api/knowledge/candidates/${encodeURIComponent(candidateId)}/promote`,
|
|
667
|
+
{ method: "POST" },
|
|
668
|
+
);
|
|
669
|
+
const raw: unknown = await response.json().catch(() => null);
|
|
670
|
+
const parsed = CandidatePromotionResponseSchema.safeParse(raw);
|
|
671
|
+
if (response.ok && parsed.success) {
|
|
672
|
+
setCandidates((current) =>
|
|
673
|
+
current.map((candidate) =>
|
|
674
|
+
candidate.id === parsed.data.candidate.id ? parsed.data.candidate : candidate,
|
|
675
|
+
),
|
|
676
|
+
);
|
|
677
|
+
setCandidateState({ status: "ready", error: null });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const failure = CandidatePromotionFailureResponseSchema.safeParse(raw);
|
|
682
|
+
if (failure.success && failure.data.candidate !== undefined) {
|
|
683
|
+
setCandidates((current) =>
|
|
684
|
+
current.map((candidate) =>
|
|
685
|
+
candidate.id === failure.data.candidate?.id ? failure.data.candidate : candidate,
|
|
686
|
+
),
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
setCandidateState({
|
|
690
|
+
status: "failed",
|
|
691
|
+
error: failure.success
|
|
692
|
+
? (failure.data.error ?? "Candidate promotion failed")
|
|
693
|
+
: `Candidate promotion failed with ${String(response.status)}`,
|
|
694
|
+
});
|
|
695
|
+
} catch (error) {
|
|
696
|
+
setCandidateState({
|
|
697
|
+
status: "failed",
|
|
698
|
+
error: error instanceof Error ? error.message : "Candidate promotion failed",
|
|
699
|
+
});
|
|
700
|
+
} finally {
|
|
701
|
+
setPromotingCandidateIds((current) => {
|
|
702
|
+
const next = new Set(current);
|
|
703
|
+
next.delete(candidateId);
|
|
704
|
+
return next;
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
})();
|
|
708
|
+
}, []);
|
|
709
|
+
|
|
710
|
+
const updateCandidate = useCallback(
|
|
711
|
+
async (candidateId: string, update: CandidateDraftUpdate): Promise<boolean> => {
|
|
712
|
+
setUpdatingCandidateIds((current) => {
|
|
713
|
+
if (current.has(candidateId)) return current;
|
|
714
|
+
const next = new Set(current);
|
|
715
|
+
next.add(candidateId);
|
|
716
|
+
return next;
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
try {
|
|
720
|
+
const response = await fetch(
|
|
721
|
+
`/api/knowledge/candidates/${encodeURIComponent(candidateId)}`,
|
|
722
|
+
{
|
|
723
|
+
method: "PATCH",
|
|
724
|
+
headers: { "Content-Type": "application/json" },
|
|
725
|
+
body: JSON.stringify(update),
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
const message = await readApiError(
|
|
730
|
+
response,
|
|
731
|
+
`Candidate update failed with ${String(response.status)}`,
|
|
732
|
+
);
|
|
733
|
+
setCandidateState({ status: "failed", error: message });
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const parsed = await parseJsonResponse(response, CandidateUpdateResponseSchema);
|
|
738
|
+
setCandidates((current) =>
|
|
739
|
+
current.map((candidate) =>
|
|
740
|
+
candidate.id === parsed.candidate.id ? parsed.candidate : candidate,
|
|
741
|
+
),
|
|
742
|
+
);
|
|
743
|
+
setCandidateState({ status: "ready", error: null });
|
|
744
|
+
return true;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
setCandidateState({
|
|
747
|
+
status: "failed",
|
|
748
|
+
error: error instanceof Error ? error.message : "Candidate update failed",
|
|
749
|
+
});
|
|
750
|
+
return false;
|
|
751
|
+
} finally {
|
|
752
|
+
setUpdatingCandidateIds((current) => {
|
|
753
|
+
const next = new Set(current);
|
|
754
|
+
next.delete(candidateId);
|
|
755
|
+
return next;
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
[],
|
|
760
|
+
);
|
|
761
|
+
|
|
141
762
|
if (logs.length === 0) return null;
|
|
142
763
|
|
|
143
764
|
return (
|
|
144
765
|
<section className="mb-2 rounded-lg border border-border bg-muted/10 px-3 py-2">
|
|
145
766
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-xs">
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
767
|
+
{showRollupMetrics && (
|
|
768
|
+
<span className="inline-flex items-center gap-1.5 font-semibold text-foreground">
|
|
769
|
+
<MessageSquare className="size-3.5 text-blue-400" />
|
|
770
|
+
{summary.llmCallCount} LLM
|
|
771
|
+
</span>
|
|
772
|
+
)}
|
|
150
773
|
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
151
|
-
<Wrench className="size-3.5 text-
|
|
774
|
+
<Wrench className="size-3.5 text-sky-400/70" />
|
|
152
775
|
{summary.toolCallCount} tools
|
|
153
776
|
</span>
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
777
|
+
{showRollupMetrics && (
|
|
778
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
779
|
+
<Zap className="size-3.5 text-emerald-400" />
|
|
780
|
+
<span className="font-mono">
|
|
781
|
+
{formatTokens(summary.totalInputTokens)} / {formatTokens(summary.totalOutputTokens)}
|
|
782
|
+
</span>
|
|
158
783
|
</span>
|
|
159
|
-
|
|
784
|
+
)}
|
|
160
785
|
{(summary.totalCacheCreationInputTokens > 0 || summary.totalCacheReadInputTokens > 0) && (
|
|
161
786
|
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
162
787
|
<Zap className="size-3.5 text-purple-400" />
|
|
163
788
|
<span className="font-mono">
|
|
164
|
-
+{formatTokens(summary.totalCacheCreationInputTokens)} / ~
|
|
789
|
+
KV Cache +{formatTokens(summary.totalCacheCreationInputTokens)} / ~
|
|
165
790
|
{formatTokens(summary.totalCacheReadInputTokens)}
|
|
166
791
|
</span>
|
|
167
792
|
</span>
|
|
168
793
|
)}
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
794
|
+
{showElapsedSummary && (
|
|
795
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
796
|
+
<Clock className="size-3.5" />
|
|
797
|
+
{showRollupMetrics && (
|
|
798
|
+
<span className="font-mono">{formatElapsed(summary.totalElapsedMs)}</span>
|
|
799
|
+
)}
|
|
800
|
+
{summary.maxElapsedMs !== null && (
|
|
801
|
+
<span className="font-mono text-muted-foreground/70">
|
|
802
|
+
max {formatElapsed(summary.maxElapsedMs)}
|
|
803
|
+
</span>
|
|
804
|
+
)}
|
|
805
|
+
</span>
|
|
806
|
+
)}
|
|
807
|
+
{showRollupMetrics && timeRange !== null && (
|
|
179
808
|
<span className="font-mono text-muted-foreground/70">{timeRange}</span>
|
|
180
809
|
)}
|
|
181
810
|
{(summary.failedCallCount > 0 ||
|
|
@@ -190,9 +819,11 @@ export function AgentTraceSummary({
|
|
|
190
819
|
</span>
|
|
191
820
|
)}
|
|
192
821
|
<span className="flex-1" />
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
822
|
+
{hasCandidates && (
|
|
823
|
+
<Badge variant="outline" className="h-6 px-2 text-[10px] font-mono">
|
|
824
|
+
{formatCandidateCount(summary.knowledgeCandidateCount)}
|
|
825
|
+
</Badge>
|
|
826
|
+
)}
|
|
196
827
|
<Button
|
|
197
828
|
type="button"
|
|
198
829
|
variant="outline"
|
|
@@ -208,11 +839,39 @@ export function AgentTraceSummary({
|
|
|
208
839
|
)}
|
|
209
840
|
Candidate
|
|
210
841
|
</Button>
|
|
842
|
+
{hasCandidates && (
|
|
843
|
+
<Button
|
|
844
|
+
type="button"
|
|
845
|
+
variant="ghost"
|
|
846
|
+
size="icon"
|
|
847
|
+
className="size-7 text-muted-foreground"
|
|
848
|
+
onClick={() => setCandidatesExpanded((value) => !value)}
|
|
849
|
+
aria-expanded={candidatesExpanded}
|
|
850
|
+
aria-label={
|
|
851
|
+
candidatesExpanded ? "Collapse memory candidates" : "Expand memory candidates"
|
|
852
|
+
}
|
|
853
|
+
>
|
|
854
|
+
{candidatesExpanded ? (
|
|
855
|
+
<ChevronDown className="size-3.5" />
|
|
856
|
+
) : (
|
|
857
|
+
<ChevronRight className="size-3.5" />
|
|
858
|
+
)}
|
|
859
|
+
</Button>
|
|
860
|
+
)}
|
|
211
861
|
</div>
|
|
212
862
|
{candidateState.status === "failed" && (
|
|
213
863
|
<p className="mt-2 text-xs text-destructive">{candidateState.error}</p>
|
|
214
864
|
)}
|
|
215
|
-
<
|
|
865
|
+
<TraceInsights insights={traceInsights} />
|
|
866
|
+
{candidatesExpanded && (
|
|
867
|
+
<CandidateList
|
|
868
|
+
candidates={candidates}
|
|
869
|
+
promotingCandidateIds={promotingCandidateIds}
|
|
870
|
+
updatingCandidateIds={updatingCandidateIds}
|
|
871
|
+
onPromoteCandidate={promoteCandidate}
|
|
872
|
+
onUpdateCandidate={updateCandidate}
|
|
873
|
+
/>
|
|
874
|
+
)}
|
|
216
875
|
</section>
|
|
217
876
|
);
|
|
218
877
|
}
|