@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.
Files changed (37) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DrY-gAzy.js → CompareDrawer-DBTFzxG5.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-ivZk8MgE.js +115 -0
  4. package/.output/public/assets/{ReplayDialog-CFyw6Vu4.js → ReplayDialog-syW2hDNE.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-DHGJ9py7.js → RequestAnatomy-BkuRtY9o.js} +1 -1
  6. package/.output/public/assets/{ResponseView-CB_3yc08.js → ResponseView-BwyvEvoE.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-CqhHwgDn.js → StreamingChunkSequence-BY4MbPKg.js} +1 -1
  8. package/.output/public/assets/_sessionId-XlPUgIIb.js +1 -0
  9. package/.output/public/assets/index-By11a28-.js +1 -0
  10. package/.output/public/assets/index-DsiKfWCp.css +1 -0
  11. package/.output/public/assets/{main-CVw7-JyE.js → main-DKRDRBdd.js} +2 -2
  12. package/.output/server/{_sessionId-V33sBWBC.mjs → _sessionId-VDd4N_1B.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-C_Fk9joM.mjs → CompareDrawer-BQZlxsOC.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-CyFKksQa.mjs → ProxyViewerContainer-njY2oQCc.mjs} +208 -22
  15. package/.output/server/_ssr/{ReplayDialog-BpQHN7O2.mjs → ReplayDialog-B8lkdAIh.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-CuhmKAma.mjs → RequestAnatomy-CwOTfdwS.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-CO6LyBmj.mjs → ResponseView-3Iz_mZpd.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-DLeVetNR.mjs → StreamingChunkSequence-hmJcQNW5.mjs} +2 -2
  19. package/.output/server/_ssr/{index-DWUKl9Bf.mjs → index-UuTKM4aC.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-BrK_DcTS.mjs → router-C7InHrxE.mjs} +352 -10
  22. package/.output/server/{_tanstack-start-manifest_v-BjW2XeWE.mjs → _tanstack-start-manifest_v-B6yfnMHA.mjs} +1 -1
  23. package/.output/server/index.mjs +58 -58
  24. package/package.json +1 -1
  25. package/src/components/providers/ProviderForm.tsx +176 -16
  26. package/src/lib/sessionInfoContract.ts +69 -0
  27. package/src/lib/utils.ts +46 -0
  28. package/src/mcp/server.ts +26 -0
  29. package/src/mcp/toolHandlers.ts +27 -0
  30. package/src/proxy/sessionInfo.ts +222 -0
  31. package/src/proxy/sessionSupervisor.ts +8 -5
  32. package/src/proxy/store.ts +77 -0
  33. package/src/routes/api/sessions.ts +29 -2
  34. package/.output/public/assets/ProxyViewerContainer-CiLvRNie.js +0 -115
  35. package/.output/public/assets/_sessionId-CgG9CQ9r.js +0 -1
  36. package/.output/public/assets/index-780zTVwp.css +0 -1
  37. package/.output/public/assets/index-CViEXkqa.js +0 -1
@@ -0,0 +1,222 @@
1
+ import type { SessionInfo, SessionLogSummary, SessionTokenUsage } from "../lib/sessionInfoContract";
2
+ import { getSessionPath } from "../lib/sessionUrl";
3
+ import type { CapturedLog } from "./schemas";
4
+ import type { SessionSnapshot } from "./sessionSupervisor";
5
+
6
+ export const SESSION_INFO_DEFAULT_LATEST_LOG_LIMIT = 10;
7
+ export const SESSION_INFO_MAX_LATEST_LOG_LIMIT = 50;
8
+
9
+ type SessionStatus = SessionInfo["status"];
10
+
11
+ type BuildSessionInfoInput = {
12
+ sessionId: string;
13
+ baseUrl: string;
14
+ snapshot: SessionSnapshot | null;
15
+ summaries: readonly SessionLogSummary[];
16
+ logCoverage: SessionInfo["logCoverage"];
17
+ latestLogLimit?: number;
18
+ };
19
+
20
+ export function clampSessionInfoLogLimit(input: number | undefined): number {
21
+ if (input === undefined) return SESSION_INFO_DEFAULT_LATEST_LOG_LIMIT;
22
+ if (!Number.isFinite(input) || input < 1) return 1;
23
+ return Math.min(Math.floor(input), SESSION_INFO_MAX_LATEST_LOG_LIMIT);
24
+ }
25
+
26
+ function normalizeString(value: string | null | undefined): string | null {
27
+ if (value === null || value === undefined || value.length === 0) return null;
28
+ return value;
29
+ }
30
+
31
+ function truncateError(value: string | null | undefined): string | null {
32
+ const normalized = normalizeString(value);
33
+ if (normalized === null) return null;
34
+ if (normalized.length <= 500) return normalized;
35
+ return `${normalized.slice(0, 500)}...`;
36
+ }
37
+
38
+ function hasLogError(log: CapturedLog): boolean {
39
+ const error = normalizeString(log.error);
40
+ if (error !== null) return true;
41
+ return log.responseStatus !== null && log.responseStatus >= 400;
42
+ }
43
+
44
+ export function buildSessionLogSummary(log: CapturedLog): SessionLogSummary {
45
+ return {
46
+ id: log.id,
47
+ timestamp: log.timestamp,
48
+ provider: normalizeString(log.providerName),
49
+ model: normalizeString(log.model),
50
+ apiFormat: log.apiFormat,
51
+ method: log.method,
52
+ path: log.path,
53
+ status: log.responseStatus,
54
+ isStreaming: log.streaming,
55
+ hasError: hasLogError(log),
56
+ error: truncateError(log.error),
57
+ latencyMs: log.elapsedMs,
58
+ tokens: {
59
+ input: log.inputTokens,
60
+ output: log.outputTokens,
61
+ cacheCreate: log.cacheCreationInputTokens,
62
+ cacheRead: log.cacheReadInputTokens,
63
+ },
64
+ sessionId: normalizeString(log.sessionId),
65
+ };
66
+ }
67
+
68
+ function nonNegativeInteger(value: number | null): number {
69
+ if (value === null || !Number.isFinite(value) || value < 0) return 0;
70
+ return Math.floor(value);
71
+ }
72
+
73
+ function addTokenUsage(total: SessionTokenUsage, summary: SessionLogSummary): SessionTokenUsage {
74
+ const input = nonNegativeInteger(summary.tokens.input);
75
+ const output = nonNegativeInteger(summary.tokens.output);
76
+ const cacheCreate = nonNegativeInteger(summary.tokens.cacheCreate);
77
+ const cacheRead = nonNegativeInteger(summary.tokens.cacheRead);
78
+ return {
79
+ input: total.input + input,
80
+ output: total.output + output,
81
+ cacheCreate: total.cacheCreate + cacheCreate,
82
+ cacheRead: total.cacheRead + cacheRead,
83
+ total: total.total + input + output + cacheCreate + cacheRead,
84
+ };
85
+ }
86
+
87
+ function buildRelativePath(pathname: string, params: URLSearchParams): string {
88
+ const query = params.toString();
89
+ return query.length > 0 ? `${pathname}?${query}` : pathname;
90
+ }
91
+
92
+ function buildAbsoluteUrl(baseUrl: string, path: string): string {
93
+ try {
94
+ return new URL(path, baseUrl).toString();
95
+ } catch {
96
+ return path;
97
+ }
98
+ }
99
+
100
+ function buildApiPath(sessionId: string): string {
101
+ return buildRelativePath("/api/sessions", new URLSearchParams({ sessionId }));
102
+ }
103
+
104
+ function buildCompactLogsPath(sessionId: string): string {
105
+ return buildRelativePath("/api/logs", new URLSearchParams({ compact: "1", sessionId }));
106
+ }
107
+
108
+ function collectSortedValues(values: readonly (string | null)[]): string[] {
109
+ const set = new Set<string>();
110
+ for (const value of values) {
111
+ if (value !== null && value.length > 0) {
112
+ set.add(value);
113
+ }
114
+ }
115
+ return [...set].sort();
116
+ }
117
+
118
+ function countCompletedLogs(summaries: readonly SessionLogSummary[]): number {
119
+ let count = 0;
120
+ for (const summary of summaries) {
121
+ if (summary.status !== null || summary.latencyMs !== null) {
122
+ count += 1;
123
+ }
124
+ }
125
+ return count;
126
+ }
127
+
128
+ function countErrorLogs(summaries: readonly SessionLogSummary[]): number {
129
+ let count = 0;
130
+ for (const summary of summaries) {
131
+ if (summary.hasError) {
132
+ count += 1;
133
+ }
134
+ }
135
+ return count;
136
+ }
137
+
138
+ function deriveSessionStatus(input: {
139
+ activeRequests: number;
140
+ runningTasks: number;
141
+ failedRequests: number;
142
+ errorCount: number;
143
+ lastTaskError: string | null;
144
+ completedRequests: number;
145
+ logCount: number;
146
+ }): SessionStatus {
147
+ if (input.activeRequests > 0 || input.runningTasks > 0) return "active";
148
+ if (input.failedRequests > 0 || input.errorCount > 0 || input.lastTaskError !== null) {
149
+ return "failed";
150
+ }
151
+ if (input.completedRequests > 0 || input.logCount > 0) return "completed";
152
+ return "empty";
153
+ }
154
+
155
+ export function buildSessionInfo(input: BuildSessionInfoInput): SessionInfo {
156
+ const latestLogLimit = clampSessionInfoLogLimit(input.latestLogLimit);
157
+ const summaries = [...input.summaries].sort((left, right) => right.id - left.id);
158
+ const latestLog = summaries[0] ?? null;
159
+ const oldestLog = summaries.length > 0 ? summaries[summaries.length - 1] : null;
160
+ const logCount =
161
+ input.logCoverage === "history"
162
+ ? summaries.length
163
+ : Math.max(input.snapshot?.requestCount ?? 0, summaries.length);
164
+ const errorCount = Math.max(countErrorLogs(summaries), input.snapshot?.failedRequests ?? 0);
165
+ const completedLogCount = countCompletedLogs(summaries);
166
+ const tokenUsage = summaries.reduce<SessionTokenUsage>(
167
+ (total, summary) => addTokenUsage(total, summary),
168
+ { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, total: 0 },
169
+ );
170
+ const requestCount = Math.max(input.snapshot?.requestCount ?? 0, logCount);
171
+ const activeRequests = input.snapshot?.activeRequests ?? 0;
172
+ const completedRequests = Math.max(input.snapshot?.completedRequests ?? 0, completedLogCount);
173
+ const failedRequests = Math.max(input.snapshot?.failedRequests ?? 0, errorCount);
174
+ const queuedTasks = input.snapshot?.queuedTasks ?? 0;
175
+ const runningTasks = input.snapshot?.runningTasks ?? 0;
176
+ const lastTaskError = normalizeString(input.snapshot?.lastTaskError);
177
+ const inspectorPath = getSessionPath(input.sessionId);
178
+ const apiPath = buildApiPath(input.sessionId);
179
+ const compactLogsPath = buildCompactLogsPath(input.sessionId);
180
+
181
+ return {
182
+ id: input.sessionId,
183
+ status: deriveSessionStatus({
184
+ activeRequests,
185
+ runningTasks,
186
+ failedRequests,
187
+ errorCount,
188
+ lastTaskError,
189
+ completedRequests,
190
+ logCount,
191
+ }),
192
+ logCoverage: input.logCoverage,
193
+ source: input.snapshot?.source ?? null,
194
+ runtimeMode: input.snapshot?.runtimeMode ?? null,
195
+ createdAt: input.snapshot?.createdAt ?? oldestLog?.timestamp ?? null,
196
+ updatedAt: input.snapshot?.updatedAt ?? latestLog?.timestamp ?? null,
197
+ requestCount,
198
+ activeRequests,
199
+ completedRequests,
200
+ failedRequests,
201
+ queuedTasks,
202
+ runningTasks,
203
+ lastTaskError,
204
+ logCount,
205
+ errorCount,
206
+ models: collectSortedValues(summaries.map((summary) => summary.model)),
207
+ providers: collectSortedValues(summaries.map((summary) => summary.provider)),
208
+ tokenUsage,
209
+ lastLogId: input.snapshot?.lastLogId ?? latestLog?.id ?? null,
210
+ lastModel: normalizeString(input.snapshot?.lastModel) ?? latestLog?.model ?? null,
211
+ clientPid: input.snapshot?.clientPid ?? null,
212
+ clientProjectFolder: normalizeString(input.snapshot?.clientProjectFolder),
213
+ inspectorPath,
214
+ inspectorUrl: buildAbsoluteUrl(input.baseUrl, inspectorPath),
215
+ apiPath,
216
+ apiUrl: buildAbsoluteUrl(input.baseUrl, apiPath),
217
+ compactLogsPath,
218
+ compactLogsUrl: buildAbsoluteUrl(input.baseUrl, compactLogsPath),
219
+ latestLogLimit,
220
+ latestLogs: summaries.slice(0, latestLogLimit),
221
+ };
222
+ }
@@ -159,11 +159,14 @@ function upsertSession(
159
159
 
160
160
  const existing = sessions.get(identity.id);
161
161
  const session = existing ?? createSessionRecord(identity.id, source, log);
162
- session.updatedAt = new Date().toISOString();
163
- session.lastLogId = log.id;
164
- session.lastModel = log.model ?? session.lastModel;
165
- session.clientPid = log.clientPid ?? session.clientPid;
166
- session.clientProjectFolder = log.clientProjectFolder ?? session.clientProjectFolder;
162
+ const isSameOrNewerLog = session.lastLogId === null || log.id >= session.lastLogId;
163
+ if (isSameOrNewerLog) {
164
+ session.updatedAt = new Date().toISOString();
165
+ session.lastLogId = log.id;
166
+ session.lastModel = log.model ?? session.lastModel;
167
+ session.clientPid = log.clientPid ?? session.clientPid;
168
+ session.clientProjectFolder = log.clientProjectFolder ?? session.clientProjectFolder;
169
+ }
167
170
 
168
171
  sessions.set(identity.id, session);
169
172
  return session;
@@ -30,11 +30,14 @@ import {
30
30
  type LogIndexEntry,
31
31
  } from "./logIndex";
32
32
  import { writeChunks } from "./chunkStorage";
33
+ import type { SessionInfo, SessionLogSummary } from "../lib/sessionInfoContract";
34
+ import { buildSessionInfo, buildSessionLogSummary } from "./sessionInfo";
33
35
  import type { CapturedLog } from "./schemas";
34
36
  import { CapturedLogSchema } from "./schemas";
35
37
  import {
36
38
  clearSessionRegistry,
37
39
  getLogSessionId,
40
+ getSessionSnapshots,
38
41
  getSessionIds,
39
42
  markSessionFinished,
40
43
  markSessionStarted,
@@ -42,6 +45,7 @@ import {
42
45
  rebuildSessionRegistry,
43
46
  resolveSessionIdentity,
44
47
  type SessionClientInfo,
48
+ type SessionSnapshot,
45
49
  } from "./sessionSupervisor";
46
50
 
47
51
  export type { CapturedLog };
@@ -87,6 +91,12 @@ export type ListLogsPageResult = {
87
91
  limit: number;
88
92
  };
89
93
 
94
+ export type GetSessionInfoOptions = {
95
+ baseUrl: string;
96
+ includeHistory?: boolean;
97
+ latestLogLimit?: number;
98
+ };
99
+
90
100
  // Memory cache: id -> CapturedLog (recent logs only, FIFO eviction)
91
101
  const memoryCache: Map<number, CapturedLog> = new Map();
92
102
 
@@ -506,6 +516,73 @@ export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLo
506
516
  };
507
517
  }
508
518
 
519
+ function findSessionSnapshot(sessionId: string): SessionSnapshot | null {
520
+ for (const snapshot of getSessionSnapshots()) {
521
+ if (snapshot.id === sessionId) return snapshot;
522
+ }
523
+ return null;
524
+ }
525
+
526
+ async function collectSessionLogSummaries(
527
+ sessionId: string,
528
+ includeHistory: boolean,
529
+ ): Promise<SessionLogSummary[]> {
530
+ const summariesById = new Map<number, SessionLogSummary>();
531
+ const observeMatchingLog = (log: CapturedLog): void => {
532
+ if (!matchesLogFilters(log, sessionId, undefined)) return;
533
+ summariesById.set(log.id, buildSessionLogSummary(log));
534
+ };
535
+
536
+ if (includeHistory) {
537
+ await visitPersistedLogs((log) => {
538
+ observeSessionLog(log);
539
+ observeMatchingLog(log);
540
+ });
541
+ }
542
+
543
+ for (const log of memoryCache.values()) {
544
+ observeMatchingLog(log);
545
+ }
546
+
547
+ return [...summariesById.values()];
548
+ }
549
+
550
+ async function includeSnapshotLastLogSummary(
551
+ sessionId: string,
552
+ snapshot: SessionSnapshot | null,
553
+ summaries: SessionLogSummary[],
554
+ ): Promise<SessionLogSummary[]> {
555
+ if (snapshot === null || snapshot.lastLogId === null) return summaries;
556
+ for (const summary of summaries) {
557
+ if (summary.id === snapshot.lastLogId) return summaries;
558
+ }
559
+
560
+ const log = await getLogById(snapshot.lastLogId);
561
+ if (log === null || !matchesLogFilters(log, sessionId, undefined)) return summaries;
562
+ return [...summaries, buildSessionLogSummary(log)];
563
+ }
564
+
565
+ export async function getSessionInfo(
566
+ sessionId: string,
567
+ options: GetSessionInfoOptions,
568
+ ): Promise<SessionInfo | null> {
569
+ const includeHistory = options.includeHistory === true;
570
+ const collectedSummaries = await collectSessionLogSummaries(sessionId, includeHistory);
571
+ const snapshot = findSessionSnapshot(sessionId);
572
+ const summaries = includeHistory
573
+ ? collectedSummaries
574
+ : await includeSnapshotLastLogSummary(sessionId, snapshot, collectedSummaries);
575
+ if (snapshot === null && summaries.length === 0) return null;
576
+ return buildSessionInfo({
577
+ sessionId,
578
+ baseUrl: options.baseUrl,
579
+ snapshot,
580
+ summaries,
581
+ logCoverage: includeHistory ? "history" : "recent",
582
+ latestLogLimit: options.latestLogLimit,
583
+ });
584
+ }
585
+
509
586
  export function getSessions(): string[] {
510
587
  return getSessionIds();
511
588
  }
@@ -1,11 +1,38 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { getSessionSnapshots, getSessions } from "../../proxy/store";
2
+ import { getSessionInfo, getSessionSnapshots, getSessions } from "../../proxy/store";
3
+
4
+ function parsePositiveInt(value: string | null): number | undefined {
5
+ if (value === null) return undefined;
6
+ const parsed = Number(value);
7
+ if (!Number.isInteger(parsed) || parsed < 1) return undefined;
8
+ return parsed;
9
+ }
10
+
11
+ function parseBooleanFlag(value: string | null): boolean {
12
+ return value === "1" || value === "true";
13
+ }
3
14
 
4
15
  export const Route = createFileRoute("/api/sessions")({
5
16
  server: {
6
17
  handlers: {
7
- GET: ({ request }: { request: Request }) => {
18
+ GET: async ({ request }: { request: Request }) => {
8
19
  const url = new URL(request.url);
20
+ const sessionId = url.searchParams.get("sessionId");
21
+ if (sessionId !== null) {
22
+ if (sessionId.length === 0) {
23
+ return Response.json({ error: "sessionId is required" }, { status: 400 });
24
+ }
25
+ const info = await getSessionInfo(sessionId, {
26
+ baseUrl: url.origin,
27
+ includeHistory: parseBooleanFlag(url.searchParams.get("includeHistory")),
28
+ latestLogLimit: parsePositiveInt(url.searchParams.get("limit")),
29
+ });
30
+ if (info === null) {
31
+ return Response.json({ error: "Session not found" }, { status: 404 });
32
+ }
33
+ return Response.json(info);
34
+ }
35
+
9
36
  const details = url.searchParams.get("details");
10
37
  if (details === "true" || details === "1") {
11
38
  return Response.json(getSessionSnapshots());