@tonyclaw/agent-inspector 2.0.28 → 2.0.30

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.
Files changed (54) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-CicCP3Hb.js → CompareDrawer-BDzfRSt1.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-NtOD8Ex_.js +115 -0
  4. package/.output/public/assets/{ReplayDialog-Bc8q3ujm.js → ReplayDialog-p-5TKg75.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-Coyy3zcH.js → RequestAnatomy-G0dV8-D_.js} +1 -1
  6. package/.output/public/assets/{ResponseView-BWJzgh0c.js → ResponseView-D2wE6lD-.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-BSC3uuE5.js → StreamingChunkSequence-D8JRK-j_.js} +1 -1
  8. package/.output/public/assets/_sessionId-zimzVrJE.js +1 -0
  9. package/.output/public/assets/index-Cv5vPgpr.js +1 -0
  10. package/.output/public/assets/{index-DsiKfWCp.css → index-CwlHPmgL.css} +1 -1
  11. package/.output/public/assets/{main-Brj0Gn91.js → main-XHHZsOBK.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +13 -13
  13. package/.output/server/{_sessionId-DDdyKVG-.mjs → _sessionId-CK9QEhkB.mjs} +3 -3
  14. package/.output/server/_ssr/{CompareDrawer-XSExe2ml.mjs → CompareDrawer-BJVPFMIx.mjs} +2 -2
  15. package/.output/server/_ssr/{ProxyViewerContainer-B5pIiBZN.mjs → ProxyViewerContainer-DhbmJ2zj.mjs} +83 -20
  16. package/.output/server/_ssr/{ReplayDialog-BOAu0ric.mjs → ReplayDialog-DXRj95Gj.mjs} +4 -4
  17. package/.output/server/_ssr/{RequestAnatomy-C8Q5lozD.mjs → RequestAnatomy-CrYJTXdJ.mjs} +2 -2
  18. package/.output/server/_ssr/{ResponseView-a_iJfPUF.mjs → ResponseView-4ESqSITr.mjs} +2 -2
  19. package/.output/server/_ssr/{StreamingChunkSequence-YMKC0j8U.mjs → StreamingChunkSequence-Czdko5n_.mjs} +2 -2
  20. package/.output/server/_ssr/{index-CkDwBPzI.mjs → index-bei0I7qL.mjs} +2 -2
  21. package/.output/server/_ssr/index.mjs +2 -2
  22. package/.output/server/_ssr/{router-DXOXdFFM.mjs → router-sQh4Gk8J.mjs} +1338 -137
  23. package/.output/server/_tanstack-start-manifest_v-C0fdSWnA.mjs +4 -0
  24. package/.output/server/index.mjs +64 -64
  25. package/README.md +24 -1
  26. package/package.json +1 -1
  27. package/src/components/ProxyViewer.tsx +3 -0
  28. package/src/components/providers/ProviderCard.tsx +32 -0
  29. package/src/components/proxy-viewer/LogEntryHeader.tsx +38 -1
  30. package/src/lib/groupContract.ts +139 -0
  31. package/src/lib/providerTestContract.ts +3 -0
  32. package/src/lib/sessionInfoContract.ts +3 -0
  33. package/src/mcp/server.ts +278 -0
  34. package/src/mcp/toolHandlers.ts +126 -0
  35. package/src/proxy/evidenceAnalysis.ts +5 -0
  36. package/src/proxy/evidenceExporter.ts +8 -3
  37. package/src/proxy/groupEvidenceExporter.ts +341 -0
  38. package/src/proxy/groupStore.ts +259 -0
  39. package/src/proxy/handler.ts +8 -1
  40. package/src/proxy/logFinalizer.ts +36 -0
  41. package/src/proxy/schemas.ts +3 -0
  42. package/src/proxy/sessionInfo.ts +3 -0
  43. package/src/proxy/store.ts +3 -0
  44. package/src/proxy/streamTiming.ts +31 -0
  45. package/src/routes/api/groups.$groupId.evidence.ts +62 -0
  46. package/src/routes/api/groups.$groupId.sessions.ts +50 -0
  47. package/src/routes/api/groups.$groupId.ts +50 -0
  48. package/src/routes/api/groups.ts +39 -0
  49. package/src/routes/api/logs.$id.replay.ts +25 -2
  50. package/src/routes/api/providers.$providerId.test.log.ts +34 -2
  51. package/.output/public/assets/ProxyViewerContainer-BccuA6p5.js +0 -115
  52. package/.output/public/assets/_sessionId-9rEF0uSE.js +0 -1
  53. package/.output/public/assets/index-B9_VaAWl.js +0 -1
  54. package/.output/server/_tanstack-start-manifest_v-DPf3uJys.mjs +0 -4
package/src/mcp/server.ts CHANGED
@@ -25,6 +25,14 @@ import {
25
25
  type Variables,
26
26
  WebStandardStreamableHTTPServerTransport,
27
27
  } from "@modelcontextprotocol/server";
28
+ import {
29
+ AddInspectorGroupSessionInputSchema,
30
+ GroupEvidenceExportOptionsSchema,
31
+ InspectorGroupSchema,
32
+ InspectorGroupStatusSchema,
33
+ InspectorGroupsListResponseSchema,
34
+ UpdateInspectorGroupInputSchema,
35
+ } from "../lib/groupContract";
28
36
  import {
29
37
  InspectorRunSchema,
30
38
  InspectorRunStatusSchema,
@@ -34,10 +42,14 @@ import { setCurrentPort } from "../lib/serverPort";
34
42
  import { JsonValueSchema } from "../proxy/schemas";
35
43
  import { callApi } from "./loopback";
36
44
  import {
45
+ addGroupSessionImpl,
37
46
  addProviderImpl,
47
+ createGroupImpl,
38
48
  createRunImpl,
39
49
  createSessionKnowledgeImpl,
40
50
  exportEvidenceImpl,
51
+ exportGroupEvidenceImpl,
52
+ getGroupImpl,
41
53
  getRecentFailuresImpl,
42
54
  getProjectContextImpl,
43
55
  getRunImpl,
@@ -45,6 +57,7 @@ import {
45
57
  getLogChunksImpl,
46
58
  getLogImpl,
47
59
  getProviderImpl,
60
+ listGroupsImpl,
48
61
  listKnowledgeCandidatesImpl,
49
62
  listLogsImpl,
50
63
  listModelsImpl,
@@ -56,6 +69,7 @@ import {
56
69
  searchLogsImpl,
57
70
  searchKnowledgeImpl,
58
71
  testProviderImpl,
72
+ updateGroupImpl,
59
73
  updateRunImpl,
60
74
  updateProviderImpl,
61
75
  } from "./toolHandlers";
@@ -172,6 +186,10 @@ const EvidenceReadResponseSchema = z.object({
172
186
  markdown: z.string(),
173
187
  });
174
188
 
189
+ const GroupEvidenceReadResponseSchema = z.object({
190
+ markdown: z.string(),
191
+ });
192
+
175
193
  type ResourceDescriptor = {
176
194
  uri: string;
177
195
  name: string;
@@ -270,6 +288,66 @@ async function listRunResources(): Promise<ListResourcesResult> {
270
288
  }
271
289
  }
272
290
 
291
+ async function listGroupResources(): Promise<ListResourcesResult> {
292
+ try {
293
+ const response = await callApi("/api/groups");
294
+ if (!response.ok) return listResult([]);
295
+ const parsed = InspectorGroupsListResponseSchema.safeParse(await response.json());
296
+ if (!parsed.success) return listResult([]);
297
+ return listResult(
298
+ parsed.data.groups.map((group) => ({
299
+ uri: `inspector://groups/${encodeURIComponent(group.id)}`,
300
+ name: `group:${group.id}`,
301
+ title: group.title,
302
+ mimeType: "application/json",
303
+ })),
304
+ );
305
+ } catch {
306
+ return listResult([]);
307
+ }
308
+ }
309
+
310
+ async function listGroupSessionResources(): Promise<ListResourcesResult> {
311
+ try {
312
+ const response = await callApi("/api/groups");
313
+ if (!response.ok) return listResult([]);
314
+ const parsed = InspectorGroupsListResponseSchema.safeParse(await response.json());
315
+ if (!parsed.success) return listResult([]);
316
+ return listResult(
317
+ parsed.data.groups.map((group) => ({
318
+ uri: `inspector://groups/${encodeURIComponent(group.id)}/sessions`,
319
+ name: `group-sessions:${group.id}`,
320
+ title: `${group.title} sessions`,
321
+ mimeType: "application/json",
322
+ })),
323
+ );
324
+ } catch {
325
+ return listResult([]);
326
+ }
327
+ }
328
+
329
+ async function listGroupEvidenceResources(): Promise<ListResourcesResult> {
330
+ try {
331
+ const response = await callApi("/api/groups");
332
+ if (!response.ok) return listResult([]);
333
+ const parsed = InspectorGroupsListResponseSchema.safeParse(await response.json());
334
+ if (!parsed.success) return listResult([]);
335
+ return listResult(
336
+ parsed.data.groups
337
+ .filter((group) => group.evidence !== null)
338
+ .map((group) => ({
339
+ uri: `inspector://groups/${encodeURIComponent(group.id)}/evidence`,
340
+ name: `group-evidence:${group.id}`,
341
+ title: `${group.title} evidence pack`,
342
+ mimeType: "text/markdown",
343
+ description: group.evidence?.markdownPath,
344
+ })),
345
+ );
346
+ } catch {
347
+ return listResult([]);
348
+ }
349
+ }
350
+
273
351
  async function listEvidenceResources(): Promise<ListResourcesResult> {
274
352
  try {
275
353
  const response = await callApi("/api/runs");
@@ -520,6 +598,92 @@ function registerTools(server: McpServer): void {
520
598
  (args) => safeCall(() => getRecentFailuresImpl(callApi, args)),
521
599
  );
522
600
 
601
+ server.registerTool(
602
+ "inspector_list_groups",
603
+ {
604
+ title: "List Inspector groups",
605
+ description:
606
+ "Lists declared Inspector groups. Groups aggregate multiple sessions/runs, commonly one evaluation matrix spanning several providers or models.",
607
+ inputSchema: z.object({}),
608
+ },
609
+ () => safeCall(() => listGroupsImpl(callApi)),
610
+ );
611
+
612
+ server.registerTool(
613
+ "inspector_create_group",
614
+ {
615
+ title: "Create or declare an Inspector group",
616
+ description:
617
+ "Creates an evaluation/batch group that external systems can use to aggregate multiple OpenCode/Codex sessions across models such as MiniMax, DeepSeek, or GLM.",
618
+ inputSchema: z.object({
619
+ groupId: z.string().min(1).optional().describe("Stable group id. Generated when omitted."),
620
+ title: z.string().min(1).optional().describe("Human-readable group title."),
621
+ kind: z
622
+ .enum(["evaluation", "batch", "manual"])
623
+ .optional()
624
+ .describe("Group kind. Defaults to evaluation."),
625
+ task: z.string().nullable().optional().describe("Task, benchmark, or evaluation goal."),
626
+ project: z.string().nullable().optional().describe("Project or repository under test."),
627
+ status: InspectorGroupStatusSchema.optional().describe("Initial group status."),
628
+ tags: z.array(z.string()).optional().describe("Free-form group labels."),
629
+ metadata: z.record(z.string(), JsonValueSchema).optional().describe("JSON metadata."),
630
+ }),
631
+ },
632
+ (args) => safeCall(() => createGroupImpl(callApi, args)),
633
+ );
634
+
635
+ server.registerTool(
636
+ "inspector_get_group",
637
+ {
638
+ title: "Get an Inspector group",
639
+ description:
640
+ "Returns one declared Inspector group with its attached session/run members and exported evidence paths when available.",
641
+ inputSchema: z.object({
642
+ groupId: z.string().min(1).describe("The Inspector group id."),
643
+ }),
644
+ },
645
+ (args) => safeCall(() => getGroupImpl(callApi, args)),
646
+ );
647
+
648
+ server.registerTool(
649
+ "inspector_update_group",
650
+ {
651
+ title: "Update an Inspector group",
652
+ description:
653
+ "PATCH-style update for a declared group. Use this to mark a whole evaluation running, failed, completed, or to merge CI metadata.",
654
+ inputSchema: UpdateInspectorGroupInputSchema.extend({
655
+ groupId: z.string().min(1).describe("The Inspector group id."),
656
+ }),
657
+ },
658
+ (args) => safeCall(() => updateGroupImpl(callApi, args)),
659
+ );
660
+
661
+ server.registerTool(
662
+ "inspector_add_group_session",
663
+ {
664
+ title: "Attach a session to an Inspector group",
665
+ description:
666
+ "Adds or updates one group member. Use it after each OpenCode process discovers its session id, or while an evaluation matrix is running to attach model/provider labels.",
667
+ inputSchema: AddInspectorGroupSessionInputSchema.extend({
668
+ groupId: z.string().min(1).describe("The Inspector group id."),
669
+ }),
670
+ },
671
+ (args) => safeCall(() => addGroupSessionImpl(callApi, args)),
672
+ );
673
+
674
+ server.registerTool(
675
+ "inspector_export_group_evidence",
676
+ {
677
+ title: "Export an Inspector group evidence pack",
678
+ description:
679
+ "Exports a whole group's evidence pack to local JSON, Markdown, and HTML files under <dataDir>/evidence/groups/<groupId>. The export contains a provider/model matrix and compact session summaries.",
680
+ inputSchema: GroupEvidenceExportOptionsSchema.unwrap().extend({
681
+ groupId: z.string().min(1).describe("The Inspector group id."),
682
+ }),
683
+ },
684
+ (args) => safeCall(() => exportGroupEvidenceImpl(callApi, args)),
685
+ );
686
+
523
687
  server.registerTool(
524
688
  "inspector_list_models",
525
689
  {
@@ -763,6 +927,49 @@ function registerResources(server: McpServer): void {
763
927
  (uri) => readApiJsonResource(uri, "/api/runs"),
764
928
  );
765
929
 
930
+ server.registerResource(
931
+ "inspector_groups",
932
+ "inspector://groups",
933
+ {
934
+ title: "Inspector groups",
935
+ description: "Declared Inspector groups aggregating multiple sessions or runs.",
936
+ mimeType: "application/json",
937
+ },
938
+ (uri) => readApiJsonResource(uri, "/api/groups"),
939
+ );
940
+
941
+ server.registerResource(
942
+ "inspector_group",
943
+ new ResourceTemplate("inspector://groups/{groupId}", { list: listGroupResources }),
944
+ {
945
+ title: "Inspector group",
946
+ description: "One declared Inspector group with attached session/run members.",
947
+ mimeType: "application/json",
948
+ },
949
+ (uri, variables) => {
950
+ const groupId = variableString(variables, "groupId");
951
+ if (groupId === null) return textResource(uri, "text/plain", "Missing groupId");
952
+ return readApiJsonResource(uri, `/api/groups/${encodeURIComponent(groupId)}`);
953
+ },
954
+ );
955
+
956
+ server.registerResource(
957
+ "inspector_group_sessions",
958
+ new ResourceTemplate("inspector://groups/{groupId}/sessions", {
959
+ list: listGroupSessionResources,
960
+ }),
961
+ {
962
+ title: "Inspector group sessions",
963
+ description: "Attached session/run members for one Inspector group.",
964
+ mimeType: "application/json",
965
+ },
966
+ (uri, variables) => {
967
+ const groupId = variableString(variables, "groupId");
968
+ if (groupId === null) return textResource(uri, "text/plain", "Missing groupId");
969
+ return readApiJsonResource(uri, `/api/groups/${encodeURIComponent(groupId)}/sessions`);
970
+ },
971
+ );
972
+
766
973
  server.registerResource(
767
974
  "inspector_recent_failures",
768
975
  "inspector://failures/recent",
@@ -819,6 +1026,38 @@ function registerResources(server: McpServer): void {
819
1026
  }
820
1027
  },
821
1028
  );
1029
+
1030
+ server.registerResource(
1031
+ "inspector_group_evidence",
1032
+ new ResourceTemplate("inspector://groups/{groupId}/evidence", {
1033
+ list: listGroupEvidenceResources,
1034
+ }),
1035
+ {
1036
+ title: "Inspector group evidence pack",
1037
+ description:
1038
+ "Markdown evidence pack for an Inspector group. Generate it first with inspector_export_group_evidence.",
1039
+ mimeType: "text/markdown",
1040
+ },
1041
+ async (uri, variables) => {
1042
+ const groupId = variableString(variables, "groupId");
1043
+ if (groupId === null) return textResource(uri, "text/plain", "Missing groupId");
1044
+ const path = `/api/groups/${encodeURIComponent(groupId)}/evidence`;
1045
+ try {
1046
+ const response = await callApi(path);
1047
+ if (!response.ok) {
1048
+ return textResource(uri, "text/plain", `GET ${path} returned ${response.status}`);
1049
+ }
1050
+ const parsed = GroupEvidenceReadResponseSchema.safeParse(await response.json());
1051
+ if (!parsed.success) {
1052
+ return textResource(uri, "text/plain", "Evidence endpoint returned an unparseable body");
1053
+ }
1054
+ return textResource(uri, "text/markdown", parsed.data.markdown);
1055
+ } catch (err) {
1056
+ const message = err instanceof Error ? err.message : String(err);
1057
+ return textResource(uri, "text/plain", `Resource read failed: ${message}`);
1058
+ }
1059
+ },
1060
+ );
822
1061
  }
823
1062
 
824
1063
  function registerPrompts(server: McpServer): void {
@@ -911,6 +1150,34 @@ The report should include:
911
1150
  5. Reproducible next action for a developer`),
912
1151
  );
913
1152
 
1153
+ server.registerPrompt(
1154
+ "inspector_generate_group_report",
1155
+ {
1156
+ title: "Generate group report",
1157
+ description:
1158
+ "Generate a model/provider evaluation report from an Inspector group and group evidence pack.",
1159
+ argsSchema: z.object({
1160
+ groupId: z.string().min(1).describe("The Inspector group id."),
1161
+ }),
1162
+ },
1163
+ ({ groupId }) =>
1164
+ userPrompt(`Generate a model/provider evaluation report for this Inspector group.
1165
+
1166
+ Evidence to read:
1167
+ - Group resource: inspector://groups/${encodeURIComponent(groupId)}
1168
+ - Group session members: inspector://groups/${encodeURIComponent(groupId)}/sessions
1169
+ - Group evidence pack: inspector://groups/${encodeURIComponent(groupId)}/evidence
1170
+
1171
+ If the evidence pack resource says it has not been exported, call inspector_export_group_evidence for this group first.
1172
+
1173
+ The report should include:
1174
+ 1. Evaluation status summary
1175
+ 2. Provider/model matrix with session links
1176
+ 3. Request counts, token totals, streaming timing, and error counts
1177
+ 4. Winner/loser observations when the evidence supports them
1178
+ 5. Concrete next actions for failed or missing sessions`),
1179
+ );
1180
+
914
1181
  server.registerPrompt(
915
1182
  "inspector_extract_repro_steps",
916
1183
  {
@@ -950,6 +1217,12 @@ export const TOOL_NAMES = [
950
1217
  "inspector_update_run",
951
1218
  "inspector_export_evidence",
952
1219
  "inspector_get_recent_failures",
1220
+ "inspector_list_groups",
1221
+ "inspector_create_group",
1222
+ "inspector_get_group",
1223
+ "inspector_update_group",
1224
+ "inspector_add_group_session",
1225
+ "inspector_export_group_evidence",
953
1226
  "inspector_list_models",
954
1227
  "inspector_list_providers",
955
1228
  "inspector_get_provider",
@@ -972,11 +1245,16 @@ export const RESOURCE_NAMES = [
972
1245
  "inspector_recent_failures",
973
1246
  "inspector_run",
974
1247
  "inspector_evidence",
1248
+ "inspector_groups",
1249
+ "inspector_group",
1250
+ "inspector_group_sessions",
1251
+ "inspector_group_evidence",
975
1252
  ] as const;
976
1253
 
977
1254
  export const PROMPT_NAMES = [
978
1255
  "inspector_analyze_session_failure",
979
1256
  "inspector_triage_recent_failures",
980
1257
  "inspector_generate_jenkins_report",
1258
+ "inspector_generate_group_report",
981
1259
  "inspector_extract_repro_steps",
982
1260
  ] as const;
@@ -17,6 +17,14 @@ 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 {
21
+ GroupEvidenceExportResultSchema,
22
+ InspectorGroupSchema,
23
+ InspectorGroupsListResponseSchema,
24
+ type AddInspectorGroupSessionInput,
25
+ type CreateInspectorGroupInput,
26
+ type UpdateInspectorGroupInput,
27
+ } from "../lib/groupContract";
20
28
  import {
21
29
  EvidenceExportResultSchema,
22
30
  InspectorRunSchema,
@@ -132,6 +140,9 @@ export function buildLogSummary(log: CapturedLog) {
132
140
  isStreaming: log.streaming,
133
141
  hasError,
134
142
  latencyMs: log.elapsedMs,
143
+ firstChunkMs: log.firstChunkMs ?? null,
144
+ totalStreamMs: log.totalStreamMs ?? null,
145
+ tokensPerSecond: log.tokensPerSecond ?? null,
135
146
  tokens: {
136
147
  input: log.inputTokens,
137
148
  output: log.outputTokens,
@@ -372,6 +383,121 @@ export async function exportEvidenceImpl(
372
383
  return textJson(parsed.data);
373
384
  }
374
385
 
386
+ export async function listGroupsImpl(callApi: CallApiFn): Promise<ToolResult> {
387
+ const res = await callApi("/api/groups");
388
+ if (!res.ok) return toolError(`GET /api/groups returned ${res.status}`);
389
+ const rawBody: unknown = await res.json();
390
+ const parsed = InspectorGroupsListResponseSchema.safeParse(rawBody);
391
+ if (!parsed.success) return toolError("GET /api/groups returned an unparseable group list");
392
+ return textJson(parsed.data);
393
+ }
394
+
395
+ export async function createGroupImpl(
396
+ callApi: CallApiFn,
397
+ args: CreateInspectorGroupInput,
398
+ ): Promise<ToolResult> {
399
+ const res = await callApi("/api/groups", {
400
+ method: "POST",
401
+ headers: { "content-type": "application/json" },
402
+ body: JSON.stringify(args ?? {}),
403
+ });
404
+ if (!res.ok) return toolError(`POST /api/groups returned ${res.status}`);
405
+ const rawBody: unknown = await res.json();
406
+ const parsed = InspectorGroupSchema.safeParse(rawBody);
407
+ if (!parsed.success) return toolError("POST /api/groups returned an unparseable group");
408
+ return textJson(parsed.data);
409
+ }
410
+
411
+ export type GetGroupArgs = {
412
+ groupId: string;
413
+ };
414
+
415
+ export async function getGroupImpl(callApi: CallApiFn, args: GetGroupArgs): Promise<ToolResult> {
416
+ const path = `/api/groups/${encodeURIComponent(args.groupId)}`;
417
+ const res = await callApi(path);
418
+ if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
419
+ const rawBody: unknown = await res.json();
420
+ const parsed = InspectorGroupSchema.safeParse(rawBody);
421
+ if (!parsed.success) return toolError("GET /api/groups/{groupId} returned an unparseable group");
422
+ return textJson(parsed.data);
423
+ }
424
+
425
+ export type UpdateGroupArgs = {
426
+ groupId: string;
427
+ } & UpdateInspectorGroupInput;
428
+
429
+ export async function updateGroupImpl(
430
+ callApi: CallApiFn,
431
+ args: UpdateGroupArgs,
432
+ ): Promise<ToolResult> {
433
+ const { groupId, ...patch } = args;
434
+ const path = `/api/groups/${encodeURIComponent(groupId)}`;
435
+ const res = await callApi(path, {
436
+ method: "PATCH",
437
+ headers: { "content-type": "application/json" },
438
+ body: JSON.stringify(patch),
439
+ });
440
+ if (!res.ok) return toolError(`PATCH ${path} returned ${res.status}`);
441
+ const rawBody: unknown = await res.json();
442
+ const parsed = InspectorGroupSchema.safeParse(rawBody);
443
+ if (!parsed.success) {
444
+ return toolError("PATCH /api/groups/{groupId} returned an unparseable group");
445
+ }
446
+ return textJson(parsed.data);
447
+ }
448
+
449
+ export type AddGroupSessionArgs = {
450
+ groupId: string;
451
+ } & AddInspectorGroupSessionInput;
452
+
453
+ export async function addGroupSessionImpl(
454
+ callApi: CallApiFn,
455
+ args: AddGroupSessionArgs,
456
+ ): Promise<ToolResult> {
457
+ const { groupId, ...session } = args;
458
+ const path = `/api/groups/${encodeURIComponent(groupId)}/sessions`;
459
+ const res = await callApi(path, {
460
+ method: "POST",
461
+ headers: { "content-type": "application/json" },
462
+ body: JSON.stringify(session),
463
+ });
464
+ if (!res.ok) return toolError(`POST ${path} returned ${res.status}`);
465
+ const rawBody: unknown = await res.json();
466
+ const parsed = InspectorGroupSchema.safeParse(rawBody);
467
+ if (!parsed.success) {
468
+ return toolError("POST /api/groups/{groupId}/sessions returned an unparseable group");
469
+ }
470
+ return textJson(parsed.data);
471
+ }
472
+
473
+ export type ExportGroupEvidenceArgs = {
474
+ groupId: string;
475
+ includeHistory?: boolean;
476
+ latestLogLimit?: number;
477
+ };
478
+
479
+ export async function exportGroupEvidenceImpl(
480
+ callApi: CallApiFn,
481
+ args: ExportGroupEvidenceArgs,
482
+ ): Promise<ToolResult> {
483
+ const path = `/api/groups/${encodeURIComponent(args.groupId)}/evidence`;
484
+ const res = await callApi(path, {
485
+ method: "POST",
486
+ headers: { "content-type": "application/json" },
487
+ body: JSON.stringify({
488
+ includeHistory: args.includeHistory,
489
+ latestLogLimit: args.latestLogLimit,
490
+ }),
491
+ });
492
+ if (!res.ok) return toolError(`POST ${path} returned ${res.status}`);
493
+ const rawBody: unknown = await res.json();
494
+ const parsed = GroupEvidenceExportResultSchema.safeParse(rawBody);
495
+ if (!parsed.success) {
496
+ return toolError("POST /api/groups/{groupId}/evidence returned an unparseable evidence result");
497
+ }
498
+ return textJson(parsed.data);
499
+ }
500
+
375
501
  export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
376
502
  const res = await callApi("/api/models");
377
503
  if (!res.ok) return toolError(`GET /api/models returned ${res.status}`);
@@ -318,6 +318,11 @@ function logDetails(log: SessionLogSummary): string {
318
318
  `streaming=${log.isStreaming ? "yes" : "no"}`,
319
319
  `latencyMs=${formatNullable(log.latencyMs)}`,
320
320
  ];
321
+ if (log.isStreaming) {
322
+ parts.push(`firstChunkMs=${formatNullable(log.firstChunkMs ?? null)}`);
323
+ parts.push(`totalStreamMs=${formatNullable(log.totalStreamMs ?? null)}`);
324
+ parts.push(`tokensPerSecond=${formatNullable(log.tokensPerSecond ?? null)}`);
325
+ }
321
326
  if (log.error !== null) {
322
327
  parts.push(`error=${log.error}`);
323
328
  }
@@ -112,14 +112,19 @@ function buildMarkdown(document: EvidenceDocument): string {
112
112
  lines.push("No compact logs were available.", "");
113
113
  } else {
114
114
  lines.push(
115
- "| ID | Status | Model | Latency | Path | Error |",
116
- "| --- | --- | --- | --- | --- | --- |",
115
+ "| ID | Status | Model | Latency | Stream | Path | Error |",
116
+ "| --- | --- | --- | --- | --- | --- | --- |",
117
117
  );
118
118
  for (const log of session.latestLogs) {
119
+ const streamTiming = log.isStreaming
120
+ ? `first=${log.firstChunkMs ?? "n/a"}ms total=${log.totalStreamMs ?? "n/a"}ms tps=${
121
+ log.tokensPerSecond ?? "n/a"
122
+ }`
123
+ : "n/a";
119
124
  lines.push(
120
125
  `| ${log.id} | ${log.status ?? "n/a"} | ${log.model ?? "n/a"} | ${
121
126
  log.latencyMs ?? "n/a"
122
- } | ${log.path} | ${log.hasError ? "yes" : "no"} |`,
127
+ } | ${streamTiming} | ${log.path} | ${log.hasError ? "yes" : "no"} |`,
123
128
  );
124
129
  }
125
130
  lines.push("");