@tonyclaw/agent-inspector 2.0.4 → 2.0.6
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-BCH_fsLm.js → CompareDrawer-DDmqSAfl.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-Cxpdziwd.js +101 -0
- package/.output/public/assets/ReplayDialog-Bt5DGzlh.js +1 -0
- package/.output/public/assets/RequestAnatomy-BxX3_N9S.js +1 -0
- package/.output/public/assets/ResponseView-Bl_5S9gZ.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-RJMwNf6F.js +1 -0
- package/.output/public/assets/_sessionId-b4isaoDp.js +1 -0
- package/.output/public/assets/index-BZ4x5UI6.js +1 -0
- package/.output/public/assets/{index-CobXD0yH.css → index-C624DUk9.css} +1 -1
- package/.output/public/assets/{json-viewer-BrzjD7qI.js → json-viewer-CRL_gWEZ.js} +1 -1
- package/.output/public/assets/{main-mgxeUdZQ.js → main-CKnTJ4-O.js} +6 -6
- package/.output/server/_libs/lucide-react.mjs +181 -114
- package/.output/server/{_sessionId-C4xsxIWm.mjs → _sessionId-B-x9fRY3.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-DuWEpqQ7.mjs → CompareDrawer-BQVNsAY2.mjs} +6 -6
- package/.output/server/_ssr/{ProxyViewerContainer-Cckz5qKu.mjs → ProxyViewerContainer-CYm2Dw19.mjs} +766 -122
- package/.output/server/_ssr/{ReplayDialog-BDRcr8E5.mjs → ReplayDialog-CaMQBc79.mjs} +240 -14
- package/.output/server/_ssr/{RequestAnatomy-BoO2_Ij0.mjs → RequestAnatomy--P5arRH2.mjs} +236 -66
- package/.output/server/_ssr/{ResponseView-DZiPBxvO.mjs → ResponseView-RtFwNvgD.mjs} +8 -8
- package/.output/server/_ssr/{StreamingChunkSequence-D-be7KEL.mjs → StreamingChunkSequence-B5HPkzab.mjs} +3 -3
- package/.output/server/_ssr/{index-5RImHKfu.mjs → index-CZIKZU43.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-aJhb93ZK.mjs → json-viewer-d4obyRaA.mjs} +3 -3
- package/.output/server/_ssr/{router-Dgkv5nKP.mjs → router-DGPt3MUc.mjs} +145 -71
- package/.output/server/_tanstack-start-manifest_v-BzH4pNaI.mjs +4 -0
- package/.output/server/index.mjs +64 -64
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +11 -19
- package/src/components/ProxyViewer.tsx +1 -1
- package/src/components/providers/ProviderCard.tsx +6 -20
- package/src/components/providers/SettingsDialog.tsx +95 -2
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +639 -38
- package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
- package/src/components/proxy-viewer/LogEntry.tsx +4 -4
- package/src/components/proxy-viewer/LogEntryHeader.tsx +15 -25
- package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
- package/src/components/proxy-viewer/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -16
- package/src/components/proxy-viewer/TurnGroup.tsx +14 -2
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +196 -45
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +92 -67
- package/src/components/proxy-viewer/anatomy/types.ts +15 -13
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/log-formats/anthropic.ts +1 -1
- package/src/components/proxy-viewer/log-formats/openai.ts +1 -1
- package/src/components/proxy-viewer/log-formats/types.ts +1 -1
- 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/components/ui/json-viewer.tsx +1 -1
- package/src/knowledge/candidateStore.ts +32 -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-D85_UANk.js +0 -101
- package/.output/public/assets/ReplayDialog-DTeaHHit.js +0 -1
- package/.output/public/assets/RequestAnatomy-DZ8grAih.js +0 -1
- package/.output/public/assets/ResponseView-Cldm6RCi.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-3x4p-yT7.js +0 -1
- package/.output/public/assets/_sessionId-YqWFBu6d.js +0 -1
- package/.output/public/assets/index-BIw2H6jO.js +0 -1
- package/.output/server/_tanstack-start-manifest_v-B8rrWXjr.mjs +0 -4
|
@@ -4,33 +4,81 @@ import {
|
|
|
4
4
|
Brain,
|
|
5
5
|
ChevronDown,
|
|
6
6
|
ChevronRight,
|
|
7
|
+
CircleCheck,
|
|
7
8
|
Clock,
|
|
9
|
+
FileSearch,
|
|
8
10
|
Loader2,
|
|
9
11
|
MessageSquare,
|
|
12
|
+
Pencil,
|
|
13
|
+
RefreshCw,
|
|
14
|
+
Save,
|
|
15
|
+
ShieldCheck,
|
|
16
|
+
UploadCloud,
|
|
10
17
|
Wrench,
|
|
18
|
+
X,
|
|
19
|
+
XCircle,
|
|
11
20
|
Zap,
|
|
12
21
|
} from "lucide-react";
|
|
13
22
|
import { z } from "zod";
|
|
14
23
|
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
15
24
|
import { formatTimestampRange } from "../../lib/timeDisplay";
|
|
16
|
-
import { formatTokens } from "../../lib/utils";
|
|
25
|
+
import { cn, formatTokens } from "../../lib/utils";
|
|
17
26
|
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
18
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
KnowledgeCandidateSchema,
|
|
29
|
+
type KnowledgeCandidate,
|
|
30
|
+
type KnowledgeCandidateStatus,
|
|
31
|
+
} from "../../knowledge/types";
|
|
19
32
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
20
33
|
import { Badge } from "../ui/badge";
|
|
21
34
|
import { Button } from "../ui/button";
|
|
22
|
-
import { buildTraceSummary } from "./viewerState";
|
|
35
|
+
import { buildTraceSummary, extractToolTraceEvents, type TraceSummary } from "./viewerState";
|
|
23
36
|
|
|
24
37
|
const CandidateResponseSchema = z.object({
|
|
25
38
|
candidates: z.array(KnowledgeCandidateSchema),
|
|
26
39
|
});
|
|
27
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
|
+
|
|
28
61
|
type CandidateLoadState =
|
|
29
62
|
| { status: "idle"; error: null }
|
|
30
63
|
| { status: "loading"; error: null }
|
|
31
64
|
| { status: "ready"; error: null }
|
|
32
65
|
| { status: "failed"; error: string };
|
|
33
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
|
+
|
|
34
82
|
type AgentTraceSummaryProps = {
|
|
35
83
|
logs: CapturedLog[];
|
|
36
84
|
scopeId: string;
|
|
@@ -71,43 +119,466 @@ function jumpToLog(logId: number): void {
|
|
|
71
119
|
target.focus({ preventScroll: true });
|
|
72
120
|
}
|
|
73
121
|
|
|
74
|
-
function
|
|
75
|
-
|
|
122
|
+
function firstFailureLog(logs: CapturedLog[]): CapturedLog | null {
|
|
123
|
+
for (const log of logs) {
|
|
124
|
+
if (log.responseStatus !== null && log.responseStatus >= 400) return log;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
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;
|
|
76
233
|
return (
|
|
77
|
-
<div className="mt-2 grid gap-1.5">
|
|
78
|
-
{
|
|
79
|
-
<
|
|
80
|
-
key={
|
|
81
|
-
|
|
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}
|
|
82
248
|
>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
</
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
/>
|
|
93
398
|
</div>
|
|
94
|
-
<
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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>
|
|
109
453
|
</div>
|
|
110
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
|
+
/>
|
|
111
582
|
))}
|
|
112
583
|
</div>
|
|
113
584
|
);
|
|
@@ -126,11 +597,21 @@ export function AgentTraceSummary({
|
|
|
126
597
|
status: "idle",
|
|
127
598
|
error: null,
|
|
128
599
|
});
|
|
600
|
+
const [promotingCandidateIds, setPromotingCandidateIds] = useState<ReadonlySet<string>>(
|
|
601
|
+
() => new Set(),
|
|
602
|
+
);
|
|
603
|
+
const [updatingCandidateIds, setUpdatingCandidateIds] = useState<ReadonlySet<string>>(
|
|
604
|
+
() => new Set(),
|
|
605
|
+
);
|
|
129
606
|
const hasCandidates = candidates.length > 0;
|
|
130
607
|
const summary = useMemo(
|
|
131
608
|
() => buildTraceSummary(logs, slowResponseThresholdSeconds, candidates.length),
|
|
132
609
|
[candidates.length, logs, slowResponseThresholdSeconds],
|
|
133
610
|
);
|
|
611
|
+
const traceInsights = useMemo(
|
|
612
|
+
() => buildTraceInsights({ logs, scopeId, summary, candidates }),
|
|
613
|
+
[candidates, logs, scopeId, summary],
|
|
614
|
+
);
|
|
134
615
|
const showElapsedSummary = showRollupMetrics || summary.maxElapsedMs !== null;
|
|
135
616
|
const timeRange = useMemo(
|
|
136
617
|
() => formatTimeRange(summary.startedAt, summary.endedAt, timeDisplayFormat),
|
|
@@ -157,7 +638,11 @@ export function AgentTraceSummary({
|
|
|
157
638
|
const parsed = await parseJsonResponse(response, CandidateResponseSchema);
|
|
158
639
|
setCandidates(parsed.candidates);
|
|
159
640
|
setCandidatesExpanded(parsed.candidates.length > 0);
|
|
160
|
-
setCandidateState(
|
|
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
|
+
);
|
|
161
646
|
} catch (error) {
|
|
162
647
|
setCandidateState({
|
|
163
648
|
status: "failed",
|
|
@@ -167,6 +652,113 @@ export function AgentTraceSummary({
|
|
|
167
652
|
})();
|
|
168
653
|
}, [candidateState.status, logs.length, scopeId]);
|
|
169
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
|
+
|
|
170
762
|
if (logs.length === 0) return null;
|
|
171
763
|
|
|
172
764
|
return (
|
|
@@ -194,7 +786,7 @@ export function AgentTraceSummary({
|
|
|
194
786
|
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
195
787
|
<Zap className="size-3.5 text-purple-400" />
|
|
196
788
|
<span className="font-mono">
|
|
197
|
-
+{formatTokens(summary.totalCacheCreationInputTokens)} / ~
|
|
789
|
+
KV Cache +{formatTokens(summary.totalCacheCreationInputTokens)} / ~
|
|
198
790
|
{formatTokens(summary.totalCacheReadInputTokens)}
|
|
199
791
|
</span>
|
|
200
792
|
</span>
|
|
@@ -270,7 +862,16 @@ export function AgentTraceSummary({
|
|
|
270
862
|
{candidateState.status === "failed" && (
|
|
271
863
|
<p className="mt-2 text-xs text-destructive">{candidateState.error}</p>
|
|
272
864
|
)}
|
|
273
|
-
|
|
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
|
+
)}
|
|
274
875
|
</section>
|
|
275
876
|
);
|
|
276
877
|
}
|