@tonyclaw/agent-inspector 2.0.26 → 2.0.27

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 (40) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DBTFzxG5.js → CompareDrawer-B_p6vmMQ.js} +1 -1
  3. package/.output/public/assets/{ProxyViewerContainer-ivZk8MgE.js → ProxyViewerContainer-DItqh1EO.js} +4 -4
  4. package/.output/public/assets/{ReplayDialog-syW2hDNE.js → ReplayDialog-BmprJ6D3.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-BkuRtY9o.js → RequestAnatomy-DqO0_SVJ.js} +1 -1
  6. package/.output/public/assets/{ResponseView-BwyvEvoE.js → ResponseView-FZbPNdHa.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-BY4MbPKg.js → StreamingChunkSequence-BZPzfJWj.js} +1 -1
  8. package/.output/public/assets/_sessionId-y2ATIp51.js +1 -0
  9. package/.output/public/assets/index-CQT4higx.js +1 -0
  10. package/.output/public/assets/{main-DKRDRBdd.js → main-CzItFZlM.js} +2 -2
  11. package/.output/server/_libs/modelcontextprotocol__server.mjs +228 -0
  12. package/.output/server/{_sessionId-VDd4N_1B.mjs → _sessionId-DZH8SrEZ.mjs} +3 -3
  13. package/.output/server/_ssr/{CompareDrawer-BQZlxsOC.mjs → CompareDrawer-L0aE1UV1.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-njY2oQCc.mjs → ProxyViewerContainer-B62RB9ER.mjs} +7 -7
  15. package/.output/server/_ssr/{ReplayDialog-B8lkdAIh.mjs → ReplayDialog-FVWnpx2C.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-CwOTfdwS.mjs → RequestAnatomy-DIyqW-Ny.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-3Iz_mZpd.mjs → ResponseView-BX4mxEZ5.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-hmJcQNW5.mjs → StreamingChunkSequence-Cs3nzOor.mjs} +2 -2
  19. package/.output/server/_ssr/{index-UuTKM4aC.mjs → index-Dik2Mc3h.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-C7InHrxE.mjs → router-DCPg8ykx.mjs} +1816 -151
  22. package/.output/server/_tanstack-start-manifest_v-BaoL3JCh.mjs +4 -0
  23. package/.output/server/index.mjs +57 -57
  24. package/README.md +55 -1
  25. package/package.json +1 -1
  26. package/src/lib/runContract.ts +162 -0
  27. package/src/mcp/server.ts +554 -2
  28. package/src/mcp/toolHandlers.ts +154 -0
  29. package/src/proxy/evidenceAnalysis.ts +522 -0
  30. package/src/proxy/evidenceExporter.ts +215 -0
  31. package/src/proxy/logSearch.ts +118 -0
  32. package/src/proxy/runFailures.ts +100 -0
  33. package/src/proxy/runStore.ts +159 -0
  34. package/src/routes/api/logs.ts +16 -0
  35. package/src/routes/api/runs.$runId.evidence.ts +62 -0
  36. package/src/routes/api/runs.$runId.ts +50 -0
  37. package/src/routes/api/runs.ts +58 -0
  38. package/.output/public/assets/_sessionId-XlPUgIIb.js +0 -1
  39. package/.output/public/assets/index-By11a28-.js +0 -1
  40. package/.output/server/_tanstack-start-manifest_v-B6yfnMHA.mjs +0 -4
@@ -0,0 +1,215 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ type EvidenceExportOptions,
5
+ type EvidenceExportResult,
6
+ type InspectorRun,
7
+ type InspectorRunEvidence,
8
+ } from "../lib/runContract";
9
+ import { getDataDir } from "./dataDir";
10
+ import { analyzeEvidence } from "./evidenceAnalysis";
11
+ import { getSessionInfo } from "./store";
12
+ import { updateRunEvidence } from "./runStore";
13
+
14
+ type EvidenceDocument = EvidenceExportResult;
15
+
16
+ function evidenceDir(runId: string): string {
17
+ return join(getDataDir(), "evidence", runId);
18
+ }
19
+
20
+ function jsonPath(runId: string): string {
21
+ return join(evidenceDir(runId), "evidence.json");
22
+ }
23
+
24
+ function markdownPath(runId: string): string {
25
+ return join(evidenceDir(runId), "evidence.md");
26
+ }
27
+
28
+ function htmlPath(runId: string): string {
29
+ return join(evidenceDir(runId), "evidence.html");
30
+ }
31
+
32
+ function formatList(values: readonly string[]): string {
33
+ return values.length > 0 ? values.join(", ") : "n/a";
34
+ }
35
+
36
+ function escapeHtml(value: string): string {
37
+ return value
38
+ .replace(/&/g, "&")
39
+ .replace(/</g, "&lt;")
40
+ .replace(/>/g, "&gt;")
41
+ .replace(/"/g, "&quot;");
42
+ }
43
+
44
+ function buildMarkdown(document: EvidenceDocument): string {
45
+ const session = document.session;
46
+ const lines = [
47
+ `# ${document.run.title}`,
48
+ "",
49
+ `- Run ID: ${document.run.id}`,
50
+ `- Session ID: ${document.run.sessionId}`,
51
+ `- Status: ${document.run.status}`,
52
+ `- Project: ${document.run.project ?? "n/a"}`,
53
+ `- Agent: ${document.run.agent ?? "n/a"}`,
54
+ `- Created: ${document.run.createdAt}`,
55
+ "",
56
+ `- Outcome: ${document.classification.outcome}`,
57
+ `- Failure Category: ${document.classification.category}`,
58
+ `- Severity: ${document.classification.severity}`,
59
+ `- Evidence JSON: ${document.evidence.jsonPath}`,
60
+ `- Evidence HTML: ${document.evidence.htmlPath}`,
61
+ "",
62
+ ];
63
+
64
+ if (document.run.task !== null) {
65
+ lines.push("## Task", "", document.run.task, "");
66
+ }
67
+
68
+ if (session === null) {
69
+ lines.push("## Session", "", "No Inspector session data was available for this run.", "");
70
+ } else {
71
+ lines.push(
72
+ "## Session",
73
+ "",
74
+ `- Inspector URL: ${session.inspectorUrl}`,
75
+ `- Compact Logs URL: ${session.compactLogsUrl}`,
76
+ `- Status: ${session.status}`,
77
+ `- Coverage: ${session.logCoverage}`,
78
+ `- Requests: ${session.requestCount}`,
79
+ `- Errors: ${session.errorCount}`,
80
+ `- Models: ${formatList(session.models)}`,
81
+ `- Providers: ${formatList(session.providers)}`,
82
+ `- Tokens: ${session.tokenUsage.total}`,
83
+ "",
84
+ "## Failure Classification",
85
+ "",
86
+ document.classification.summary,
87
+ "",
88
+ "### Evidence Logs",
89
+ "",
90
+ document.classification.evidenceLogIds.length > 0
91
+ ? document.classification.evidenceLogIds.map((id) => `- #${id}`).join("\n")
92
+ : "No failing log ids were identified.",
93
+ "",
94
+ "### Suggested Next Actions",
95
+ "",
96
+ ...document.classification.hints.map((hint) => `- ${hint}`),
97
+ "",
98
+ "## Run Timeline",
99
+ "",
100
+ "| Time | Event | Severity | Evidence |",
101
+ "| --- | --- | --- | --- |",
102
+ ...document.timeline.map((event) => {
103
+ const evidence = event.logId === null ? "n/a" : `log ${event.logId}`;
104
+ return `| ${event.timestamp} | ${event.title} | ${event.severity} | ${evidence} |`;
105
+ }),
106
+ "",
107
+ "## Latest Logs",
108
+ "",
109
+ );
110
+
111
+ if (session.latestLogs.length === 0) {
112
+ lines.push("No compact logs were available.", "");
113
+ } else {
114
+ lines.push(
115
+ "| ID | Status | Model | Latency | Path | Error |",
116
+ "| --- | --- | --- | --- | --- | --- |",
117
+ );
118
+ for (const log of session.latestLogs) {
119
+ lines.push(
120
+ `| ${log.id} | ${log.status ?? "n/a"} | ${log.model ?? "n/a"} | ${
121
+ log.latencyMs ?? "n/a"
122
+ } | ${log.path} | ${log.hasError ? "yes" : "no"} |`,
123
+ );
124
+ }
125
+ lines.push("");
126
+ }
127
+ }
128
+
129
+ lines.push("## Jenkins Report", "", document.jenkinsReport.markdown, "");
130
+
131
+ return `${lines.join("\n")}\n`;
132
+ }
133
+
134
+ function buildHtml(markdown: string, title: string): string {
135
+ return `<!doctype html>
136
+ <html lang="en">
137
+ <head>
138
+ <meta charset="utf-8">
139
+ <title>${escapeHtml(title)}</title>
140
+ <style>
141
+ body { font-family: system-ui, sans-serif; max-width: 960px; margin: 40px auto; line-height: 1.5; }
142
+ pre { white-space: pre-wrap; background: #f6f8fa; padding: 16px; border-radius: 8px; }
143
+ </style>
144
+ </head>
145
+ <body>
146
+ <pre>${escapeHtml(markdown)}</pre>
147
+ </body>
148
+ </html>
149
+ `;
150
+ }
151
+
152
+ export async function exportRunEvidence(
153
+ run: InspectorRun,
154
+ options: EvidenceExportOptions,
155
+ baseUrl: string,
156
+ ): Promise<EvidenceExportResult> {
157
+ const session = await getSessionInfo(run.sessionId, {
158
+ baseUrl,
159
+ includeHistory: options?.includeHistory,
160
+ latestLogLimit: options?.latestLogLimit,
161
+ });
162
+ const exportedAt = new Date().toISOString();
163
+ const evidence: InspectorRunEvidence = {
164
+ jsonPath: jsonPath(run.id),
165
+ markdownPath: markdownPath(run.id),
166
+ htmlPath: htmlPath(run.id),
167
+ exportedAt,
168
+ };
169
+ const documentRun: InspectorRun = {
170
+ ...run,
171
+ evidence,
172
+ updatedAt: exportedAt,
173
+ };
174
+ const analysis = analyzeEvidence({
175
+ run: documentRun,
176
+ session,
177
+ exportedAt,
178
+ evidencePaths: {
179
+ jsonPath: evidence.jsonPath,
180
+ markdownPath: evidence.markdownPath,
181
+ htmlPath: evidence.htmlPath,
182
+ },
183
+ });
184
+ const document: EvidenceDocument = {
185
+ run: documentRun,
186
+ evidence,
187
+ session,
188
+ timeline: analysis.timeline,
189
+ classification: analysis.classification,
190
+ jenkinsReport: analysis.jenkinsReport,
191
+ };
192
+ const markdown = buildMarkdown(document);
193
+
194
+ mkdirSync(evidenceDir(run.id), { recursive: true });
195
+ writeFileSync(
196
+ evidence.jsonPath,
197
+ `${JSON.stringify({ ...document, evidence }, null, 2)}\n`,
198
+ "utf8",
199
+ );
200
+ writeFileSync(evidence.markdownPath, markdown, "utf8");
201
+ writeFileSync(evidence.htmlPath, buildHtml(markdown, run.title), "utf8");
202
+
203
+ const updatedRun = updateRunEvidence(run.id, evidence) ?? documentRun;
204
+ return { ...document, run: updatedRun };
205
+ }
206
+
207
+ export function readEvidenceMarkdown(run: InspectorRun): string | null {
208
+ const path = run.evidence?.markdownPath;
209
+ if (path === undefined || !existsSync(path)) return null;
210
+ try {
211
+ return readFileSync(path, "utf8");
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
@@ -0,0 +1,118 @@
1
+ import type { CapturedLog } from "./schemas";
2
+ import { listLogsPage } from "./store";
3
+
4
+ export const LOG_SEARCH_DEFAULT_LIMIT = 10;
5
+ export const LOG_SEARCH_MAX_LIMIT = 50;
6
+ export const LOG_SEARCH_DEFAULT_SCAN_LIMIT = 200;
7
+ export const LOG_SEARCH_MAX_SCAN_LIMIT = 1_000;
8
+
9
+ export type LogSearchOptions = {
10
+ query: string;
11
+ sessionId?: string;
12
+ model?: string;
13
+ offset: number;
14
+ limit: number;
15
+ scanLimit: number;
16
+ };
17
+
18
+ export type LogSearchResult = {
19
+ query: string;
20
+ logs: CapturedLog[];
21
+ total: number;
22
+ offset: number;
23
+ limit: number;
24
+ scanned: number;
25
+ };
26
+
27
+ function clampPositiveInt(value: number, fallback: number, max: number): number {
28
+ if (!Number.isInteger(value) || value < 1) return fallback;
29
+ return Math.min(value, max);
30
+ }
31
+
32
+ export function clampLogSearchLimit(value: number): number {
33
+ return clampPositiveInt(value, LOG_SEARCH_DEFAULT_LIMIT, LOG_SEARCH_MAX_LIMIT);
34
+ }
35
+
36
+ export function clampLogSearchScanLimit(value: number): number {
37
+ return clampPositiveInt(value, LOG_SEARCH_DEFAULT_SCAN_LIMIT, LOG_SEARCH_MAX_SCAN_LIMIT);
38
+ }
39
+
40
+ function queryTerms(query: string): string[] {
41
+ return query
42
+ .trim()
43
+ .toLowerCase()
44
+ .split(/\s+/)
45
+ .filter((term) => term.length > 0);
46
+ }
47
+
48
+ function optionalText(value: string | number | boolean | null | undefined): string | null {
49
+ if (value === null || value === undefined) return null;
50
+ return String(value);
51
+ }
52
+
53
+ function searchableFields(log: CapturedLog): string[] {
54
+ return [
55
+ String(log.id),
56
+ log.timestamp,
57
+ log.method,
58
+ log.path,
59
+ optionalText(log.model),
60
+ optionalText(log.sessionId),
61
+ optionalText(log.responseStatus),
62
+ optionalText(log.elapsedMs),
63
+ optionalText(log.streaming),
64
+ log.streaming ? "streaming" : "non-streaming",
65
+ log.apiFormat,
66
+ optionalText(log.providerName),
67
+ optionalText(log.userAgent),
68
+ optionalText(log.origin),
69
+ optionalText(log.clientPid),
70
+ optionalText(log.clientCwd),
71
+ optionalText(log.clientProjectFolder),
72
+ optionalText(log.error),
73
+ ].filter((value): value is string => value !== null && value.length > 0);
74
+ }
75
+
76
+ function matchesTerms(log: CapturedLog, terms: readonly string[]): boolean {
77
+ if (terms.length === 0) return true;
78
+ const haystack = searchableFields(log).join("\n").toLowerCase();
79
+ return terms.every((term) => haystack.includes(term));
80
+ }
81
+
82
+ function sortNewestFirst(logs: CapturedLog[]): CapturedLog[] {
83
+ return logs.sort((left, right) => {
84
+ const timestampCompare = right.timestamp.localeCompare(left.timestamp);
85
+ return timestampCompare !== 0 ? timestampCompare : right.id - left.id;
86
+ });
87
+ }
88
+
89
+ export async function searchLogsPage(options: LogSearchOptions): Promise<LogSearchResult> {
90
+ const limit = clampLogSearchLimit(options.limit);
91
+ const scanLimit = clampLogSearchScanLimit(Math.max(options.scanLimit, options.offset + limit));
92
+ const firstPage = await listLogsPage({
93
+ sessionId: options.sessionId,
94
+ model: options.model,
95
+ offset: 0,
96
+ limit: 1,
97
+ includeBodies: false,
98
+ });
99
+ const scanOffset = Math.max(firstPage.total - scanLimit, 0);
100
+ const scanPage = await listLogsPage({
101
+ sessionId: options.sessionId,
102
+ model: options.model,
103
+ offset: scanOffset,
104
+ limit: scanLimit,
105
+ includeBodies: false,
106
+ });
107
+ const terms = queryTerms(options.query);
108
+ const matches = sortNewestFirst(scanPage.logs.filter((log) => matchesTerms(log, terms)));
109
+
110
+ return {
111
+ query: options.query,
112
+ logs: matches.slice(options.offset, options.offset + limit),
113
+ total: matches.length,
114
+ offset: options.offset,
115
+ limit,
116
+ scanned: scanPage.logs.length,
117
+ };
118
+ }
@@ -0,0 +1,100 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import {
3
+ EvidenceExportResultSchema,
4
+ type EvidenceExportResult,
5
+ type FailureClassification,
6
+ type InspectorRun,
7
+ type InspectorRunEvidence,
8
+ type JenkinsReport,
9
+ type RecentFailure,
10
+ type RecentFailuresResponse,
11
+ } from "../lib/runContract";
12
+ import { listRuns } from "./runStore";
13
+
14
+ export const RECENT_FAILURES_DEFAULT_LIMIT = 5;
15
+ export const RECENT_FAILURES_MAX_LIMIT = 20;
16
+
17
+ export function clampRecentFailuresLimit(value: number): number {
18
+ if (!Number.isInteger(value) || value < 1) return RECENT_FAILURES_DEFAULT_LIMIT;
19
+ return Math.min(value, RECENT_FAILURES_MAX_LIMIT);
20
+ }
21
+
22
+ function readEvidenceDocument(run: InspectorRun): EvidenceExportResult | null {
23
+ const path = run.evidence?.jsonPath;
24
+ if (path === undefined || !existsSync(path)) return null;
25
+
26
+ try {
27
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
28
+ const result = EvidenceExportResultSchema.safeParse(parsed);
29
+ return result.success ? result.data : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function syntheticFailedClassification(run: InspectorRun): FailureClassification {
36
+ return {
37
+ outcome: "failure",
38
+ severity: "high",
39
+ category: "unknown-failure",
40
+ summary: `Run ${run.id} is marked ${run.status}, but no exported evidence document was available.`,
41
+ evidenceLogIds: [],
42
+ hints: [
43
+ "Call inspector_export_evidence for this run to collect session metrics, failing log ids, and artifact paths.",
44
+ "Confirm the run's sessionId matches the coding agent traffic captured by Inspector.",
45
+ ],
46
+ };
47
+ }
48
+
49
+ function shouldIncludeRun(
50
+ run: InspectorRun,
51
+ document: EvidenceExportResult | null,
52
+ classification: FailureClassification,
53
+ ): boolean {
54
+ if (run.status === "failed" || run.status === "cancelled") return true;
55
+ if (document === null) return false;
56
+ return classification.outcome !== "success";
57
+ }
58
+
59
+ function evidenceFor(
60
+ run: InspectorRun,
61
+ document: EvidenceExportResult | null,
62
+ ): InspectorRunEvidence | null {
63
+ return document?.evidence ?? run.evidence;
64
+ }
65
+
66
+ function jenkinsReportFor(document: EvidenceExportResult | null): JenkinsReport | null {
67
+ return document?.jenkinsReport ?? null;
68
+ }
69
+
70
+ function failureFromRun(run: InspectorRun): RecentFailure | null {
71
+ const document = readEvidenceDocument(run);
72
+ const classification =
73
+ document === null ? syntheticFailedClassification(run) : document.classification;
74
+ if (!shouldIncludeRun(run, document, classification)) return null;
75
+
76
+ const evidence = evidenceFor(run, document);
77
+ return {
78
+ run,
79
+ classification,
80
+ evidence,
81
+ jenkinsReport: jenkinsReportFor(document),
82
+ sessionId: run.sessionId,
83
+ inspectorUrl: document?.session?.inspectorUrl ?? null,
84
+ evidenceMarkdownPath: evidence?.markdownPath ?? null,
85
+ updatedAt: evidence?.exportedAt ?? run.updatedAt,
86
+ };
87
+ }
88
+
89
+ export function listRecentFailures(limit: number): RecentFailuresResponse {
90
+ const failures = listRuns()
91
+ .map(failureFromRun)
92
+ .filter((failure): failure is RecentFailure => failure !== null)
93
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
94
+ const clampedLimit = clampRecentFailuresLimit(limit);
95
+ return {
96
+ failures: failures.slice(0, clampedLimit),
97
+ total: failures.length,
98
+ limit: clampedLimit,
99
+ };
100
+ }
@@ -0,0 +1,159 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dirname, join } from "node:path";
4
+ import { z } from "zod";
5
+ import {
6
+ CreateInspectorRunInputSchema,
7
+ InspectorRunSchema,
8
+ UpdateInspectorRunInputSchema,
9
+ type CreateInspectorRunInput,
10
+ type InspectorRun,
11
+ type InspectorRunEvidence,
12
+ type UpdateInspectorRunInput,
13
+ } from "../lib/runContract";
14
+ import { getDataDir } from "./dataDir";
15
+
16
+ const RunsFileSchema = z.object({
17
+ runs: z.array(InspectorRunSchema),
18
+ });
19
+
20
+ function runsFilePath(): string {
21
+ return join(getDataDir(), "runs.json");
22
+ }
23
+
24
+ function nowIso(): string {
25
+ return new Date().toISOString();
26
+ }
27
+
28
+ function stableId(input: string): string {
29
+ return input
30
+ .trim()
31
+ .replace(/[^A-Za-z0-9_.-]+/g, "-")
32
+ .replace(/^-+|-+$/g, "")
33
+ .slice(0, 80);
34
+ }
35
+
36
+ function createRunId(): string {
37
+ return `run_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
38
+ }
39
+
40
+ function normalizeRunId(input: string | undefined): string {
41
+ if (input === undefined) return createRunId();
42
+ const normalized = stableId(input);
43
+ return normalized.length > 0 ? normalized : createRunId();
44
+ }
45
+
46
+ function normalizeSessionId(input: string | undefined, runId: string): string {
47
+ if (input === undefined || input.trim().length === 0) return runId;
48
+ return input.trim();
49
+ }
50
+
51
+ function readRuns(): InspectorRun[] {
52
+ const filePath = runsFilePath();
53
+ if (!existsSync(filePath)) return [];
54
+
55
+ try {
56
+ const parsed: unknown = JSON.parse(readFileSync(filePath, "utf8"));
57
+ const result = RunsFileSchema.safeParse(parsed);
58
+ return result.success ? result.data.runs : [];
59
+ } catch {
60
+ return [];
61
+ }
62
+ }
63
+
64
+ function writeRuns(runs: readonly InspectorRun[]): void {
65
+ const filePath = runsFilePath();
66
+ mkdirSync(dirname(filePath), { recursive: true });
67
+ writeFileSync(filePath, `${JSON.stringify({ runs }, null, 2)}\n`, "utf8");
68
+ }
69
+
70
+ export function listRuns(): InspectorRun[] {
71
+ return readRuns().sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
72
+ }
73
+
74
+ export function getRun(runId: string): InspectorRun | null {
75
+ return readRuns().find((run) => run.id === runId) ?? null;
76
+ }
77
+
78
+ export function createRun(input: CreateInspectorRunInput): InspectorRun {
79
+ const parsed = CreateInspectorRunInputSchema.parse(input);
80
+ const runId = normalizeRunId(parsed?.runId);
81
+ const existing = getRun(runId);
82
+ if (existing !== null) return existing;
83
+
84
+ const timestamp = nowIso();
85
+ const sessionId = normalizeSessionId(parsed?.sessionId, runId);
86
+ const run: InspectorRun = {
87
+ id: runId,
88
+ sessionId,
89
+ title: parsed?.title ?? `Inspector Run ${runId}`,
90
+ task: parsed?.task ?? null,
91
+ project: parsed?.project ?? null,
92
+ agent: parsed?.agent ?? null,
93
+ status: parsed?.status ?? "created",
94
+ tags: parsed?.tags ?? [],
95
+ metadata: parsed?.metadata ?? {},
96
+ evidence: null,
97
+ createdAt: timestamp,
98
+ updatedAt: timestamp,
99
+ };
100
+
101
+ writeRuns([...readRuns(), run]);
102
+ return run;
103
+ }
104
+
105
+ export function updateRunEvidence(
106
+ runId: string,
107
+ evidence: InspectorRunEvidence,
108
+ ): InspectorRun | null {
109
+ const runs = readRuns();
110
+ let updatedRun: InspectorRun | null = null;
111
+ const updatedRuns = runs.map((run) => {
112
+ if (run.id !== runId) return run;
113
+ updatedRun = {
114
+ ...run,
115
+ evidence,
116
+ updatedAt: evidence.exportedAt,
117
+ };
118
+ return updatedRun;
119
+ });
120
+ if (updatedRun === null) return null;
121
+ writeRuns(updatedRuns);
122
+ return updatedRun;
123
+ }
124
+
125
+ export function updateRun(runId: string, input: UpdateInspectorRunInput): InspectorRun | null {
126
+ const parsed = UpdateInspectorRunInputSchema.parse(input);
127
+ const runs = readRuns();
128
+ let updatedRun: InspectorRun | null = null;
129
+ const updatedAt = nowIso();
130
+
131
+ const updatedRuns = runs.map((run) => {
132
+ if (run.id !== runId) return run;
133
+ updatedRun = {
134
+ ...run,
135
+ sessionId:
136
+ parsed.sessionId === undefined
137
+ ? run.sessionId
138
+ : normalizeSessionId(parsed.sessionId, run.id),
139
+ title: parsed.title ?? run.title,
140
+ task: parsed.task === undefined ? run.task : parsed.task,
141
+ project: parsed.project === undefined ? run.project : parsed.project,
142
+ agent: parsed.agent === undefined ? run.agent : parsed.agent,
143
+ status: parsed.status ?? run.status,
144
+ tags: parsed.tags ?? run.tags,
145
+ metadata:
146
+ parsed.metadata === undefined ? run.metadata : { ...run.metadata, ...parsed.metadata },
147
+ updatedAt,
148
+ };
149
+ return updatedRun;
150
+ });
151
+
152
+ if (updatedRun === null) return null;
153
+ writeRuns(updatedRuns);
154
+ return updatedRun;
155
+ }
156
+
157
+ export function _resetRunsForTests(): void {
158
+ writeRuns([]);
159
+ }
@@ -7,6 +7,11 @@ import {
7
7
  getReplayLogsForSource,
8
8
  listLogsPage,
9
9
  } from "../../proxy/store";
10
+ import {
11
+ LOG_SEARCH_DEFAULT_LIMIT,
12
+ LOG_SEARCH_DEFAULT_SCAN_LIMIT,
13
+ searchLogsPage,
14
+ } from "../../proxy/logSearch";
10
15
 
11
16
  const DeleteBodySchema = z
12
17
  .object({
@@ -47,7 +52,18 @@ export const Route = createFileRoute("/api/logs")({
47
52
  }
48
53
  const sessionId = url.searchParams.get("sessionId") ?? undefined;
49
54
  const model = url.searchParams.get("model") ?? undefined;
55
+ const query = url.searchParams.get("q") ?? url.searchParams.get("search");
50
56
  const offset = parseNonNegativeInt(url.searchParams.get("offset"), 0);
57
+ if (query !== null) {
58
+ const limit = parsePositiveInt(url.searchParams.get("limit"), LOG_SEARCH_DEFAULT_LIMIT);
59
+ const scanLimit = parsePositiveInt(
60
+ url.searchParams.get("scanLimit"),
61
+ LOG_SEARCH_DEFAULT_SCAN_LIMIT,
62
+ );
63
+ return Response.json(
64
+ await searchLogsPage({ query, sessionId, model, offset, limit, scanLimit }),
65
+ );
66
+ }
51
67
  const limit = parsePositiveInt(url.searchParams.get("limit"), 50);
52
68
  const includeBodies = url.searchParams.get("compact") !== "1";
53
69
 
@@ -0,0 +1,62 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { EvidenceExportOptionsSchema } from "../../lib/runContract";
3
+ import { exportRunEvidence, readEvidenceMarkdown } from "../../proxy/evidenceExporter";
4
+ import { getRun } from "../../proxy/runStore";
5
+
6
+ type JsonBodyResult = { ok: true; value: unknown } | { ok: false };
7
+
8
+ async function readJsonBody(request: Request): Promise<JsonBodyResult> {
9
+ try {
10
+ const raw = await request.text();
11
+ if (raw.trim().length === 0) return { ok: true, value: undefined };
12
+ return { ok: true, value: JSON.parse(raw) };
13
+ } catch {
14
+ return { ok: false };
15
+ }
16
+ }
17
+
18
+ export const Route = createFileRoute("/api/runs/$runId/evidence")({
19
+ server: {
20
+ handlers: {
21
+ GET: ({ params }: { params: { runId: string } }) => {
22
+ const run = getRun(params.runId);
23
+ if (run === null) {
24
+ return Response.json({ error: "Run not found" }, { status: 404 });
25
+ }
26
+
27
+ const markdown = readEvidenceMarkdown(run);
28
+ if (run.evidence === null || markdown === null) {
29
+ return Response.json(
30
+ { error: "Evidence has not been exported for this run" },
31
+ { status: 404 },
32
+ );
33
+ }
34
+
35
+ return Response.json({ run, evidence: run.evidence, markdown });
36
+ },
37
+ POST: async ({ params, request }: { params: { runId: string }; request: Request }) => {
38
+ const run = getRun(params.runId);
39
+ if (run === null) {
40
+ return Response.json({ error: "Run not found" }, { status: 404 });
41
+ }
42
+
43
+ const body = await readJsonBody(request);
44
+ if (!body.ok) {
45
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
46
+ }
47
+
48
+ const parsed = EvidenceExportOptionsSchema.safeParse(body.value);
49
+ if (!parsed.success) {
50
+ return Response.json(
51
+ { error: "Invalid request body", details: parsed.error.format() },
52
+ { status: 400 },
53
+ );
54
+ }
55
+
56
+ return Response.json(
57
+ await exportRunEvidence(run, parsed.data, new URL(request.url).origin),
58
+ );
59
+ },
60
+ },
61
+ },
62
+ });