@tonyclaw/agent-inspector 2.0.24 → 2.0.26
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-DrY-gAzy.js → CompareDrawer-DBTFzxG5.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-ivZk8MgE.js +115 -0
- package/.output/public/assets/{ReplayDialog-CFyw6Vu4.js → ReplayDialog-syW2hDNE.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DHGJ9py7.js → RequestAnatomy-BkuRtY9o.js} +1 -1
- package/.output/public/assets/{ResponseView-CB_3yc08.js → ResponseView-BwyvEvoE.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-CqhHwgDn.js → StreamingChunkSequence-BY4MbPKg.js} +1 -1
- package/.output/public/assets/_sessionId-XlPUgIIb.js +1 -0
- package/.output/public/assets/index-By11a28-.js +1 -0
- package/.output/public/assets/index-DsiKfWCp.css +1 -0
- package/.output/public/assets/{main-CVw7-JyE.js → main-DKRDRBdd.js} +2 -2
- package/.output/server/{_sessionId-V33sBWBC.mjs → _sessionId-VDd4N_1B.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-C_Fk9joM.mjs → CompareDrawer-BQZlxsOC.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-CyFKksQa.mjs → ProxyViewerContainer-njY2oQCc.mjs} +208 -22
- package/.output/server/_ssr/{ReplayDialog-BpQHN7O2.mjs → ReplayDialog-B8lkdAIh.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-CuhmKAma.mjs → RequestAnatomy-CwOTfdwS.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-CO6LyBmj.mjs → ResponseView-3Iz_mZpd.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-DLeVetNR.mjs → StreamingChunkSequence-hmJcQNW5.mjs} +2 -2
- package/.output/server/_ssr/{index-DWUKl9Bf.mjs → index-UuTKM4aC.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BrK_DcTS.mjs → router-C7InHrxE.mjs} +352 -10
- package/.output/server/{_tanstack-start-manifest_v-BjW2XeWE.mjs → _tanstack-start-manifest_v-B6yfnMHA.mjs} +1 -1
- package/.output/server/index.mjs +58 -58
- package/package.json +1 -1
- package/src/components/providers/ProviderForm.tsx +176 -16
- package/src/lib/sessionInfoContract.ts +69 -0
- package/src/lib/utils.ts +46 -0
- package/src/mcp/server.ts +26 -0
- package/src/mcp/toolHandlers.ts +27 -0
- package/src/proxy/sessionInfo.ts +222 -0
- package/src/proxy/sessionSupervisor.ts +8 -5
- package/src/proxy/store.ts +77 -0
- package/src/routes/api/sessions.ts +29 -2
- package/.output/public/assets/ProxyViewerContainer-CiLvRNie.js +0 -115
- package/.output/public/assets/_sessionId-CgG9CQ9r.js +0 -1
- package/.output/public/assets/index-780zTVwp.css +0 -1
- package/.output/public/assets/index-CViEXkqa.js +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type JSX, useState, useEffect, useRef } from "react";
|
|
2
2
|
import { Button } from "../ui/button";
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
|
3
4
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
4
5
|
import { Eye, EyeOff, Copy, Check, ChevronDown } from "lucide-react";
|
|
5
6
|
import type { ProviderConfig, ProviderModelMetadata } from "../../lib/providerContract";
|
|
6
7
|
import { maskApiKey } from "../../lib/mask";
|
|
8
|
+
import { formatContextWindowInput, parseContextWindowTokensInput } from "../../lib/utils";
|
|
7
9
|
|
|
8
10
|
const ZHIPU_PROVIDER_KEYWORDS = ["zhipu", "zhipuai", "bigmodel", "z.ai", "zai", "glm"];
|
|
9
11
|
const ZHIPU_CODING_PROVIDER_KEYWORDS = [
|
|
@@ -129,11 +131,29 @@ type ProviderFormProps = {
|
|
|
129
131
|
|
|
130
132
|
type ModelMetadataDraft = {
|
|
131
133
|
contextWindow: string;
|
|
134
|
+
contextWindowMode: ContextWindowMode;
|
|
132
135
|
outputLimit: string;
|
|
133
136
|
};
|
|
134
137
|
|
|
138
|
+
type ContextWindowMode = "unset" | "preset" | "custom";
|
|
139
|
+
|
|
140
|
+
type ContextWindowUnit = "K" | "M" | "tokens";
|
|
141
|
+
|
|
142
|
+
const CONTEXT_WINDOW_UNSET = "__unset__";
|
|
143
|
+
const CONTEXT_WINDOW_CUSTOM = "__custom__";
|
|
144
|
+
|
|
145
|
+
const CONTEXT_WINDOW_PRESETS = [
|
|
146
|
+
{ value: "32K", label: "Small" },
|
|
147
|
+
{ value: "64K", label: "Standard" },
|
|
148
|
+
{ value: "128K", label: "Medium" },
|
|
149
|
+
{ value: "200K", label: "Long" },
|
|
150
|
+
{ value: "256K", label: "Extended" },
|
|
151
|
+
{ value: "512K", label: "Large" },
|
|
152
|
+
{ value: "1M", label: "Max" },
|
|
153
|
+
];
|
|
154
|
+
|
|
135
155
|
function emptyMetadataDraft(): ModelMetadataDraft {
|
|
136
|
-
return { contextWindow: "", outputLimit: "" };
|
|
156
|
+
return { contextWindow: "", contextWindowMode: "unset", outputLimit: "" };
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
function normalizeModelName(value: string): string {
|
|
@@ -152,14 +172,74 @@ function findModelMetadata(
|
|
|
152
172
|
);
|
|
153
173
|
}
|
|
154
174
|
|
|
175
|
+
function findContextWindowPreset(value: string): string | null {
|
|
176
|
+
const tokens = parseContextWindowTokensInput(value);
|
|
177
|
+
if (tokens === null) return null;
|
|
178
|
+
for (const preset of CONTEXT_WINDOW_PRESETS) {
|
|
179
|
+
if (parseContextWindowTokensInput(preset.value) === tokens) return preset.value;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function contextWindowModeForValue(value: string): ContextWindowMode {
|
|
185
|
+
if (value.trim() === "") return "unset";
|
|
186
|
+
return findContextWindowPreset(value) !== null ? "preset" : "custom";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function selectedContextWindowPresetValue(draft: ModelMetadataDraft): string {
|
|
190
|
+
switch (draft.contextWindowMode) {
|
|
191
|
+
case "unset":
|
|
192
|
+
return CONTEXT_WINDOW_UNSET;
|
|
193
|
+
case "custom":
|
|
194
|
+
return CONTEXT_WINDOW_CUSTOM;
|
|
195
|
+
case "preset":
|
|
196
|
+
return findContextWindowPreset(draft.contextWindow) ?? CONTEXT_WINDOW_CUSTOM;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function splitContextWindowEditorValue(value: string): { amount: string; unit: ContextWindowUnit } {
|
|
201
|
+
const trimmed = value.trim().replaceAll(",", "");
|
|
202
|
+
if (trimmed === "") return { amount: "", unit: "K" };
|
|
203
|
+
const match = /^(\d+(?:\.\d+)?)\s*([kKmM])?$/.exec(trimmed);
|
|
204
|
+
if (match === null) return { amount: trimmed, unit: "K" };
|
|
205
|
+
const amount = match[1];
|
|
206
|
+
if (amount === undefined) return { amount: trimmed, unit: "K" };
|
|
207
|
+
const unit = match[2];
|
|
208
|
+
if (unit === undefined || unit === "") return { amount, unit: "tokens" };
|
|
209
|
+
switch (unit.toLowerCase()) {
|
|
210
|
+
case "k":
|
|
211
|
+
return { amount, unit: "K" };
|
|
212
|
+
case "m":
|
|
213
|
+
return { amount, unit: "M" };
|
|
214
|
+
default:
|
|
215
|
+
return { amount: trimmed, unit: "K" };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function joinContextWindowEditorValue(amount: string, unit: ContextWindowUnit): string {
|
|
220
|
+
const trimmed = amount.trim();
|
|
221
|
+
if (trimmed === "") return "";
|
|
222
|
+
switch (unit) {
|
|
223
|
+
case "K":
|
|
224
|
+
return `${trimmed}K`;
|
|
225
|
+
case "M":
|
|
226
|
+
return `${trimmed}M`;
|
|
227
|
+
case "tokens":
|
|
228
|
+
return trimmed;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
155
232
|
function createMetadataDrafts(
|
|
156
233
|
provider: ProviderConfig | undefined,
|
|
157
234
|
models: readonly string[],
|
|
158
235
|
): ModelMetadataDraft[] {
|
|
159
236
|
return models.map((model) => {
|
|
160
237
|
const metadata = findModelMetadata(provider, model);
|
|
238
|
+
const contextWindow =
|
|
239
|
+
metadata?.contextWindow !== undefined ? formatContextWindowInput(metadata.contextWindow) : "";
|
|
161
240
|
return {
|
|
162
|
-
contextWindow
|
|
241
|
+
contextWindow,
|
|
242
|
+
contextWindowMode: contextWindowModeForValue(contextWindow),
|
|
163
243
|
outputLimit: metadata?.outputLimit?.toString() ?? "",
|
|
164
244
|
};
|
|
165
245
|
});
|
|
@@ -315,11 +395,11 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
315
395
|
newErrors.modelMetadataUrl = "Invalid URL format";
|
|
316
396
|
}
|
|
317
397
|
modelMetadataDrafts.forEach((draft, index) => {
|
|
318
|
-
const contextWindow =
|
|
398
|
+
const contextWindow = parseContextWindowTokensInput(draft.contextWindow);
|
|
319
399
|
const outputLimit = readPositiveIntegerInput(draft.outputLimit);
|
|
320
400
|
if (draft.contextWindow.trim() !== "" && contextWindow === null) {
|
|
321
401
|
newErrors[`modelMetadata.${index}.contextWindow`] =
|
|
322
|
-
"Context window must be a positive
|
|
402
|
+
"Context window must be a positive number, optionally using K or M";
|
|
323
403
|
}
|
|
324
404
|
if (draft.outputLimit.trim() !== "" && outputLimit === null) {
|
|
325
405
|
newErrors[`modelMetadata.${index}.outputLimit`] = "Output limit must be a positive integer";
|
|
@@ -361,6 +441,51 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
361
441
|
});
|
|
362
442
|
}
|
|
363
443
|
|
|
444
|
+
function updateContextWindowPreset(index: number, value: string): void {
|
|
445
|
+
switch (value) {
|
|
446
|
+
case CONTEXT_WINDOW_UNSET:
|
|
447
|
+
updateMetadataDraft(index, { contextWindow: "", contextWindowMode: "unset" });
|
|
448
|
+
return;
|
|
449
|
+
case CONTEXT_WINDOW_CUSTOM: {
|
|
450
|
+
const current = modelMetadataDrafts[index] ?? emptyMetadataDraft();
|
|
451
|
+
updateMetadataDraft(index, {
|
|
452
|
+
contextWindow: current.contextWindow,
|
|
453
|
+
contextWindowMode: "custom",
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
default:
|
|
458
|
+
updateMetadataDraft(index, { contextWindow: value, contextWindowMode: "preset" });
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function updateContextWindowAmount(index: number, amount: string): void {
|
|
464
|
+
const current = modelMetadataDrafts[index] ?? emptyMetadataDraft();
|
|
465
|
+
const editorValue = splitContextWindowEditorValue(current.contextWindow);
|
|
466
|
+
updateMetadataDraft(index, {
|
|
467
|
+
contextWindow: joinContextWindowEditorValue(amount, editorValue.unit),
|
|
468
|
+
contextWindowMode: "custom",
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function updateContextWindowUnit(index: number, unit: string): void {
|
|
473
|
+
const current = modelMetadataDrafts[index] ?? emptyMetadataDraft();
|
|
474
|
+
const editorValue = splitContextWindowEditorValue(current.contextWindow);
|
|
475
|
+
switch (unit) {
|
|
476
|
+
case "K":
|
|
477
|
+
case "M":
|
|
478
|
+
case "tokens":
|
|
479
|
+
updateMetadataDraft(index, {
|
|
480
|
+
contextWindow: joinContextWindowEditorValue(editorValue.amount, unit),
|
|
481
|
+
contextWindowMode: "custom",
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
default:
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
364
489
|
function removeModel(index: number): void {
|
|
365
490
|
setModels((prev) => prev.filter((_, idx) => idx !== index));
|
|
366
491
|
setModelMetadataDrafts((prev) => prev.filter((_, idx) => idx !== index));
|
|
@@ -379,7 +504,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
379
504
|
const trimmedModel = model.trim();
|
|
380
505
|
if (trimmedModel === "") return;
|
|
381
506
|
const draft = modelMetadataDrafts[index] ?? emptyMetadataDraft();
|
|
382
|
-
const contextWindow =
|
|
507
|
+
const contextWindow = parseContextWindowTokensInput(draft.contextWindow);
|
|
383
508
|
const outputLimit = readPositiveIntegerInput(draft.outputLimit);
|
|
384
509
|
if (contextWindow === null && outputLimit === null) return;
|
|
385
510
|
|
|
@@ -548,6 +673,8 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
548
673
|
const draft = modelMetadataDrafts[i] ?? emptyMetadataDraft();
|
|
549
674
|
const contextWindowError = errors[`modelMetadata.${i}.contextWindow`];
|
|
550
675
|
const outputLimitError = errors[`modelMetadata.${i}.outputLimit`];
|
|
676
|
+
const contextWindowPresetValue = selectedContextWindowPresetValue(draft);
|
|
677
|
+
const contextWindowEditorValue = splitContextWindowEditorValue(draft.contextWindow);
|
|
551
678
|
return (
|
|
552
679
|
<div
|
|
553
680
|
key={i}
|
|
@@ -624,21 +751,54 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
624
751
|
</div>
|
|
625
752
|
|
|
626
753
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
627
|
-
<
|
|
754
|
+
<div className="space-y-1 text-xs text-muted-foreground">
|
|
628
755
|
<span>Context Window</span>
|
|
629
|
-
<
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
className="
|
|
637
|
-
|
|
756
|
+
<Select
|
|
757
|
+
value={contextWindowPresetValue}
|
|
758
|
+
onValueChange={(value) => updateContextWindowPreset(i, value)}
|
|
759
|
+
>
|
|
760
|
+
<SelectTrigger size="sm" className="w-full bg-background font-mono text-xs">
|
|
761
|
+
<SelectValue />
|
|
762
|
+
</SelectTrigger>
|
|
763
|
+
<SelectContent className="max-h-64">
|
|
764
|
+
<SelectItem value={CONTEXT_WINDOW_UNSET}>Not set</SelectItem>
|
|
765
|
+
{CONTEXT_WINDOW_PRESETS.map((preset) => (
|
|
766
|
+
<SelectItem key={preset.value} value={preset.value}>
|
|
767
|
+
{preset.label} · {preset.value}
|
|
768
|
+
</SelectItem>
|
|
769
|
+
))}
|
|
770
|
+
<SelectItem value={CONTEXT_WINDOW_CUSTOM}>Custom</SelectItem>
|
|
771
|
+
</SelectContent>
|
|
772
|
+
</Select>
|
|
773
|
+
{draft.contextWindowMode === "custom" && (
|
|
774
|
+
<div className="grid grid-cols-[minmax(0,1fr)_5.75rem] gap-1.5">
|
|
775
|
+
<input
|
|
776
|
+
type="text"
|
|
777
|
+
inputMode="decimal"
|
|
778
|
+
value={contextWindowEditorValue.amount}
|
|
779
|
+
onChange={(e) => updateContextWindowAmount(i, e.target.value)}
|
|
780
|
+
placeholder="128"
|
|
781
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px]"
|
|
782
|
+
/>
|
|
783
|
+
<Select
|
|
784
|
+
value={contextWindowEditorValue.unit}
|
|
785
|
+
onValueChange={(value) => updateContextWindowUnit(i, value)}
|
|
786
|
+
>
|
|
787
|
+
<SelectTrigger size="sm" className="w-full bg-background font-mono text-xs">
|
|
788
|
+
<SelectValue />
|
|
789
|
+
</SelectTrigger>
|
|
790
|
+
<SelectContent>
|
|
791
|
+
<SelectItem value="K">K</SelectItem>
|
|
792
|
+
<SelectItem value="M">M</SelectItem>
|
|
793
|
+
<SelectItem value="tokens">tokens</SelectItem>
|
|
794
|
+
</SelectContent>
|
|
795
|
+
</Select>
|
|
796
|
+
</div>
|
|
797
|
+
)}
|
|
638
798
|
{contextWindowError !== undefined && (
|
|
639
799
|
<span className="block text-destructive">{contextWindowError}</span>
|
|
640
800
|
)}
|
|
641
|
-
</
|
|
801
|
+
</div>
|
|
642
802
|
<label className="space-y-1 text-xs text-muted-foreground">
|
|
643
803
|
<span>Output Limit</span>
|
|
644
804
|
<input
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const SessionTokenUsageSchema = z.object({
|
|
4
|
+
input: z.number().int().nonnegative(),
|
|
5
|
+
output: z.number().int().nonnegative(),
|
|
6
|
+
cacheCreate: z.number().int().nonnegative(),
|
|
7
|
+
cacheRead: z.number().int().nonnegative(),
|
|
8
|
+
total: z.number().int().nonnegative(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const SessionLogSummarySchema = z.object({
|
|
12
|
+
id: z.number().int().positive(),
|
|
13
|
+
timestamp: z.string(),
|
|
14
|
+
provider: z.string().nullable(),
|
|
15
|
+
model: z.string().nullable(),
|
|
16
|
+
apiFormat: z.enum(["anthropic", "openai", "unknown"]),
|
|
17
|
+
method: z.string(),
|
|
18
|
+
path: z.string(),
|
|
19
|
+
status: z.number().nullable(),
|
|
20
|
+
isStreaming: z.boolean(),
|
|
21
|
+
hasError: z.boolean(),
|
|
22
|
+
error: z.string().nullable(),
|
|
23
|
+
latencyMs: z.number().nullable(),
|
|
24
|
+
tokens: z.object({
|
|
25
|
+
input: z.number().nullable(),
|
|
26
|
+
output: z.number().nullable(),
|
|
27
|
+
cacheCreate: z.number().nullable(),
|
|
28
|
+
cacheRead: z.number().nullable(),
|
|
29
|
+
}),
|
|
30
|
+
sessionId: z.string().nullable(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const SessionInfoSchema = z.object({
|
|
34
|
+
id: z.string(),
|
|
35
|
+
status: z.enum(["active", "failed", "completed", "empty"]),
|
|
36
|
+
logCoverage: z.enum(["recent", "history"]),
|
|
37
|
+
source: z.enum(["explicit", "client-process", "client-connection", "provider-test"]).nullable(),
|
|
38
|
+
runtimeMode: z.enum(["in-process", "worker-thread", "child-process"]).nullable(),
|
|
39
|
+
createdAt: z.string().nullable(),
|
|
40
|
+
updatedAt: z.string().nullable(),
|
|
41
|
+
requestCount: z.number().int().nonnegative(),
|
|
42
|
+
activeRequests: z.number().int().nonnegative(),
|
|
43
|
+
completedRequests: z.number().int().nonnegative(),
|
|
44
|
+
failedRequests: z.number().int().nonnegative(),
|
|
45
|
+
queuedTasks: z.number().int().nonnegative(),
|
|
46
|
+
runningTasks: z.number().int().nonnegative(),
|
|
47
|
+
lastTaskError: z.string().nullable(),
|
|
48
|
+
logCount: z.number().int().nonnegative(),
|
|
49
|
+
errorCount: z.number().int().nonnegative(),
|
|
50
|
+
models: z.array(z.string()),
|
|
51
|
+
providers: z.array(z.string()),
|
|
52
|
+
tokenUsage: SessionTokenUsageSchema,
|
|
53
|
+
lastLogId: z.number().nullable(),
|
|
54
|
+
lastModel: z.string().nullable(),
|
|
55
|
+
clientPid: z.number().nullable(),
|
|
56
|
+
clientProjectFolder: z.string().nullable(),
|
|
57
|
+
inspectorPath: z.string(),
|
|
58
|
+
inspectorUrl: z.string(),
|
|
59
|
+
apiPath: z.string(),
|
|
60
|
+
apiUrl: z.string(),
|
|
61
|
+
compactLogsPath: z.string(),
|
|
62
|
+
compactLogsUrl: z.string(),
|
|
63
|
+
latestLogLimit: z.number().int().positive(),
|
|
64
|
+
latestLogs: z.array(SessionLogSummarySchema),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type SessionTokenUsage = z.infer<typeof SessionTokenUsageSchema>;
|
|
68
|
+
export type SessionLogSummary = z.infer<typeof SessionLogSummarySchema>;
|
|
69
|
+
export type SessionInfo = z.infer<typeof SessionInfoSchema>;
|
package/src/lib/utils.ts
CHANGED
|
@@ -17,6 +17,52 @@ export function formatContextWindowTokens(count: number): string {
|
|
|
17
17
|
return (count / 1024).toFixed(1).replace(/\.0$/, "") + "K";
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function formatExactContextUnit(count: number, multiplier: number, unit: "K" | "M"): string | null {
|
|
21
|
+
const tenths = count * 10;
|
|
22
|
+
if (!Number.isSafeInteger(tenths) || tenths % multiplier !== 0) return null;
|
|
23
|
+
return (tenths / multiplier / 10).toFixed(1).replace(/\.0$/, "") + unit;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatContextWindowInput(count: number): string {
|
|
27
|
+
if (!Number.isSafeInteger(count) || count <= 0) return count.toString();
|
|
28
|
+
if (count >= 1024 * 1024) {
|
|
29
|
+
const megaValue = formatExactContextUnit(count, 1024 * 1024, "M");
|
|
30
|
+
if (megaValue !== null) return megaValue;
|
|
31
|
+
}
|
|
32
|
+
const kiloValue = formatExactContextUnit(count, 1024, "K");
|
|
33
|
+
if (kiloValue !== null) return kiloValue;
|
|
34
|
+
return count.toString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function contextWindowUnitMultiplier(unit: string | undefined): number | null {
|
|
38
|
+
if (unit === undefined || unit === "") return 1;
|
|
39
|
+
switch (unit.toLowerCase()) {
|
|
40
|
+
case "k":
|
|
41
|
+
return 1024;
|
|
42
|
+
case "m":
|
|
43
|
+
return 1024 * 1024;
|
|
44
|
+
default:
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseContextWindowTokensInput(value: string): number | null {
|
|
50
|
+
const trimmed = value.trim().replaceAll(",", "");
|
|
51
|
+
if (trimmed === "") return null;
|
|
52
|
+
const match = /^(\d+(?:\.\d+)?)\s*([kKmM])?$/.exec(trimmed);
|
|
53
|
+
if (match === null) return null;
|
|
54
|
+
const amountText = match[1];
|
|
55
|
+
if (amountText === undefined) return null;
|
|
56
|
+
const amount = Number(amountText);
|
|
57
|
+
const unit = match[2];
|
|
58
|
+
const multiplier = contextWindowUnitMultiplier(unit);
|
|
59
|
+
if (!Number.isFinite(amount) || amount <= 0 || multiplier === null) return null;
|
|
60
|
+
if ((unit === undefined || unit === "") && !Number.isInteger(amount)) return null;
|
|
61
|
+
const tokens = Math.round(amount * multiplier);
|
|
62
|
+
if (!Number.isSafeInteger(tokens) || tokens <= 0) return null;
|
|
63
|
+
return tokens;
|
|
64
|
+
}
|
|
65
|
+
|
|
20
66
|
export type StatusCategory = "success" | "client_error" | "server_error" | "pending";
|
|
21
67
|
|
|
22
68
|
export function getStatusCategory(status: number | null): StatusCategory {
|
package/src/mcp/server.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
addProviderImpl,
|
|
24
24
|
createSessionKnowledgeImpl,
|
|
25
25
|
getProjectContextImpl,
|
|
26
|
+
getSessionImpl,
|
|
26
27
|
getLogChunksImpl,
|
|
27
28
|
getLogImpl,
|
|
28
29
|
getProviderImpl,
|
|
@@ -201,6 +202,30 @@ function registerTools(server: McpServer): void {
|
|
|
201
202
|
() => safeCall(() => listSessionsImpl(callApi)),
|
|
202
203
|
);
|
|
203
204
|
|
|
205
|
+
server.registerTool(
|
|
206
|
+
"inspector_get_session",
|
|
207
|
+
{
|
|
208
|
+
title: "Get Inspector session info",
|
|
209
|
+
description:
|
|
210
|
+
"Returns a lightweight, evidence-friendly summary for one Inspector session: clickable inspectorUrl, compact logs URL, status, request/error counts, token totals, models/providers, and the latest compact log summaries. Defaults to recent in-memory/cache data for speed; set includeHistory=true only when you explicitly want to scan persisted jsonl history. It avoids raw request/response bodies so it is safe to use in evaluation artifacts.",
|
|
211
|
+
inputSchema: z.object({
|
|
212
|
+
sessionId: z.string().min(1).describe("The Inspector session id."),
|
|
213
|
+
includeHistory: z
|
|
214
|
+
.boolean()
|
|
215
|
+
.optional()
|
|
216
|
+
.describe("Scan persisted jsonl history for this session. Slower; default false."),
|
|
217
|
+
latestLogLimit: z
|
|
218
|
+
.number()
|
|
219
|
+
.int()
|
|
220
|
+
.positive()
|
|
221
|
+
.max(50)
|
|
222
|
+
.optional()
|
|
223
|
+
.describe("How many latest compact log summaries to include. Max 50, default 10."),
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
(args) => safeCall(() => getSessionImpl(callApi, args)),
|
|
227
|
+
);
|
|
228
|
+
|
|
204
229
|
server.registerTool(
|
|
205
230
|
"inspector_list_models",
|
|
206
231
|
{
|
|
@@ -389,6 +414,7 @@ export const TOOL_NAMES = [
|
|
|
389
414
|
"inspector_get_log",
|
|
390
415
|
"inspector_get_log_chunks",
|
|
391
416
|
"inspector_list_sessions",
|
|
417
|
+
"inspector_get_session",
|
|
392
418
|
"inspector_list_models",
|
|
393
419
|
"inspector_list_providers",
|
|
394
420
|
"inspector_get_provider",
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { Buffer } from "node:buffer";
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { LoopbackTimeoutError, type CallApiOptions } from "./loopback";
|
|
19
19
|
import { extractLastUserMessagePreview, extractResponsePreview } from "./previewExtractor";
|
|
20
|
+
import { SessionInfoSchema } from "../lib/sessionInfoContract";
|
|
20
21
|
import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
|
|
21
22
|
|
|
22
23
|
/** Minimal callApi contract: same shape as the real `callApi` in loopback.ts. */
|
|
@@ -191,6 +192,32 @@ export async function listSessionsImpl(callApi: CallApiFn): Promise<ToolResult>
|
|
|
191
192
|
return textJson(await res.json());
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
export type GetSessionArgs = {
|
|
196
|
+
sessionId: string;
|
|
197
|
+
includeHistory?: boolean;
|
|
198
|
+
latestLogLimit?: number;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export async function getSessionImpl(
|
|
202
|
+
callApi: CallApiFn,
|
|
203
|
+
args: GetSessionArgs,
|
|
204
|
+
): Promise<ToolResult> {
|
|
205
|
+
const params = new URLSearchParams({ sessionId: args.sessionId });
|
|
206
|
+
if (args.latestLogLimit !== undefined) {
|
|
207
|
+
params.set("limit", String(args.latestLogLimit));
|
|
208
|
+
}
|
|
209
|
+
if (args.includeHistory === true) {
|
|
210
|
+
params.set("includeHistory", "1");
|
|
211
|
+
}
|
|
212
|
+
const path = `/api/sessions?${params.toString()}`;
|
|
213
|
+
const res = await callApi(path);
|
|
214
|
+
if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
|
|
215
|
+
const rawBody: unknown = await res.json();
|
|
216
|
+
const parsed = SessionInfoSchema.safeParse(rawBody);
|
|
217
|
+
if (!parsed.success) return toolError("GET /api/sessions returned an unparseable session info");
|
|
218
|
+
return textJson(parsed.data);
|
|
219
|
+
}
|
|
220
|
+
|
|
194
221
|
export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
195
222
|
const res = await callApi("/api/models");
|
|
196
223
|
if (!res.ok) return toolError(`GET /api/models returned ${res.status}`);
|