@tonyclaw/agent-inspector 3.0.7 → 3.0.9
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 +245 -171
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-C0Kbsm-Q.js → CompareDrawer-CJKL4LMm.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DBUy29O-.js +106 -0
- package/.output/public/assets/ReplayDialog-BYanl-a_.js +1 -0
- package/.output/public/assets/{RequestAnatomy-fa0JXy5p.js → RequestAnatomy-DvPz1jqE.js} +1 -1
- package/.output/public/assets/ResponseView-sZnxp9GN.js +3 -0
- package/.output/public/assets/StreamingChunkSequence-G-tiYiBd.js +1 -0
- package/.output/public/assets/{_sessionId-D5fYH2C0.js → _sessionId-_hDcsmXq.js} +1 -1
- package/.output/public/assets/agent-inspector-58K1_MsC.ico +0 -0
- package/.output/public/assets/index-DAPAWsAd.js +1 -0
- package/.output/public/assets/index-DspnLqUW.css +1 -0
- package/.output/public/assets/{index-BEsqvywM.js → index-SuOSQb2_.js} +1 -1
- package/.output/public/assets/{json-viewer-B0QRbMSf.js → json-viewer-yQdhgjaw.js} +1 -1
- package/.output/public/assets/{main-CuKZD7n0.js → main-CgjcDxjR.js} +2 -2
- package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -2
- package/.output/server/{_sessionId-DdBTK3qm.mjs → _sessionId-BGISf2En.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-m82cVZ73.mjs → CompareDrawer-s-UF9gW3.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-7geuf78f.mjs → ProxyViewerContainer-C0Sbz9ED.mjs} +1033 -530
- package/.output/server/_ssr/{ReplayDialog-B8RlWLLH.mjs → ReplayDialog-DvS8gvJr.mjs} +58 -23
- package/.output/server/_ssr/{RequestAnatomy-BE7pmwpv.mjs → RequestAnatomy-BwhWglJL.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-D-sDsdQr.mjs → ResponseView-wBVreqQf.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-DU_hThJr.mjs → StreamingChunkSequence-Dr6eg-MJ.mjs} +30 -20
- package/.output/server/_ssr/{index-D0sV8UbE.mjs → index-B98Wia1X.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-CaDh1zrw.mjs → json-viewer-D7L8y0FH.mjs} +2 -2
- package/.output/server/_ssr/{router-Zh2iZ_u0.mjs → router-g9mo0nWT.mjs} +459 -196
- package/.output/server/_tanstack-start-manifest_v-CbGc-o50.mjs +4 -0
- package/.output/server/index.mjs +76 -69
- package/package.json +1 -1
- package/src/assets/IDE/opencode.svg +1 -1
- package/src/assets/IDE/xiaomimimo.svg +1 -1
- package/src/cli/detect-tools.ts +2 -27
- package/src/cli/templates/codex-skill-onboard.ts +1 -1
- package/src/cli/templates/skill-onboard.ts +2 -3
- package/src/cli.ts +9 -86
- package/src/components/ProxyViewer.tsx +249 -37
- package/src/components/ProxyViewerContainer.tsx +167 -22
- package/src/components/clients/ClientLogo.tsx +38 -3
- package/src/components/ecosystem/AgentLabDialog.tsx +204 -3
- package/src/components/groups/GroupsDialog.tsx +28 -40
- package/src/components/providers/ImportWizardDialog.tsx +51 -101
- package/src/components/providers/ProvidersPanel.tsx +82 -68
- package/src/components/providers/SettingsDialog.tsx +11 -12
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +15 -23
- package/src/components/proxy-viewer/ReplayDialog.tsx +65 -22
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +33 -20
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -16
- package/src/components/proxy-viewer/log-formats/anthropic.ts +5 -5
- package/src/components/proxy-viewer/log-formats/openai.ts +7 -10
- package/src/components/proxy-viewer/requestTools.ts +5 -2
- package/src/lib/apiClient.ts +43 -10
- package/src/lib/ecosystemContract.ts +32 -0
- package/src/lib/export-logs.ts +37 -3
- package/src/proxy/ecosystemRegistry.ts +244 -0
- package/src/proxy/ecosystemTasks.ts +153 -0
- package/src/proxy/platformCommands.ts +212 -0
- package/src/proxy/socketTracker.ts +84 -143
- package/src/routes/api/ecosystem.packages.$packageId.help.ts +16 -0
- package/src/routes/api/ecosystem.packages.$packageId.install.ts +16 -0
- package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +19 -0
- package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +16 -0
- package/src/routes/api/ecosystem.packages.ts +1 -248
- package/src/routes/api/ecosystem.tasks.$taskId.ts +16 -0
- package/src/routes/api/ecosystem.tasks.ts +12 -0
- package/src/routes/api/providers.$providerId.test.log.ts +28 -24
- package/.output/public/assets/ProxyViewerContainer-p-rpYyiF.js +0 -106
- package/.output/public/assets/ReplayDialog-CM3dhP8v.js +0 -1
- package/.output/public/assets/ResponseView-Bq6sC7Ay.js +0 -3
- package/.output/public/assets/StreamingChunkSequence-CGOun3Df.js +0 -1
- package/.output/public/assets/index-C29Olfzt.js +0 -1
- package/.output/public/assets/index-DgANE0r3.css +0 -1
- package/.output/server/_tanstack-start-manifest_v-LCE-2AaF.mjs +0 -4
|
@@ -7,7 +7,13 @@ import { ImportWizardDialog } from "./ImportWizardDialog";
|
|
|
7
7
|
import { ConfirmDialog } from "../ui/confirm-dialog";
|
|
8
8
|
import { ProviderCard } from "./ProviderCard";
|
|
9
9
|
import { ProviderForm } from "./ProviderForm";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ApiTimeoutError,
|
|
12
|
+
fetchJsonWithTimeout,
|
|
13
|
+
fetchWithTimeout,
|
|
14
|
+
parseJsonResponse,
|
|
15
|
+
readApiError,
|
|
16
|
+
} from "../../lib/apiClient";
|
|
11
17
|
import { copyTextToClipboard } from "../../lib/clipboard";
|
|
12
18
|
import {
|
|
13
19
|
ProviderConfigSchema,
|
|
@@ -44,6 +50,13 @@ const ModelMetadataRefreshResponseSchema = z.object({
|
|
|
44
50
|
});
|
|
45
51
|
|
|
46
52
|
const NETWORK_ERROR_MESSAGE = "Network error: could not reach the server. Is the proxy running?";
|
|
53
|
+
const CONFIG_PATH_FETCH_TIMEOUT_MS = 5_000;
|
|
54
|
+
const PROVIDER_TEST_LOG_TIMEOUT_MS = 5_000;
|
|
55
|
+
const PROVIDER_WRITE_TIMEOUT_MS = 15_000;
|
|
56
|
+
const PROVIDER_DELETE_TIMEOUT_MS = 10_000;
|
|
57
|
+
const PROVIDER_EXPORT_TIMEOUT_MS = 20_000;
|
|
58
|
+
const PROVIDER_IMPORT_TIMEOUT_MS = 20_000;
|
|
59
|
+
const PROVIDER_METADATA_TIMEOUT_MS = 30_000;
|
|
47
60
|
|
|
48
61
|
type ProviderFormData = {
|
|
49
62
|
name: string;
|
|
@@ -88,7 +101,7 @@ async function persistProviderTestLog(providerId: string, results: TestResults):
|
|
|
88
101
|
return;
|
|
89
102
|
}
|
|
90
103
|
try {
|
|
91
|
-
await
|
|
104
|
+
await fetchWithTimeout(`/api/providers/${providerId}/test/log`, PROVIDER_TEST_LOG_TIMEOUT_MS, {
|
|
92
105
|
method: "POST",
|
|
93
106
|
headers: { "Content-Type": "application/json" },
|
|
94
107
|
body: JSON.stringify(results),
|
|
@@ -176,11 +189,12 @@ export function ProvidersPanel({
|
|
|
176
189
|
useEffect(() => {
|
|
177
190
|
void (async () => {
|
|
178
191
|
try {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
192
|
+
const data = await fetchJsonWithTimeout(
|
|
193
|
+
"/api/config/paths",
|
|
194
|
+
ConfigPathsResponseSchema,
|
|
195
|
+
CONFIG_PATH_FETCH_TIMEOUT_MS,
|
|
196
|
+
);
|
|
197
|
+
setConfigPath(data.providerConfig);
|
|
184
198
|
} catch {
|
|
185
199
|
// Ignore
|
|
186
200
|
}
|
|
@@ -270,34 +284,33 @@ export function ProvidersPanel({
|
|
|
270
284
|
}, 1000);
|
|
271
285
|
|
|
272
286
|
try {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
`HTTP ${res.status}: ${res.statusText}`,
|
|
286
|
-
"server_error",
|
|
287
|
-
),
|
|
288
|
-
);
|
|
289
|
-
}
|
|
287
|
+
const results = await fetchJsonWithTimeout(
|
|
288
|
+
`/api/providers/${providerId}/test`,
|
|
289
|
+
ProviderTestResultsSchema,
|
|
290
|
+
timeoutSeconds * 1000 + 1000,
|
|
291
|
+
{
|
|
292
|
+
method: "POST",
|
|
293
|
+
signal: controller.signal,
|
|
294
|
+
},
|
|
295
|
+
(response) => `HTTP ${response.status}: ${response.statusText}`,
|
|
296
|
+
);
|
|
297
|
+
updateTestResults(providerId, results);
|
|
298
|
+
await persistProviderTestLog(providerId, results);
|
|
290
299
|
} catch (err) {
|
|
291
300
|
// Check if this was an abort (timeout)
|
|
292
301
|
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
293
|
-
|
|
302
|
+
const isTimeout = err instanceof ApiTimeoutError;
|
|
303
|
+
if (isAbort || isTimeout) {
|
|
294
304
|
updateTestResults(
|
|
295
305
|
providerId,
|
|
296
306
|
createFailedProviderTestResults("Request timed out", "timeout"),
|
|
297
307
|
);
|
|
308
|
+
} else if (err instanceof Error) {
|
|
309
|
+
updateTestResults(
|
|
310
|
+
providerId,
|
|
311
|
+
createFailedProviderTestResults(err.message, "server_error"),
|
|
312
|
+
);
|
|
298
313
|
}
|
|
299
|
-
// If it's not an abort error, the test results won't be updated
|
|
300
|
-
// which means the previous results will persist (or it will show "Not configured" reset state)
|
|
301
314
|
} finally {
|
|
302
315
|
// Always clear the countdown and testing state, even if an error occurs
|
|
303
316
|
clearInterval(intervalId);
|
|
@@ -323,18 +336,17 @@ export function ProvidersPanel({
|
|
|
323
336
|
});
|
|
324
337
|
try {
|
|
325
338
|
const payload = url !== undefined && url.trim() !== "" ? { url } : {};
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const data = await parseJsonResponse(res, ModelMetadataRefreshResponseSchema);
|
|
339
|
+
const data = await fetchJsonWithTimeout(
|
|
340
|
+
`/api/providers/${providerId}/model-metadata`,
|
|
341
|
+
ModelMetadataRefreshResponseSchema,
|
|
342
|
+
PROVIDER_METADATA_TIMEOUT_MS,
|
|
343
|
+
{
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: { "Content-Type": "application/json" },
|
|
346
|
+
body: JSON.stringify(payload),
|
|
347
|
+
},
|
|
348
|
+
() => "Failed to refresh model metadata",
|
|
349
|
+
);
|
|
338
350
|
if (data.imported === 0 && showError) {
|
|
339
351
|
setError("No matching model metadata found for this provider.");
|
|
340
352
|
} else {
|
|
@@ -343,9 +355,9 @@ export function ProvidersPanel({
|
|
|
343
355
|
triggerHighlight(data.provider.id);
|
|
344
356
|
refreshProviders();
|
|
345
357
|
return data.imported > 0;
|
|
346
|
-
} catch {
|
|
358
|
+
} catch (err) {
|
|
347
359
|
if (showError) {
|
|
348
|
-
setError(NETWORK_ERROR_MESSAGE);
|
|
360
|
+
setError(err instanceof Error ? err.message : NETWORK_ERROR_MESSAGE);
|
|
349
361
|
}
|
|
350
362
|
return false;
|
|
351
363
|
} finally {
|
|
@@ -362,16 +374,17 @@ export function ProvidersPanel({
|
|
|
362
374
|
function handleAddProvider(data: ProviderFormData): void {
|
|
363
375
|
void (async () => {
|
|
364
376
|
try {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
377
|
+
const newProvider = await fetchJsonWithTimeout(
|
|
378
|
+
"/api/providers",
|
|
379
|
+
ProviderConfigSchema,
|
|
380
|
+
PROVIDER_WRITE_TIMEOUT_MS,
|
|
381
|
+
{
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: { "Content-Type": "application/json" },
|
|
384
|
+
body: JSON.stringify(createProviderPayload(data)),
|
|
385
|
+
},
|
|
386
|
+
() => "Failed to add provider",
|
|
387
|
+
);
|
|
375
388
|
setShowForm(false);
|
|
376
389
|
triggerHighlight(newProvider.id);
|
|
377
390
|
if (data.modelMetadataUrl !== undefined && data.modelMetadataUrl.trim() !== "") {
|
|
@@ -379,8 +392,8 @@ export function ProvidersPanel({
|
|
|
379
392
|
}
|
|
380
393
|
refreshProviders();
|
|
381
394
|
await runTest(newProvider.id);
|
|
382
|
-
} catch {
|
|
383
|
-
setError(NETWORK_ERROR_MESSAGE);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
setError(err instanceof Error ? err.message : NETWORK_ERROR_MESSAGE);
|
|
384
397
|
}
|
|
385
398
|
})();
|
|
386
399
|
}
|
|
@@ -389,16 +402,17 @@ export function ProvidersPanel({
|
|
|
389
402
|
if (!editingProvider) return;
|
|
390
403
|
void (async () => {
|
|
391
404
|
try {
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
405
|
+
const updated = await fetchJsonWithTimeout(
|
|
406
|
+
`/api/providers/${editingProvider.id}`,
|
|
407
|
+
ProviderConfigSchema,
|
|
408
|
+
PROVIDER_WRITE_TIMEOUT_MS,
|
|
409
|
+
{
|
|
410
|
+
method: "PUT",
|
|
411
|
+
headers: { "Content-Type": "application/json" },
|
|
412
|
+
body: JSON.stringify(createProviderPayload(data)),
|
|
413
|
+
},
|
|
414
|
+
() => "Failed to update provider",
|
|
415
|
+
);
|
|
402
416
|
setEditingProvider(undefined);
|
|
403
417
|
triggerHighlight(updated.id);
|
|
404
418
|
if (data.modelMetadataUrl !== undefined && data.modelMetadataUrl.trim() !== "") {
|
|
@@ -415,8 +429,8 @@ export function ProvidersPanel({
|
|
|
415
429
|
if (criticalFieldsChanged) {
|
|
416
430
|
await runTest(updated.id);
|
|
417
431
|
}
|
|
418
|
-
} catch {
|
|
419
|
-
setError(NETWORK_ERROR_MESSAGE);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
setError(err instanceof Error ? err.message : NETWORK_ERROR_MESSAGE);
|
|
420
434
|
}
|
|
421
435
|
})();
|
|
422
436
|
}
|
|
@@ -432,7 +446,7 @@ export function ProvidersPanel({
|
|
|
432
446
|
void (async () => {
|
|
433
447
|
let res: Response;
|
|
434
448
|
try {
|
|
435
|
-
res = await
|
|
449
|
+
res = await fetchWithTimeout(`/api/providers/${providerId}`, PROVIDER_DELETE_TIMEOUT_MS, {
|
|
436
450
|
method: "DELETE",
|
|
437
451
|
});
|
|
438
452
|
} catch {
|
|
@@ -453,7 +467,7 @@ export function ProvidersPanel({
|
|
|
453
467
|
const url = `/api/providers/export${includeKeys ? "?includeKeys=true" : ""}`;
|
|
454
468
|
void (async () => {
|
|
455
469
|
try {
|
|
456
|
-
const res = await
|
|
470
|
+
const res = await fetchWithTimeout(url, PROVIDER_EXPORT_TIMEOUT_MS);
|
|
457
471
|
if (!res.ok) {
|
|
458
472
|
setError("Failed to export providers");
|
|
459
473
|
return;
|
|
@@ -485,7 +499,7 @@ export function ProvidersPanel({
|
|
|
485
499
|
void (async () => {
|
|
486
500
|
try {
|
|
487
501
|
const text = await file.text();
|
|
488
|
-
const res = await
|
|
502
|
+
const res = await fetchWithTimeout("/api/providers/import", PROVIDER_IMPORT_TIMEOUT_MS, {
|
|
489
503
|
method: "POST",
|
|
490
504
|
headers: { "Content-Type": "application/json" },
|
|
491
505
|
body: JSON.stringify(text),
|
|
@@ -9,6 +9,7 @@ import { ProvidersPanel } from "./ProvidersPanel";
|
|
|
9
9
|
import { useProviders } from "../../lib/useProviders";
|
|
10
10
|
import { useStripConfig } from "../../lib/useStripConfig";
|
|
11
11
|
import { copyTextToClipboard } from "../../lib/clipboard";
|
|
12
|
+
import { fetchJsonWithTimeout } from "../../lib/apiClient";
|
|
12
13
|
import {
|
|
13
14
|
MAX_PROVIDER_TEST_TIMEOUT_SECONDS,
|
|
14
15
|
MAX_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
@@ -148,6 +149,8 @@ const LogStorageStatsSchema = z.object({
|
|
|
148
149
|
sessionArchiveBytes: z.number(),
|
|
149
150
|
});
|
|
150
151
|
|
|
152
|
+
const STORAGE_STATS_FETCH_TIMEOUT_MS = 10_000;
|
|
153
|
+
|
|
151
154
|
function formatBytes(bytes: number): string {
|
|
152
155
|
if (bytes < 1024) return `${bytes} B`;
|
|
153
156
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
@@ -165,18 +168,14 @@ function StorageSettingsTab(): JSX.Element {
|
|
|
165
168
|
setLoading(true);
|
|
166
169
|
setError(null);
|
|
167
170
|
try {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
setError("Storage stats response was not valid.");
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
setStats(parsed.data);
|
|
171
|
+
const data = await fetchJsonWithTimeout(
|
|
172
|
+
"/api/logs?stats=1",
|
|
173
|
+
LogStorageStatsSchema,
|
|
174
|
+
STORAGE_STATS_FETCH_TIMEOUT_MS,
|
|
175
|
+
undefined,
|
|
176
|
+
(response) => `Failed to load storage stats: HTTP ${String(response.status)}`,
|
|
177
|
+
);
|
|
178
|
+
setStats(data);
|
|
180
179
|
} catch (err) {
|
|
181
180
|
setError(err instanceof Error ? err.message : String(err));
|
|
182
181
|
} finally {
|
|
@@ -23,7 +23,7 @@ import { z } from "zod";
|
|
|
23
23
|
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
24
24
|
import { formatTimestampRange } from "../../lib/timeDisplay";
|
|
25
25
|
import { cn, formatTokens } from "../../lib/utils";
|
|
26
|
-
import {
|
|
26
|
+
import { fetchJsonWithTimeout, fetchWithTimeout } from "../../lib/apiClient";
|
|
27
27
|
import {
|
|
28
28
|
KnowledgeCandidateSchema,
|
|
29
29
|
type KnowledgeCandidate,
|
|
@@ -64,6 +64,10 @@ type CandidateLoadState =
|
|
|
64
64
|
| { status: "ready"; error: null }
|
|
65
65
|
| { status: "failed"; error: string };
|
|
66
66
|
|
|
67
|
+
const CANDIDATE_GENERATE_TIMEOUT_MS = 30_000;
|
|
68
|
+
const CANDIDATE_PROMOTE_TIMEOUT_MS = 15_000;
|
|
69
|
+
const CANDIDATE_UPDATE_TIMEOUT_MS = 15_000;
|
|
70
|
+
|
|
67
71
|
type TraceInsightKind = "session" | "tool" | "slow" | "failure" | "candidate";
|
|
68
72
|
|
|
69
73
|
type TraceInsight = {
|
|
@@ -655,19 +659,13 @@ export function AgentTraceSummary({
|
|
|
655
659
|
setCandidateState({ status: "loading", error: null });
|
|
656
660
|
void (async () => {
|
|
657
661
|
try {
|
|
658
|
-
const
|
|
662
|
+
const parsed = await fetchJsonWithTimeout(
|
|
659
663
|
`/api/knowledge/sessions/${encodeURIComponent(scopeId)}/candidates`,
|
|
664
|
+
CandidateResponseSchema,
|
|
665
|
+
CANDIDATE_GENERATE_TIMEOUT_MS,
|
|
660
666
|
{ method: "POST" },
|
|
667
|
+
(response) => `Candidate generation failed with ${String(response.status)}`,
|
|
661
668
|
);
|
|
662
|
-
if (!response.ok) {
|
|
663
|
-
const message = await readApiError(
|
|
664
|
-
response,
|
|
665
|
-
`Candidate generation failed with ${String(response.status)}`,
|
|
666
|
-
);
|
|
667
|
-
setCandidateState({ status: "failed", error: message });
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
const parsed = await parseJsonResponse(response, CandidateResponseSchema);
|
|
671
669
|
setCandidates(parsed.candidates);
|
|
672
670
|
setCandidatesExpanded(parsed.candidates.length > 0);
|
|
673
671
|
setCandidateState(
|
|
@@ -694,8 +692,9 @@ export function AgentTraceSummary({
|
|
|
694
692
|
|
|
695
693
|
void (async () => {
|
|
696
694
|
try {
|
|
697
|
-
const response = await
|
|
695
|
+
const response = await fetchWithTimeout(
|
|
698
696
|
`/api/knowledge/candidates/${encodeURIComponent(candidateId)}/promote`,
|
|
697
|
+
CANDIDATE_PROMOTE_TIMEOUT_MS,
|
|
699
698
|
{ method: "POST" },
|
|
700
699
|
);
|
|
701
700
|
const raw: unknown = await response.json().catch(() => null);
|
|
@@ -749,24 +748,17 @@ export function AgentTraceSummary({
|
|
|
749
748
|
});
|
|
750
749
|
|
|
751
750
|
try {
|
|
752
|
-
const
|
|
751
|
+
const parsed = await fetchJsonWithTimeout(
|
|
753
752
|
`/api/knowledge/candidates/${encodeURIComponent(candidateId)}`,
|
|
753
|
+
CandidateUpdateResponseSchema,
|
|
754
|
+
CANDIDATE_UPDATE_TIMEOUT_MS,
|
|
754
755
|
{
|
|
755
756
|
method: "PATCH",
|
|
756
757
|
headers: { "Content-Type": "application/json" },
|
|
757
758
|
body: JSON.stringify(update),
|
|
758
759
|
},
|
|
760
|
+
(response) => `Candidate update failed with ${String(response.status)}`,
|
|
759
761
|
);
|
|
760
|
-
if (!response.ok) {
|
|
761
|
-
const message = await readApiError(
|
|
762
|
-
response,
|
|
763
|
-
`Candidate update failed with ${String(response.status)}`,
|
|
764
|
-
);
|
|
765
|
-
setCandidateState({ status: "failed", error: message });
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const parsed = await parseJsonResponse(response, CandidateUpdateResponseSchema);
|
|
770
762
|
setCandidates((current) =>
|
|
771
763
|
current.map((candidate) =>
|
|
772
764
|
candidate.id === parsed.candidate.id ? parsed.candidate : candidate,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Braces, Minimize2, RotateCcw } from "lucide-react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { useProviders } from "../../lib/useProviders";
|
|
6
6
|
import { Button } from "../ui/button";
|
|
@@ -9,6 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
|
|
|
9
9
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
|
10
10
|
import { ResponseView } from "./ResponseView";
|
|
11
11
|
import { CapturedLogSchema, type CapturedLog } from "../../contracts";
|
|
12
|
+
import { ApiTimeoutError, fetchJsonWithTimeout } from "../../lib/apiClient";
|
|
12
13
|
import { dispatchLogFocusRequest } from "./logFocus";
|
|
13
14
|
import {
|
|
14
15
|
buildReplayComparisons,
|
|
@@ -53,6 +54,9 @@ const ReplayHistorySchema = z.object({
|
|
|
53
54
|
total: z.number(),
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
const REPLAY_TIMEOUT_MS = 60_000;
|
|
58
|
+
const REPLAY_HISTORY_TIMEOUT_MS = 10_000;
|
|
59
|
+
|
|
56
60
|
function formatJsonForEdit(value: string): string {
|
|
57
61
|
try {
|
|
58
62
|
const parsed: unknown = JSON.parse(value);
|
|
@@ -175,17 +179,26 @@ export function ReplayDialog({
|
|
|
175
179
|
const [replayHistory, setReplayHistory] = useState<CapturedLog[]>([]);
|
|
176
180
|
const [loading, setLoading] = useState(false);
|
|
177
181
|
const [error, setError] = useState<string | null>(null);
|
|
182
|
+
const replayAbortRef = useRef<AbortController | null>(null);
|
|
183
|
+
const replayRequestIdRef = useRef(0);
|
|
178
184
|
const jsonEditState = useMemo(() => readJsonEditState(modifiedBody), [modifiedBody]);
|
|
179
185
|
|
|
186
|
+
const cancelReplay = useCallback(() => {
|
|
187
|
+
replayRequestIdRef.current += 1;
|
|
188
|
+
replayAbortRef.current?.abort();
|
|
189
|
+
replayAbortRef.current = null;
|
|
190
|
+
setLoading(false);
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
180
193
|
const refreshReplayHistory = useCallback(async (): Promise<void> => {
|
|
181
194
|
if (!open) return;
|
|
182
195
|
try {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
setReplayHistory(
|
|
196
|
+
const data = await fetchJsonWithTimeout(
|
|
197
|
+
`/api/logs?replayOf=${String(log.id)}`,
|
|
198
|
+
ReplayHistorySchema,
|
|
199
|
+
REPLAY_HISTORY_TIMEOUT_MS,
|
|
200
|
+
);
|
|
201
|
+
setReplayHistory(data.logs);
|
|
189
202
|
} catch {
|
|
190
203
|
// Replay history is helpful context, not required for replay itself.
|
|
191
204
|
}
|
|
@@ -195,44 +208,74 @@ export function ReplayDialog({
|
|
|
195
208
|
void refreshReplayHistory();
|
|
196
209
|
}, [refreshReplayHistory]);
|
|
197
210
|
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (open) return undefined;
|
|
213
|
+
cancelReplay();
|
|
214
|
+
return undefined;
|
|
215
|
+
}, [cancelReplay, open]);
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
return () => {
|
|
219
|
+
replayRequestIdRef.current += 1;
|
|
220
|
+
replayAbortRef.current?.abort();
|
|
221
|
+
replayAbortRef.current = null;
|
|
222
|
+
};
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
198
225
|
async function handleReplay() {
|
|
199
226
|
if (!jsonEditState.valid) {
|
|
200
227
|
setError("Request body must be valid JSON before replay.");
|
|
201
228
|
return;
|
|
202
229
|
}
|
|
203
230
|
|
|
231
|
+
replayAbortRef.current?.abort();
|
|
232
|
+
const requestId = replayRequestIdRef.current + 1;
|
|
233
|
+
replayRequestIdRef.current = requestId;
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
replayAbortRef.current = controller;
|
|
236
|
+
|
|
204
237
|
setLoading(true);
|
|
205
238
|
setError(null);
|
|
206
239
|
setReplayResult(null);
|
|
207
240
|
|
|
208
241
|
try {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
const data = parsed.data;
|
|
242
|
+
const data = await fetchJsonWithTimeout(
|
|
243
|
+
`/api/logs/${log.id}/replay`,
|
|
244
|
+
ReplayResultSchema,
|
|
245
|
+
REPLAY_TIMEOUT_MS,
|
|
246
|
+
{
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: { "Content-Type": "application/json" },
|
|
249
|
+
body: JSON.stringify({ modifiedBody }),
|
|
250
|
+
signal: controller.signal,
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
if (replayRequestIdRef.current !== requestId) return;
|
|
223
254
|
setReplayResult(data);
|
|
224
255
|
await refreshReplayHistory();
|
|
225
256
|
if (!data.success) {
|
|
226
257
|
setError(data.error ?? "Replay failed");
|
|
227
258
|
}
|
|
228
259
|
} catch (err) {
|
|
260
|
+
if (replayRequestIdRef.current !== requestId) return;
|
|
261
|
+
if (
|
|
262
|
+
err instanceof ApiTimeoutError ||
|
|
263
|
+
(err instanceof DOMException && err.name === "AbortError")
|
|
264
|
+
) {
|
|
265
|
+
setError("Replay timed out or was canceled.");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
229
268
|
setError(err instanceof Error ? err.message : "Network error");
|
|
230
269
|
} finally {
|
|
231
|
-
|
|
270
|
+
if (replayRequestIdRef.current === requestId) {
|
|
271
|
+
replayAbortRef.current = null;
|
|
272
|
+
setLoading(false);
|
|
273
|
+
}
|
|
232
274
|
}
|
|
233
275
|
}
|
|
234
276
|
|
|
235
277
|
function handleClose() {
|
|
278
|
+
cancelReplay();
|
|
236
279
|
setReplayResult(null);
|
|
237
280
|
setError(null);
|
|
238
281
|
onOpenChange(false);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { memo, Suspense, useState, useEffect, useMemo, type JSX } from "react";
|
|
1
|
+
import { memo, Suspense, useState, useEffect, useMemo, useRef, type JSX } from "react";
|
|
2
2
|
import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { Badge } from "../ui/badge";
|
|
4
5
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
5
|
-
import type
|
|
6
|
+
import { StreamingChunkSchema, type StreamingChunk } from "../../contracts";
|
|
7
|
+
import { fetchJsonWithTimeout } from "../../lib/apiClient";
|
|
6
8
|
import { LazyJsonViewer } from "./lazy";
|
|
7
9
|
|
|
8
10
|
export type StreamingChunkSequenceProps = {
|
|
@@ -21,6 +23,13 @@ type ChunkGroup = {
|
|
|
21
23
|
chunks: StreamingChunk[];
|
|
22
24
|
};
|
|
23
25
|
|
|
26
|
+
const StreamingChunksResponseSchema = z.object({
|
|
27
|
+
chunks: z.array(StreamingChunkSchema),
|
|
28
|
+
truncated: z.boolean().optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const CHUNK_FETCH_TIMEOUT_MS = 10_000;
|
|
32
|
+
|
|
24
33
|
export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
25
34
|
logId,
|
|
26
35
|
truncated,
|
|
@@ -28,33 +37,37 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
|
|
|
28
37
|
const [containerExpanded, setContainerExpanded] = useState(false);
|
|
29
38
|
const [chunkState, setChunkState] = useState<ChunkState>({ status: "idle" });
|
|
30
39
|
const [expandedIndices, setExpandedIndices] = useState<Set<number>>(new Set());
|
|
40
|
+
const loadedLogIdRef = useRef<number | null>(null);
|
|
31
41
|
|
|
32
42
|
useEffect(() => {
|
|
33
|
-
if (!containerExpanded
|
|
43
|
+
if (!containerExpanded) return;
|
|
44
|
+
if (loadedLogIdRef.current === logId) return;
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
const controller = new AbortController();
|
|
36
47
|
setChunkState({ status: "loading" });
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.then((data
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
+
void fetchJsonWithTimeout(
|
|
50
|
+
`/api/logs/${logId}/chunks`,
|
|
51
|
+
StreamingChunksResponseSchema,
|
|
52
|
+
CHUNK_FETCH_TIMEOUT_MS,
|
|
53
|
+
{ signal: controller.signal },
|
|
54
|
+
() => "Chunks not found",
|
|
55
|
+
)
|
|
56
|
+
.then((data) => {
|
|
57
|
+
loadedLogIdRef.current = logId;
|
|
58
|
+
setChunkState({ status: "success", chunks: data.chunks });
|
|
49
59
|
})
|
|
50
|
-
.catch(() => {
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
.catch((error: unknown) => {
|
|
61
|
+
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
62
|
+
loadedLogIdRef.current = null;
|
|
63
|
+
setChunkState({
|
|
64
|
+
status: "error",
|
|
65
|
+
message: error instanceof Error ? error.message : "Chunk data unavailable",
|
|
66
|
+
});
|
|
54
67
|
});
|
|
55
68
|
|
|
56
69
|
return () => {
|
|
57
|
-
|
|
70
|
+
controller.abort();
|
|
58
71
|
};
|
|
59
72
|
}, [containerExpanded, logId]);
|
|
60
73
|
|
|
@@ -217,13 +217,6 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
217
217
|
}
|
|
218
218
|
}}
|
|
219
219
|
>
|
|
220
|
-
{/* Turn number */}
|
|
221
|
-
<div className="w-5 shrink-0 flex items-start pt-1.5">
|
|
222
|
-
<span className="text-[10px] text-muted-foreground/50 font-mono tabular-nums leading-none select-none">
|
|
223
|
-
{turnIndex + 1}
|
|
224
|
-
</span>
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
220
|
<div className="w-6 shrink-0 flex flex-col items-center pt-1.5 pb-1.5">
|
|
228
221
|
<div className="flex justify-center h-[calc(0.75rem-8px)]" />
|
|
229
222
|
<CollapsedCrab
|
|
@@ -344,15 +337,6 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
344
337
|
|
|
345
338
|
return (
|
|
346
339
|
<div key={log.id} className="flex items-stretch">
|
|
347
|
-
{isTurnStart ? (
|
|
348
|
-
<div className="w-5 shrink-0 flex items-start pt-1.5">
|
|
349
|
-
<span className="text-[10px] text-muted-foreground/50 font-mono tabular-nums leading-none select-none">
|
|
350
|
-
{turnIndex + 1}
|
|
351
|
-
</span>
|
|
352
|
-
</div>
|
|
353
|
-
) : (
|
|
354
|
-
<div className="w-5 shrink-0" />
|
|
355
|
-
)}
|
|
356
340
|
<ThreadConnector
|
|
357
341
|
stopReason={reason}
|
|
358
342
|
isPending={log.responseStatus === null}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnthropicRequestSchema, AnthropicResponseSchema } from "../../../contracts/anthropic";
|
|
2
2
|
import type { AnatomySegment } from "../anatomy/types";
|
|
3
3
|
import { countCharacters, estimateTokens } from "../anatomy/tokenEstimate";
|
|
4
|
+
import { parseRequestTools } from "../requestTools";
|
|
4
5
|
import type { LogFormatAdapter } from "./types";
|
|
5
6
|
import { emptyRequestAnalysis, EMPTY_RESPONSE_ANALYSIS } from "./types";
|
|
6
7
|
|
|
@@ -113,16 +114,15 @@ export const anthropicLogFormatAdapter: LogFormatAdapter = {
|
|
|
113
114
|
analyzeRequest(rawBody) {
|
|
114
115
|
if (rawBody === null) return emptyRequestAnalysis(rawBody);
|
|
115
116
|
try {
|
|
116
|
-
const
|
|
117
|
+
const parsedBody: unknown = JSON.parse(rawBody);
|
|
118
|
+
const toolCount = parseRequestTools(parsedBody)?.tools.length ?? null;
|
|
119
|
+
const result = AnthropicRequestSchema.safeParse(parsedBody);
|
|
117
120
|
if (!result.success) return emptyRequestAnalysis(rawBody);
|
|
118
121
|
return {
|
|
119
122
|
parsed: result.data,
|
|
120
123
|
comparisonValue: result.data,
|
|
121
124
|
messageCount: result.data.messages.length,
|
|
122
|
-
toolCount
|
|
123
|
-
result.data.tools !== undefined && result.data.tools.length > 0
|
|
124
|
-
? result.data.tools.length
|
|
125
|
-
: null,
|
|
125
|
+
toolCount,
|
|
126
126
|
};
|
|
127
127
|
} catch {
|
|
128
128
|
return emptyRequestAnalysis(rawBody);
|