@tonyclaw/agent-inspector 2.0.1 → 2.0.3
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/cli.js +344 -53
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-sVLGhCO3.js → CompareDrawer-D5A4bTfV.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-Da0jpBkp.js +101 -0
- package/.output/public/assets/{ReplayDialog-DxbFUqNW.js → ReplayDialog-CxUk_TF0.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CSmGQa_g.js → RequestAnatomy-DIlzjgjJ.js} +1 -1
- package/.output/public/assets/ResponseView-DQCuKJ1G.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-BzqpY0TN.js → StreamingChunkSequence-DHk4SGGL.js} +1 -1
- package/.output/public/assets/_sessionId-dY1TTl7N.js +1 -0
- package/.output/public/assets/index-D7wwbwly.css +1 -0
- package/.output/public/assets/index-FqQZbfl2.js +1 -0
- package/.output/public/assets/{json-viewer-CKNMihlh.js → json-viewer-BbU0n8eM.js} +1 -1
- package/.output/public/assets/{main-yWf8dv9w.js → main-CZT_F-gu.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +8 -8
- package/.output/server/{_sessionId-DfHd0gd8.mjs → _sessionId-B-s9P7fJ.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-DGYAUWgF.mjs → CompareDrawer-C08L3UOO.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-fawglkTo.mjs → ProxyViewerContainer-CMWl3Ijy.mjs} +414 -70
- package/.output/server/_ssr/{ReplayDialog-B4vlKa2W.mjs → ReplayDialog-CPDo9_G5.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-BNQvEIZK.mjs → RequestAnatomy-D9wt_K1E.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-X6X6G16_.mjs → ResponseView-DXaL7nY3.mjs} +4 -4
- package/.output/server/_ssr/{StreamingChunkSequence-BPVN3MnF.mjs → StreamingChunkSequence-B_hudZyb.mjs} +3 -3
- package/.output/server/_ssr/{index-CXmpc2X5.mjs → index-CuE_BN86.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-3XC3eq4R.mjs → json-viewer-Ci6kkjde.mjs} +2 -2
- package/.output/server/_ssr/{router-C0B2qvIM.mjs → router-BemxgIg7.mjs} +402 -131
- package/.output/server/{_tanstack-start-manifest_v-7tfsmd2I.mjs → _tanstack-start-manifest_v--L1_b4sd.mjs} +1 -1
- package/.output/server/index.mjs +62 -62
- package/README.md +50 -7
- package/package.json +3 -2
- package/scripts/setup-codex-skill.mjs +38 -0
- package/scripts/setup-windows-runtime.mjs +4 -3
- package/src/cli/onboard.ts +175 -68
- package/src/cli/templates/codex-skill-onboard.ts +210 -0
- package/src/components/providers/ProviderCard.tsx +2 -27
- package/src/components/providers/ProvidersPanel.tsx +16 -0
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +218 -0
- package/src/components/proxy-viewer/ConversationGroup.tsx +6 -0
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +33 -0
- package/src/components/proxy-viewer/TurnGroup.tsx +11 -1
- package/src/components/proxy-viewer/viewerState.ts +177 -0
- package/src/knowledge/openclawClient.ts +34 -5
- package/src/knowledge/openclawGatewayClient.ts +237 -0
- package/src/knowledge/openclawMarkdown.ts +146 -0
- package/src/lib/providerTestPrompt.ts +78 -0
- package/src/proxy/chunkStorage.ts +3 -4
- package/src/proxy/logger.ts +8 -15
- package/src/proxy/store.ts +8 -16
- package/src/routes/api/providers.$providerId.test.log.ts +7 -99
- package/.output/public/assets/ProxyViewerContainer-p9QvzZ6U.js +0 -101
- package/.output/public/assets/ResponseView-B5f89c8Z.js +0 -1
- package/.output/public/assets/_sessionId-BF7ftHV3.js +0 -1
- package/.output/public/assets/index-BU0PpLby.js +0 -1
- package/.output/public/assets/index-CpWG2hFn.css +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState, type JSX } from "react";
|
|
2
|
+
import { AlertTriangle, Brain, Clock, Loader2, MessageSquare, Wrench, Zap } from "lucide-react";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { formatTokens } from "../../lib/utils";
|
|
5
|
+
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
6
|
+
import { KnowledgeCandidateSchema, type KnowledgeCandidate } from "../../knowledge/types";
|
|
7
|
+
import type { CapturedLog } from "../../proxy/schemas";
|
|
8
|
+
import { Badge } from "../ui/badge";
|
|
9
|
+
import { Button } from "../ui/button";
|
|
10
|
+
import { buildTraceSummary } from "./viewerState";
|
|
11
|
+
|
|
12
|
+
const CandidateResponseSchema = z.object({
|
|
13
|
+
candidates: z.array(KnowledgeCandidateSchema),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
type CandidateLoadState =
|
|
17
|
+
| { status: "idle"; error: null }
|
|
18
|
+
| { status: "loading"; error: null }
|
|
19
|
+
| { status: "ready"; error: null }
|
|
20
|
+
| { status: "failed"; error: string };
|
|
21
|
+
|
|
22
|
+
type AgentTraceSummaryProps = {
|
|
23
|
+
logs: CapturedLog[];
|
|
24
|
+
scopeId: string;
|
|
25
|
+
slowResponseThresholdSeconds: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function formatElapsed(ms: number | null): string {
|
|
29
|
+
if (ms === null) return "-";
|
|
30
|
+
if (ms < 1000) return `${String(ms)}ms`;
|
|
31
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatTimeRange(startedAt: string | null, endedAt: string | null): string | null {
|
|
35
|
+
if (startedAt === null || endedAt === null) return null;
|
|
36
|
+
const format = (iso: string): string =>
|
|
37
|
+
new Date(iso).toLocaleTimeString([], {
|
|
38
|
+
hour: "2-digit",
|
|
39
|
+
minute: "2-digit",
|
|
40
|
+
second: "2-digit",
|
|
41
|
+
});
|
|
42
|
+
return `${format(startedAt)} - ${format(endedAt)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scrollToLog(logId: number): void {
|
|
46
|
+
const target = document.getElementById(`log-${String(logId)}`);
|
|
47
|
+
if (!(target instanceof HTMLElement)) return;
|
|
48
|
+
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
49
|
+
target.focus({ preventScroll: true });
|
|
50
|
+
if (target.getAttribute("data-nav-action") === "expand") {
|
|
51
|
+
target.click();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function CandidateList({ candidates }: { candidates: KnowledgeCandidate[] }): JSX.Element | null {
|
|
56
|
+
if (candidates.length === 0) return null;
|
|
57
|
+
return (
|
|
58
|
+
<div className="mt-2 grid gap-1.5">
|
|
59
|
+
{candidates.map((candidate) => (
|
|
60
|
+
<div
|
|
61
|
+
key={candidate.id}
|
|
62
|
+
className="rounded-md border border-border/80 bg-background/60 px-2.5 py-2"
|
|
63
|
+
>
|
|
64
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
65
|
+
<Badge variant="outline" className="h-5 px-1.5 text-[10px] font-mono">
|
|
66
|
+
{candidate.type}
|
|
67
|
+
</Badge>
|
|
68
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium" title={candidate.title}>
|
|
69
|
+
{candidate.title}
|
|
70
|
+
</span>
|
|
71
|
+
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">
|
|
72
|
+
{candidate.status}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
76
|
+
{candidate.logIds.map((logId) => (
|
|
77
|
+
<button
|
|
78
|
+
key={logId}
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => scrollToLog(logId)}
|
|
81
|
+
className="rounded border border-border px-1.5 py-0.5 font-mono text-[10px] text-blue-400 transition-colors hover:bg-muted hover:text-blue-300"
|
|
82
|
+
>
|
|
83
|
+
#{logId}
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function AgentTraceSummary({
|
|
94
|
+
logs,
|
|
95
|
+
scopeId,
|
|
96
|
+
slowResponseThresholdSeconds,
|
|
97
|
+
}: AgentTraceSummaryProps): JSX.Element | null {
|
|
98
|
+
const [candidates, setCandidates] = useState<KnowledgeCandidate[]>([]);
|
|
99
|
+
const [candidateState, setCandidateState] = useState<CandidateLoadState>({
|
|
100
|
+
status: "idle",
|
|
101
|
+
error: null,
|
|
102
|
+
});
|
|
103
|
+
const summary = useMemo(
|
|
104
|
+
() => buildTraceSummary(logs, slowResponseThresholdSeconds, candidates.length),
|
|
105
|
+
[candidates.length, logs, slowResponseThresholdSeconds],
|
|
106
|
+
);
|
|
107
|
+
const timeRange = useMemo(
|
|
108
|
+
() => formatTimeRange(summary.startedAt, summary.endedAt),
|
|
109
|
+
[summary.endedAt, summary.startedAt],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const createCandidates = useCallback(() => {
|
|
113
|
+
if (logs.length === 0 || candidateState.status === "loading") return;
|
|
114
|
+
setCandidateState({ status: "loading", error: null });
|
|
115
|
+
void (async () => {
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(
|
|
118
|
+
`/api/knowledge/sessions/${encodeURIComponent(scopeId)}/candidates`,
|
|
119
|
+
{ method: "POST" },
|
|
120
|
+
);
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
const message = await readApiError(
|
|
123
|
+
response,
|
|
124
|
+
`Candidate generation failed with ${String(response.status)}`,
|
|
125
|
+
);
|
|
126
|
+
setCandidateState({ status: "failed", error: message });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const parsed = await parseJsonResponse(response, CandidateResponseSchema);
|
|
130
|
+
setCandidates(parsed.candidates);
|
|
131
|
+
setCandidateState({ status: "ready", error: null });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
setCandidateState({
|
|
134
|
+
status: "failed",
|
|
135
|
+
error: error instanceof Error ? error.message : "Candidate response was invalid",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
}, [candidateState.status, logs.length, scopeId]);
|
|
140
|
+
|
|
141
|
+
if (logs.length === 0) return null;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<section className="mb-2 rounded-lg border border-border bg-muted/10 px-3 py-2">
|
|
145
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-xs">
|
|
146
|
+
<span className="inline-flex items-center gap-1.5 font-semibold text-foreground">
|
|
147
|
+
<MessageSquare className="size-3.5 text-blue-400" />
|
|
148
|
+
{summary.llmCallCount} LLM
|
|
149
|
+
</span>
|
|
150
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
151
|
+
<Wrench className="size-3.5 text-amber-400" />
|
|
152
|
+
{summary.toolCallCount} tools
|
|
153
|
+
</span>
|
|
154
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
155
|
+
<Zap className="size-3.5 text-emerald-400" />
|
|
156
|
+
<span className="font-mono">
|
|
157
|
+
{formatTokens(summary.totalInputTokens)} / {formatTokens(summary.totalOutputTokens)}
|
|
158
|
+
</span>
|
|
159
|
+
</span>
|
|
160
|
+
{(summary.totalCacheCreationInputTokens > 0 || summary.totalCacheReadInputTokens > 0) && (
|
|
161
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
162
|
+
<Zap className="size-3.5 text-purple-400" />
|
|
163
|
+
<span className="font-mono">
|
|
164
|
+
+{formatTokens(summary.totalCacheCreationInputTokens)} / ~
|
|
165
|
+
{formatTokens(summary.totalCacheReadInputTokens)}
|
|
166
|
+
</span>
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
170
|
+
<Clock className="size-3.5" />
|
|
171
|
+
<span className="font-mono">{formatElapsed(summary.totalElapsedMs)}</span>
|
|
172
|
+
{summary.maxElapsedMs !== null && (
|
|
173
|
+
<span className="font-mono text-muted-foreground/70">
|
|
174
|
+
max {formatElapsed(summary.maxElapsedMs)}
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
</span>
|
|
178
|
+
{timeRange !== null && (
|
|
179
|
+
<span className="font-mono text-muted-foreground/70">{timeRange}</span>
|
|
180
|
+
)}
|
|
181
|
+
{(summary.failedCallCount > 0 ||
|
|
182
|
+
summary.pendingCallCount > 0 ||
|
|
183
|
+
summary.slowCallCount > 0) && (
|
|
184
|
+
<span className="inline-flex items-center gap-1.5 text-amber-400">
|
|
185
|
+
<AlertTriangle className="size-3.5" />
|
|
186
|
+
<span className="font-mono">
|
|
187
|
+
{summary.failedCallCount} fail / {summary.pendingCallCount} pending /{" "}
|
|
188
|
+
{summary.slowCallCount} slow
|
|
189
|
+
</span>
|
|
190
|
+
</span>
|
|
191
|
+
)}
|
|
192
|
+
<span className="flex-1" />
|
|
193
|
+
<Badge variant="outline" className="h-6 px-2 text-[10px] font-mono">
|
|
194
|
+
{summary.knowledgeCandidateCount} memory
|
|
195
|
+
</Badge>
|
|
196
|
+
<Button
|
|
197
|
+
type="button"
|
|
198
|
+
variant="outline"
|
|
199
|
+
size="sm"
|
|
200
|
+
className="h-7 gap-1.5 px-2 text-xs"
|
|
201
|
+
onClick={createCandidates}
|
|
202
|
+
disabled={candidateState.status === "loading"}
|
|
203
|
+
>
|
|
204
|
+
{candidateState.status === "loading" ? (
|
|
205
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
206
|
+
) : (
|
|
207
|
+
<Brain className="size-3.5" />
|
|
208
|
+
)}
|
|
209
|
+
Candidate
|
|
210
|
+
</Button>
|
|
211
|
+
</div>
|
|
212
|
+
{candidateState.status === "failed" && (
|
|
213
|
+
<p className="mt-2 text-xs text-destructive">{candidateState.error}</p>
|
|
214
|
+
)}
|
|
215
|
+
<CandidateList candidates={candidates} />
|
|
216
|
+
</section>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ConversationGroupData,
|
|
9
9
|
} from "./ConversationHeader";
|
|
10
10
|
import { TurnGroup } from "./TurnGroup";
|
|
11
|
+
import { AgentTraceSummary } from "./AgentTraceSummary";
|
|
11
12
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
12
13
|
import { buildTurnGroups, shouldRenderConversationContent } from "./viewerState";
|
|
13
14
|
|
|
@@ -87,6 +88,11 @@ export const ConversationGroup = memo(function ({
|
|
|
87
88
|
|
|
88
89
|
{shouldRenderConversationContent(standalone, expanded) && (
|
|
89
90
|
<div>
|
|
91
|
+
<AgentTraceSummary
|
|
92
|
+
logs={group.logs}
|
|
93
|
+
scopeId={group.conversationId}
|
|
94
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
95
|
+
/>
|
|
90
96
|
{turnGroups.map((tg) => (
|
|
91
97
|
<TurnGroup
|
|
92
98
|
key={tg.turnIndex}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type JSX } from "react";
|
|
2
|
+
import { ChevronRight, Wrench } from "lucide-react";
|
|
3
|
+
import type { ToolTraceEvent } from "./viewerState";
|
|
4
|
+
|
|
5
|
+
type ToolTraceEventsProps = {
|
|
6
|
+
events: ToolTraceEvent[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function ToolTraceEvents({ events }: ToolTraceEventsProps): JSX.Element | null {
|
|
10
|
+
if (events.length === 0) return null;
|
|
11
|
+
return (
|
|
12
|
+
<div className="mx-3 mb-2 grid gap-1.5">
|
|
13
|
+
{events.map((event) => (
|
|
14
|
+
<div
|
|
15
|
+
key={event.id}
|
|
16
|
+
className="flex min-w-0 items-center gap-2 rounded-md border border-amber-500/20 bg-amber-500/5 px-2.5 py-1.5 text-xs"
|
|
17
|
+
>
|
|
18
|
+
<Wrench className="size-3.5 shrink-0 text-amber-400" />
|
|
19
|
+
<span className="font-mono font-semibold text-amber-300">{event.name}</span>
|
|
20
|
+
<span className="font-mono text-muted-foreground">#{event.logId}</span>
|
|
21
|
+
{event.argumentsPreview !== null && (
|
|
22
|
+
<>
|
|
23
|
+
<ChevronRight className="size-3 shrink-0 text-muted-foreground/60" />
|
|
24
|
+
<span className="min-w-0 truncate font-mono text-muted-foreground">
|
|
25
|
+
{event.argumentsPreview}
|
|
26
|
+
</span>
|
|
27
|
+
</>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -9,7 +9,8 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/
|
|
|
9
9
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
10
10
|
import { LogEntry } from "./LogEntry";
|
|
11
11
|
import { ThreadConnector } from "./ThreadConnector";
|
|
12
|
-
import {
|
|
12
|
+
import { ToolTraceEvents } from "./ToolTraceEvents";
|
|
13
|
+
import { extractToolTraceEvents, isTurnCollapsible, type TurnEntry } from "./viewerState";
|
|
13
14
|
|
|
14
15
|
function formatElapsed(ms: number): string {
|
|
15
16
|
if (ms < 1000) return `${ms}ms`;
|
|
@@ -106,6 +107,14 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
106
107
|
aggregate.maxElapsed !== null &&
|
|
107
108
|
slowResponseThresholdSeconds > 0 &&
|
|
108
109
|
aggregate.maxElapsed > slowResponseThresholdSeconds * 1000;
|
|
110
|
+
const toolEventsByLogId = useMemo(() => {
|
|
111
|
+
const events = new Map<number, ReturnType<typeof extractToolTraceEvents>>();
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const extracted = extractToolTraceEvents(entry.log);
|
|
114
|
+
if (extracted.length > 0) events.set(entry.log.id, extracted);
|
|
115
|
+
}
|
|
116
|
+
return events;
|
|
117
|
+
}, [entries]);
|
|
109
118
|
|
|
110
119
|
// ResizeObserver → re-render connectors when any LogEntry height changes
|
|
111
120
|
const [layoutVersion, setLayoutVersion] = useState(0);
|
|
@@ -327,6 +336,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
327
336
|
comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
|
|
328
337
|
}
|
|
329
338
|
/>
|
|
339
|
+
<ToolTraceEvents events={toolEventsByLogId.get(log.id) ?? []} />
|
|
330
340
|
</div>
|
|
331
341
|
</div>
|
|
332
342
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extractStopReason, isTurnBoundary, type StopReason } from "../../lib/stopReason";
|
|
2
|
+
import { safeGetOwnProperty } from "../../lib/objectUtils";
|
|
2
3
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
3
4
|
import { resolveLogFormat } from "./log-formats";
|
|
4
5
|
|
|
@@ -16,6 +17,34 @@ type ConversationLike = {
|
|
|
16
17
|
logs: CapturedLog[];
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
export type TraceSummary = {
|
|
21
|
+
llmCallCount: number;
|
|
22
|
+
toolCallCount: number;
|
|
23
|
+
failedCallCount: number;
|
|
24
|
+
pendingCallCount: number;
|
|
25
|
+
slowCallCount: number;
|
|
26
|
+
totalInputTokens: number;
|
|
27
|
+
totalOutputTokens: number;
|
|
28
|
+
totalCacheCreationInputTokens: number;
|
|
29
|
+
totalCacheReadInputTokens: number;
|
|
30
|
+
totalElapsedMs: number;
|
|
31
|
+
maxElapsedMs: number | null;
|
|
32
|
+
startedAt: string | null;
|
|
33
|
+
endedAt: string | null;
|
|
34
|
+
knowledgeCandidateCount: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ToolTraceEvent = {
|
|
38
|
+
id: string;
|
|
39
|
+
logId: number;
|
|
40
|
+
index: number;
|
|
41
|
+
provider: "anthropic" | "openai";
|
|
42
|
+
name: string;
|
|
43
|
+
argumentsPreview: string | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const PREVIEW_LIMIT = 180;
|
|
47
|
+
|
|
19
48
|
export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
|
|
20
49
|
return standalone || expanded;
|
|
21
50
|
}
|
|
@@ -64,3 +93,151 @@ export function buildValidPredecessors(groups: ConversationLike[]): Map<number,
|
|
|
64
93
|
|
|
65
94
|
return predecessors;
|
|
66
95
|
}
|
|
96
|
+
|
|
97
|
+
function parseJsonResponse(responseText: string | null): unknown {
|
|
98
|
+
if (responseText === null) return null;
|
|
99
|
+
try {
|
|
100
|
+
const parsed: unknown = JSON.parse(responseText);
|
|
101
|
+
if (typeof parsed === "string") {
|
|
102
|
+
return JSON.parse(parsed);
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function previewValue(value: unknown): string | null {
|
|
111
|
+
if (value === undefined || value === null) return null;
|
|
112
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
113
|
+
if (raw === undefined) return null;
|
|
114
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
115
|
+
if (normalized.length === 0) return null;
|
|
116
|
+
return normalized.length > PREVIEW_LIMIT
|
|
117
|
+
? `${normalized.slice(0, PREVIEW_LIMIT - 1)}...`
|
|
118
|
+
: normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
122
|
+
const parsed = parseJsonResponse(log.responseText);
|
|
123
|
+
const content = safeGetOwnProperty(parsed, "content");
|
|
124
|
+
if (!Array.isArray(content)) return [];
|
|
125
|
+
|
|
126
|
+
const events: ToolTraceEvent[] = [];
|
|
127
|
+
for (const block of content) {
|
|
128
|
+
const type = safeGetOwnProperty(block, "type");
|
|
129
|
+
if (type !== "tool_use") continue;
|
|
130
|
+
const name = safeGetOwnProperty(block, "name");
|
|
131
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
132
|
+
events.push({
|
|
133
|
+
id: `${String(log.id)}-anthropic-tool-${String(events.length)}`,
|
|
134
|
+
logId: log.id,
|
|
135
|
+
index: events.length,
|
|
136
|
+
provider: "anthropic",
|
|
137
|
+
name,
|
|
138
|
+
argumentsPreview: previewValue(safeGetOwnProperty(block, "input")),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return events;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
145
|
+
const parsed = parseJsonResponse(log.responseText);
|
|
146
|
+
const choices = safeGetOwnProperty(parsed, "choices");
|
|
147
|
+
if (!Array.isArray(choices)) return [];
|
|
148
|
+
|
|
149
|
+
const events: ToolTraceEvent[] = [];
|
|
150
|
+
for (const choice of choices) {
|
|
151
|
+
const message = safeGetOwnProperty(choice, "message");
|
|
152
|
+
const toolCalls = safeGetOwnProperty(message, "tool_calls");
|
|
153
|
+
if (!Array.isArray(toolCalls)) continue;
|
|
154
|
+
for (const call of toolCalls) {
|
|
155
|
+
const fn = safeGetOwnProperty(call, "function");
|
|
156
|
+
const name = safeGetOwnProperty(fn, "name");
|
|
157
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
158
|
+
events.push({
|
|
159
|
+
id: `${String(log.id)}-openai-tool-${String(events.length)}`,
|
|
160
|
+
logId: log.id,
|
|
161
|
+
index: events.length,
|
|
162
|
+
provider: "openai",
|
|
163
|
+
name,
|
|
164
|
+
argumentsPreview: previewValue(safeGetOwnProperty(fn, "arguments")),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return events;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function extractToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
172
|
+
const format = resolveLogFormat(log);
|
|
173
|
+
switch (format) {
|
|
174
|
+
case "anthropic":
|
|
175
|
+
return extractAnthropicToolTraceEvents(log);
|
|
176
|
+
case "openai":
|
|
177
|
+
return extractOpenAIToolTraceEvents(log);
|
|
178
|
+
case "unknown":
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildTraceSummary(
|
|
184
|
+
logs: CapturedLog[],
|
|
185
|
+
slowResponseThresholdSeconds: number,
|
|
186
|
+
knowledgeCandidateCount = 0,
|
|
187
|
+
): TraceSummary {
|
|
188
|
+
let failedCallCount = 0;
|
|
189
|
+
let pendingCallCount = 0;
|
|
190
|
+
let slowCallCount = 0;
|
|
191
|
+
let totalInputTokens = 0;
|
|
192
|
+
let totalOutputTokens = 0;
|
|
193
|
+
let totalCacheCreationInputTokens = 0;
|
|
194
|
+
let totalCacheReadInputTokens = 0;
|
|
195
|
+
let totalElapsedMs = 0;
|
|
196
|
+
let maxElapsedMs: number | null = null;
|
|
197
|
+
let toolCallCount = 0;
|
|
198
|
+
|
|
199
|
+
for (const log of logs) {
|
|
200
|
+
if (log.responseStatus === null) {
|
|
201
|
+
pendingCallCount += 1;
|
|
202
|
+
} else if (log.responseStatus >= 400) {
|
|
203
|
+
failedCallCount += 1;
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
log.elapsedMs !== null &&
|
|
207
|
+
slowResponseThresholdSeconds > 0 &&
|
|
208
|
+
log.elapsedMs > slowResponseThresholdSeconds * 1000
|
|
209
|
+
) {
|
|
210
|
+
slowCallCount += 1;
|
|
211
|
+
}
|
|
212
|
+
if (log.inputTokens !== null) totalInputTokens += log.inputTokens;
|
|
213
|
+
if (log.outputTokens !== null) totalOutputTokens += log.outputTokens;
|
|
214
|
+
if (log.cacheCreationInputTokens !== null) {
|
|
215
|
+
totalCacheCreationInputTokens += log.cacheCreationInputTokens;
|
|
216
|
+
}
|
|
217
|
+
if (log.cacheReadInputTokens !== null) {
|
|
218
|
+
totalCacheReadInputTokens += log.cacheReadInputTokens;
|
|
219
|
+
}
|
|
220
|
+
if (log.elapsedMs !== null) {
|
|
221
|
+
totalElapsedMs += log.elapsedMs;
|
|
222
|
+
maxElapsedMs = maxElapsedMs === null ? log.elapsedMs : Math.max(maxElapsedMs, log.elapsedMs);
|
|
223
|
+
}
|
|
224
|
+
toolCallCount += extractToolTraceEvents(log).length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
llmCallCount: logs.length,
|
|
229
|
+
toolCallCount,
|
|
230
|
+
failedCallCount,
|
|
231
|
+
pendingCallCount,
|
|
232
|
+
slowCallCount,
|
|
233
|
+
totalInputTokens,
|
|
234
|
+
totalOutputTokens,
|
|
235
|
+
totalCacheCreationInputTokens,
|
|
236
|
+
totalCacheReadInputTokens,
|
|
237
|
+
totalElapsedMs,
|
|
238
|
+
maxElapsedMs,
|
|
239
|
+
startedAt: logs[0]?.timestamp ?? null,
|
|
240
|
+
endedAt: logs[logs.length - 1]?.timestamp ?? null,
|
|
241
|
+
knowledgeCandidateCount,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { KnowledgeCandidate, KnowledgeSearchResponse, OpenClawMemoryPayload } from "./types";
|
|
3
3
|
import { KnowledgeSearchResponseSchema, KnowledgeSearchResultSchema } from "./types";
|
|
4
|
+
import { isOpenClawGatewayConfigured, searchOpenClawGateway } from "./openclawGatewayClient";
|
|
5
|
+
import { isOpenClawFileBridgeConfigured, writeCandidateToOpenClawMemory } from "./openclawMarkdown";
|
|
4
6
|
import { redactCandidate } from "./redactor";
|
|
5
7
|
|
|
6
8
|
const OpenClawWriteResponseSchema = z.object({
|
|
@@ -18,12 +20,16 @@ export type PromoteResult =
|
|
|
18
20
|
error: string;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
|
-
function
|
|
23
|
+
function getLegacyEndpoint(): string | null {
|
|
22
24
|
const endpoint = process.env["OPENCLAW_MEMORY_URL"] ?? process.env["OPENCLAW_API_URL"];
|
|
23
25
|
if (endpoint === undefined || endpoint.trim() === "") return null;
|
|
24
26
|
return endpoint.trim();
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function isLegacyBackendConfigured(): boolean {
|
|
30
|
+
return getLegacyEndpoint() !== null;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
function buildMemoryUrl(endpoint: string): string {
|
|
28
34
|
return endpoint.endsWith("/memories") ? endpoint : `${endpoint.replace(/\/$/, "")}/memories`;
|
|
29
35
|
}
|
|
@@ -61,11 +67,21 @@ export function buildOpenClawPayload(candidate: KnowledgeCandidate): OpenClawMem
|
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export async function promoteToOpenClaw(candidate: KnowledgeCandidate): Promise<PromoteResult> {
|
|
64
|
-
|
|
70
|
+
if (isOpenClawFileBridgeConfigured()) {
|
|
71
|
+
const result = await writeCandidateToOpenClawMemory(candidate);
|
|
72
|
+
if (result.success) {
|
|
73
|
+
return { success: true, memoryId: result.memoryId };
|
|
74
|
+
}
|
|
75
|
+
return { success: false, error: result.error };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const endpoint = getLegacyEndpoint();
|
|
65
79
|
if (endpoint === null) {
|
|
66
80
|
return {
|
|
67
81
|
success: false,
|
|
68
|
-
error:
|
|
82
|
+
error:
|
|
83
|
+
"OpenClaw memory backend is not configured. Set OPENCLAW_WORKSPACE_DIR " +
|
|
84
|
+
"for file-backed memory or OPENCLAW_MEMORY_URL for a legacy HTTP backend.",
|
|
69
85
|
};
|
|
70
86
|
}
|
|
71
87
|
|
|
@@ -91,11 +107,20 @@ export async function searchOpenClaw(
|
|
|
91
107
|
query: string,
|
|
92
108
|
project?: string,
|
|
93
109
|
): Promise<KnowledgeSearchResponse> {
|
|
94
|
-
|
|
110
|
+
if (isOpenClawGatewayConfigured()) {
|
|
111
|
+
return await searchOpenClawGateway(query, project);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const endpoint = getLegacyEndpoint();
|
|
95
115
|
if (endpoint === null) {
|
|
116
|
+
const workspaceHint = isOpenClawFileBridgeConfigured()
|
|
117
|
+
? " OPENCLAW_WORKSPACE_DIR is configured for writes, but search requires OPENCLAW_GATEWAY_URL."
|
|
118
|
+
: "";
|
|
96
119
|
return {
|
|
97
120
|
results: [],
|
|
98
|
-
warning:
|
|
121
|
+
warning:
|
|
122
|
+
"OpenClaw search backend is not configured. Set OPENCLAW_GATEWAY_URL " +
|
|
123
|
+
`or OPENCLAW_MEMORY_URL.${workspaceHint}`,
|
|
99
124
|
};
|
|
100
125
|
}
|
|
101
126
|
|
|
@@ -116,3 +141,7 @@ export async function searchOpenClaw(
|
|
|
116
141
|
}
|
|
117
142
|
return { results: [], warning: "OpenClaw search returned an unparseable response." };
|
|
118
143
|
}
|
|
144
|
+
|
|
145
|
+
export function isOpenClawPromotionConfigured(): boolean {
|
|
146
|
+
return isOpenClawFileBridgeConfigured() || isLegacyBackendConfigured();
|
|
147
|
+
}
|