kiku-agent 1.3.1 → 1.3.3

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 (232) hide show
  1. package/README.md +69 -12
  2. package/package.json +5 -12
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/AGENTS.md +228 -0
  18. package/server/agent/eval/analyze.py +684 -0
  19. package/server/agent/eval/gates.json +15 -0
  20. package/server/agent/eval/modules/analysis/compare.js +198 -0
  21. package/server/agent/eval/modules/analysis/gates.js +72 -0
  22. package/server/agent/eval/modules/analysis/judge.js +70 -0
  23. package/server/agent/eval/modules/core/cli.js +453 -0
  24. package/server/agent/eval/modules/core/config.js +60 -0
  25. package/server/agent/eval/modules/core/store.js +236 -0
  26. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  27. package/server/agent/eval/modules/evaluation/runner.js +288 -0
  28. package/server/agent/eval/modules/evaluation/scoring.js +363 -0
  29. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  30. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  31. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  32. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  33. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  34. package/server/agent/eval/modules/ui/evalUi.js +4799 -0
  35. package/server/agent/eval/modules/utils/smokes.js +119 -0
  36. package/server/agent/eventBus.js +1 -0
  37. package/server/agent/ink/main.jsx +196 -191
  38. package/server/agent/jobManager.js +11 -1
  39. package/server/agent/lanDiscovery.js +721 -23
  40. package/server/agent/llmClient.js +317 -71
  41. package/server/agent/lmStudioWakeup.js +517 -65
  42. package/server/agent/locationService.js +797 -0
  43. package/server/agent/mcpQuizData.js +509 -0
  44. package/server/agent/memory/contextAllocator.js +19 -7
  45. package/server/agent/memory.js +3 -1
  46. package/server/agent/modelCompatibility.js +352 -0
  47. package/server/agent/modelDefaults.js +1 -0
  48. package/server/agent/modelManager.js +46 -2
  49. package/server/agent/newsWatch.js +9 -3
  50. package/server/agent/processManager.js +229 -0
  51. package/server/agent/prompts.js +174 -8
  52. package/server/agent/routingInterceptor.js +9 -1
  53. package/server/agent/sanitizer.js +41 -6
  54. package/server/agent/searchQueryPlanner.js +202 -0
  55. package/server/agent/session.js +59 -4
  56. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  57. package/server/agent/tests/agentFollowUps.test.js +253 -0
  58. package/server/agent/tests/agentMusicControl.test.js +245 -0
  59. package/server/agent/tests/apiRouter.test.js +108 -0
  60. package/server/agent/tests/architectureFixes.test.js +81 -0
  61. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  62. package/server/agent/tests/autoContext.test.js +176 -0
  63. package/server/agent/tests/autoResearch.test.js +64 -0
  64. package/server/agent/tests/bilibiliEmbed.test.js +296 -0
  65. package/server/agent/tests/catalogSync.test.js +130 -0
  66. package/server/agent/tests/chatHardening.test.js +121 -0
  67. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  68. package/server/agent/tests/citations.test.js +180 -0
  69. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  70. package/server/agent/tests/clarifyingQuestions.test.js +627 -0
  71. package/server/agent/tests/cliAutomation.test.js +158 -0
  72. package/server/agent/tests/cloudProviders.test.js +322 -0
  73. package/server/agent/tests/commandInput.test.js +24 -0
  74. package/server/agent/tests/commands.test.js +506 -0
  75. package/server/agent/tests/contextIntelligence.test.js +209 -0
  76. package/server/agent/tests/conversationSafety.test.js +53 -0
  77. package/server/agent/tests/cpuMetrics.test.js +101 -0
  78. package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
  79. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  80. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  81. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  82. package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
  83. package/server/agent/tests/engineService.test.js +436 -0
  84. package/server/agent/tests/engineUnload.test.js +94 -0
  85. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  86. package/server/agent/tests/envBootstrap.test.js +87 -0
  87. package/server/agent/tests/envCommand.test.js +284 -0
  88. package/server/agent/tests/errorHandling.test.js +80 -0
  89. package/server/agent/tests/errors.test.js +13 -0
  90. package/server/agent/tests/evalAnalyze.test.js +54 -0
  91. package/server/agent/tests/evalCommand.test.js +54 -0
  92. package/server/agent/tests/evalCorpus.test.js +126 -0
  93. package/server/agent/tests/evalHarness.test.js +62 -0
  94. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  95. package/server/agent/tests/evalMetrics.test.js +320 -0
  96. package/server/agent/tests/evalStore.test.js +190 -0
  97. package/server/agent/tests/evalStudio.test.js +179 -0
  98. package/server/agent/tests/feedbackLearning.test.js +186 -0
  99. package/server/agent/tests/fetchApi.test.js +99 -0
  100. package/server/agent/tests/firstRun.test.js +44 -0
  101. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  102. package/server/agent/tests/hardwareTiers.test.js +206 -0
  103. package/server/agent/tests/hybridWebSearch.test.js +194 -0
  104. package/server/agent/tests/judgeService.test.js +251 -0
  105. package/server/agent/tests/lanDiscovery.test.js +324 -0
  106. package/server/agent/tests/lanInterfaces.test.js +510 -0
  107. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  108. package/server/agent/tests/locationTool.test.js +278 -0
  109. package/server/agent/tests/mcpClient.test.js +201 -0
  110. package/server/agent/tests/memoryRoute.test.js +173 -0
  111. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  112. package/server/agent/tests/modelCompatibility.test.js +148 -0
  113. package/server/agent/tests/modelManager.test.js +141 -0
  114. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  115. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  116. package/server/agent/tests/newCommands.test.js +347 -0
  117. package/server/agent/tests/newCommandsTui.test.js +141 -0
  118. package/server/agent/tests/newTools.test.js +113 -0
  119. package/server/agent/tests/newsIntelligence.test.js +178 -0
  120. package/server/agent/tests/nextGenArchitecture.test.js +101 -0
  121. package/server/agent/tests/offlineInput.test.js +217 -0
  122. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  123. package/server/agent/tests/openDomainSearch.test.js +121 -0
  124. package/server/agent/tests/packagingGuard.test.js +57 -0
  125. package/server/agent/tests/presentation.test.js +61 -0
  126. package/server/agent/tests/processManager.test.js +144 -0
  127. package/server/agent/tests/providerPersistence.test.js +44 -0
  128. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  129. package/server/agent/tests/queryDetails.test.js +126 -0
  130. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  131. package/server/agent/tests/realWorldValidation.test.js +391 -0
  132. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  133. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  134. package/server/agent/tests/sanitizer.test.js +37 -0
  135. package/server/agent/tests/scenariosData.js +378 -0
  136. package/server/agent/tests/schemaRouting.test.js +54 -0
  137. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  138. package/server/agent/tests/securityAuditRemediation.test.js +221 -0
  139. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  140. package/server/agent/tests/securityHardening.test.js +146 -0
  141. package/server/agent/tests/securitySanitizer.test.js +38 -0
  142. package/server/agent/tests/sessionAndCopy.test.js +267 -0
  143. package/server/agent/tests/sidebarControls.test.js +135 -0
  144. package/server/agent/tests/statefulMemory.test.js +277 -0
  145. package/server/agent/tests/subagent.test.js +216 -0
  146. package/server/agent/tests/subagentProgress.test.js +109 -0
  147. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  148. package/server/agent/tests/suggestions.test.js +199 -0
  149. package/server/agent/tests/suggestionsTui.test.js +251 -0
  150. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  151. package/server/agent/tests/terminalImage.test.js +80 -0
  152. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  153. package/server/agent/tests/terminalText.test.js +133 -0
  154. package/server/agent/tests/textProtocolLoop.test.js +197 -0
  155. package/server/agent/tests/textToolParsing.test.js +187 -0
  156. package/server/agent/tests/theme.test.js +26 -0
  157. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  158. package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
  159. package/server/agent/tests/tools.test.js +259 -0
  160. package/server/agent/tests/unifiedEndpoints.test.js +159 -0
  161. package/server/agent/tests/v110Features.test.js +262 -0
  162. package/server/agent/tests/versionBump.test.js +69 -0
  163. package/server/agent/tests/vramEstimator.test.js +595 -0
  164. package/server/agent/tests/webChatDocs.test.js +199 -0
  165. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  166. package/server/agent/tests/webChatInspection.test.js +551 -0
  167. package/server/agent/tests/webChatMcp.test.js +94 -0
  168. package/server/agent/tests/webChatMemory.test.js +112 -0
  169. package/server/agent/tests/webChatSecurity.test.js +169 -0
  170. package/server/agent/tests/webExtract.test.js +284 -0
  171. package/server/agent/tests/webSources.test.js +281 -0
  172. package/server/agent/textToolProtocol.js +173 -46
  173. package/server/agent/toolRouter.js +318 -44
  174. package/server/agent/tools.js +877 -96
  175. package/server/agent/ui.js +39 -34
  176. package/server/agent/vramService.js +35 -54
  177. package/server/agent/webSearch.js +441 -162
  178. package/server/agent/websearch/orchestrator.js +15 -5
  179. package/server/agent/websearch/resilience/cache.js +9 -2
  180. package/server/agent/websearch/security/ssrf.js +41 -10
  181. package/server/agent/youtubeAuth.js +5 -20
  182. package/server/apiRouter.js +5558 -4422
  183. package/server/eval/judgeRubrics.js +110 -0
  184. package/server/eval/judgeService.js +290 -0
  185. package/server/eval/runBenchmark.js +182 -0
  186. package/server/mcp/data/discography.js +2 -2
  187. package/server/mcp/examples/antigravity.example.json +17 -0
  188. package/server/mcp/examples/librechat.example.yaml +16 -0
  189. package/server/mcp/handler.js +8 -4
  190. package/server/mcp/security.js +220 -21
  191. package/server/mcp/server.js +2 -1
  192. package/server/mcp/tests/connection.test.js +162 -0
  193. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  194. package/server/mcp/tools.js +7 -1
  195. package/server/supabase_migration_prefix.sql +102 -0
  196. package/server/supabase_schema.sql +63 -0
  197. package/server/web-chat/core/connection.js +179 -218
  198. package/server/web-chat/core/conversationContext.js +9 -2
  199. package/server/web-chat/core/directCatalogEngine.js +52 -28
  200. package/server/web-chat/core/dramaKnowledge.js +33 -22
  201. package/server/web-chat/core/followUpEngine.js +735 -81
  202. package/server/web-chat/core/futureEventGuard.js +5 -1
  203. package/server/web-chat/core/musicControlAction.js +70 -0
  204. package/server/web-chat/core/security.js +32 -21
  205. package/server/web-chat/core/toolInspector.js +6 -6
  206. package/server/web-chat/core/urlGuard.js +45 -0
  207. package/server/web-chat/router.js +204 -157
  208. package/server/web-chat/routes/chatRoute.js +885 -488
  209. package/server/web-chat/routes/engineRoute.js +270 -0
  210. package/server/web-chat/routes/exportRoute.js +1 -1
  211. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  212. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  213. package/server/web-chat/routes/mcpRoute.js +386 -0
  214. package/server/web-chat/routes/memoryRoute.js +253 -0
  215. package/server/web-chat/routes/providerRoute.js +49 -16
  216. package/server/web-chat/routes/staticRoute.js +306 -17
  217. package/server/web-chat/routes/statusRoute.js +194 -73
  218. package/server/web-chat/routes/suggestRoute.js +11 -3
  219. package/server/web-chat/routes/verifyRoute.js +26 -4
  220. package/server/web-chat/server.js +7 -6
  221. package/server/web-chat/ui/docsBuilder.js +3558 -0
  222. package/server/web-chat/ui/htmlBuilder.js +12 -0
  223. package/server/web-chat/ui/scripts/client.js +3401 -1302
  224. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  225. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  226. package/server/web-chat/ui/styles/lan-card.css +165 -0
  227. package/server/web-chat/ui/styles/main.css +4289 -447
  228. package/server/web-chat/ui/sw.js +64 -0
  229. package/server/web-chat/ui/templates/body.html +407 -281
  230. package/src/data/awardsData.js +146 -146
  231. package/src/data/musicVideoData.js +6 -4
  232. package/src/lib/vram/estimator.js +195 -19
@@ -0,0 +1,1099 @@
1
+ import fs from 'node:fs/promises';
2
+ import fsSync from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { spawn } from 'node:child_process';
7
+ import readline from 'node:readline';
8
+ import { runSuite } from '../evaluation/runner.js';
9
+ import { cases, expandedCases, securityCases, allCases, legacyCases } from '../evaluation/cases.js';
10
+ import { DEFAULTS, REPO_ROOT } from '../core/config.js';
11
+ import { getEvalUiHtml } from '../ui/evalUi.js';
12
+ import { diffReports } from '../analysis/compare.js';
13
+ import { renderBenchmarkPng } from '../reporting/benchmarkPng.js';
14
+ import { judgeReport } from '../analysis/judge.js';
15
+ import { acquireRunLock, createSaver, loadReportWithBackups } from '../core/store.js';
16
+ import { runSingleCase } from '../evaluation/worker.js';
17
+ import { evaluateGates, loadGatesFile } from '../analysis/gates.js';
18
+
19
+ let isRunning = false;
20
+ let activeAbortController = null;
21
+ let currentReport = { rows: [], metadata: {}, summary: {} };
22
+ const sseClients = new Set();
23
+ let activeRunMeta = null;
24
+ let activeCaseMeta = null;
25
+ let liveWatcherTimer = null;
26
+ let lastLiveMtime = 0;
27
+
28
+ const lmsStreamClients = new Set();
29
+ let lmsProcess = null;
30
+ let lmsRl = null;
31
+ let lmsStopTimer = null;
32
+ const lastKnownLmsEvents = [];
33
+
34
+ function getLmsExecutable() {
35
+ const userHome = os.homedir();
36
+ const isWin = process.platform === 'win32';
37
+ const candidates = [
38
+ path.join(userHome, '.lmstudio', 'bin', isWin ? 'lms.exe' : 'lms'),
39
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'LM Studio', 'resources', 'app', '.webpack', 'main', isWin ? 'lms.exe' : 'lms'),
40
+ isWin ? 'lms.exe' : 'lms'
41
+ ];
42
+ for (const c of candidates) {
43
+ try {
44
+ if (fsSync.existsSync(c)) return c;
45
+ } catch {}
46
+ }
47
+ return isWin ? 'lms.exe' : 'lms';
48
+ }
49
+
50
+ function broadcastLmsStream(event, data) {
51
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
52
+ for (const client of lmsStreamClients) {
53
+ try {
54
+ client.write(payload);
55
+ } catch {
56
+ lmsStreamClients.delete(client);
57
+ }
58
+ }
59
+ }
60
+
61
+ function startLmsStream() {
62
+ if (lmsStopTimer) {
63
+ clearTimeout(lmsStopTimer);
64
+ lmsStopTimer = null;
65
+ }
66
+ if (lmsProcess) return;
67
+ const lmsBin = getLmsExecutable();
68
+ try {
69
+ lmsProcess = spawn(lmsBin, ['log', 'stream', '--json'], {
70
+ windowsHide: true,
71
+ stdio: ['ignore', 'pipe', 'pipe']
72
+ });
73
+
74
+ lmsRl = readline.createInterface({
75
+ input: lmsProcess.stdout,
76
+ terminal: false
77
+ });
78
+
79
+ lmsRl.on('line', (line) => {
80
+ const trimmed = line.trim();
81
+ if (!trimmed) return;
82
+ let parsed = null;
83
+ try {
84
+ parsed = JSON.parse(trimmed);
85
+ } catch {
86
+ parsed = { raw: trimmed, timestamp: Date.now() };
87
+ }
88
+ lastKnownLmsEvents.push(parsed);
89
+ if (lastKnownLmsEvents.length > 100) lastKnownLmsEvents.shift();
90
+ broadcastLmsStream('lms_log', parsed);
91
+ });
92
+
93
+ lmsProcess.stderr?.on('data', (data) => {
94
+ const text = data.toString().trim();
95
+ if (text) {
96
+ broadcastLmsStream('lms_log', { info: text, timestamp: Date.now() });
97
+ }
98
+ });
99
+
100
+ lmsProcess.on('error', (err) => {
101
+ broadcastLmsStream('lms_log', { error: err.message, timestamp: Date.now() });
102
+ stopLmsStream();
103
+ });
104
+
105
+ lmsProcess.on('exit', () => {
106
+ stopLmsStream();
107
+ });
108
+ } catch (err) {
109
+ lmsProcess = null;
110
+ }
111
+ }
112
+
113
+ function stopLmsStream() {
114
+ if (lmsStopTimer) {
115
+ clearTimeout(lmsStopTimer);
116
+ lmsStopTimer = null;
117
+ }
118
+ if (lmsRl) {
119
+ try { lmsRl.close(); } catch {}
120
+ lmsRl = null;
121
+ }
122
+ if (lmsProcess) {
123
+ try {
124
+ if (process.platform === 'win32' && lmsProcess.pid) {
125
+ spawn('taskkill', ['/pid', String(lmsProcess.pid), '/f', '/t'], { windowsHide: true });
126
+ } else {
127
+ lmsProcess.kill('SIGTERM');
128
+ }
129
+ } catch {}
130
+ lmsProcess = null;
131
+ }
132
+ }
133
+
134
+ async function getEffectiveReport() {
135
+ const defaultOut = path.resolve(REPO_ROOT, DEFAULTS.out);
136
+ try {
137
+ const content = await fs.readFile(defaultOut, 'utf8');
138
+ const diskReport = JSON.parse(content);
139
+ if (!Array.isArray(diskReport?.rows)) return currentReport;
140
+ if (!isRunning || !currentReport?.rows?.length || (diskReport.rows?.length || 0) >= (currentReport.rows?.length || 0)) {
141
+ currentReport = diskReport;
142
+ return diskReport;
143
+ }
144
+ } catch {}
145
+ return currentReport;
146
+ }
147
+
148
+ function ensureLiveWatcher() {
149
+ if (liveWatcherTimer) return;
150
+ const defaultOut = path.resolve(REPO_ROOT, DEFAULTS.out);
151
+ liveWatcherTimer = setInterval(async () => {
152
+ if (sseClients.size === 0 || isRunning) return;
153
+ try {
154
+ const st = await fs.stat(defaultOut);
155
+ if (st.mtimeMs !== lastLiveMtime) {
156
+ lastLiveMtime = st.mtimeMs;
157
+ const rep = await getEffectiveReport();
158
+ if (rep && Array.isArray(rep.rows) && rep.rows.length > 0) {
159
+ broadcastSSE('sync_state', {
160
+ running: isRunning,
161
+ activeRun: activeRunMeta,
162
+ activeCase: activeCaseMeta,
163
+ report: rep
164
+ });
165
+ }
166
+ }
167
+ } catch {}
168
+ }, 1000);
169
+ if (liveWatcherTimer && typeof liveWatcherTimer.unref === 'function') {
170
+ liveWatcherTimer.unref();
171
+ }
172
+ }
173
+
174
+ function broadcastSSE(event, data) {
175
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
176
+ for (const client of sseClients) {
177
+ try {
178
+ client.write(payload);
179
+ } catch {
180
+ sseClients.delete(client);
181
+ }
182
+ }
183
+ }
184
+
185
+ async function readRequestBody(req) {
186
+ return new Promise((resolve, reject) => {
187
+ let body = '';
188
+ req.on('data', chunk => {
189
+ body += chunk.toString();
190
+ if (body.length > 10 * 1024 * 1024) {
191
+ req.destroy();
192
+ reject(new Error('Payload too large'));
193
+ }
194
+ });
195
+ req.on('end', () => {
196
+ try {
197
+ resolve(body ? JSON.parse(body) : {});
198
+ } catch {
199
+ resolve({});
200
+ }
201
+ });
202
+ req.on('error', reject);
203
+ });
204
+ }
205
+
206
+ function sendJson(res, statusCode, data) {
207
+ res.writeHead(statusCode, {
208
+ 'Content-Type': 'application/json; charset=utf-8',
209
+ 'Access-Control-Allow-Origin': '*',
210
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
211
+ 'Access-Control-Allow-Headers': 'Content-Type'
212
+ });
213
+ res.end(JSON.stringify(data));
214
+ }
215
+
216
+ async function loadReportByIdentifier(ident) {
217
+ if (ident === 'current' || ident === 'live') {
218
+ return getEffectiveReport();
219
+ }
220
+ if (typeof ident !== 'string' || !ident.trim() || ident.includes('..') || path.isAbsolute(ident)) {
221
+ return null;
222
+ }
223
+ if (ident.startsWith('history/')) {
224
+ const historyDir = path.dirname(path.resolve(REPO_ROOT, DEFAULTS.out));
225
+ const archivePath = path.join(historyDir, 'history', `${ident.slice('history/'.length)}.json`);
226
+ try {
227
+ return JSON.parse(await fs.readFile(archivePath, 'utf8'));
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+ const snapshotPath = path.resolve(REPO_ROOT, DEFAULTS.snapshotsDir, `${ident}.json`);
233
+ try {
234
+ return JSON.parse(await fs.readFile(snapshotPath, 'utf8'));
235
+ } catch {
236
+ const rawPath = path.resolve(REPO_ROOT, ident.endsWith('.json') ? ident : `${ident}.json`);
237
+ try {
238
+ return JSON.parse(await fs.readFile(rawPath, 'utf8'));
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+ }
244
+
245
+ function generateCsv(report) {
246
+ const rows = Array.isArray(report?.rows) ? report.rows : [];
247
+ const header = ['id', 'category', 'language', 'status', 'allMatched', 'matchedFacts', 'totalFacts', 'toolCorrect', 'loopsDetected', 'durationMs', 'llmMs', 'toolMs', 'decodeTps', 'question'];
248
+ const lines = [header.join(',')];
249
+ for (const r of rows) {
250
+ const matched = r.coverage?.matched ?? '';
251
+ const total = r.coverage?.total ?? '';
252
+ const allMatched = r.coverage?.allMatched ?? '';
253
+ const toolCorrect = r.toolCorrectness?.isCorrect ?? '';
254
+ const loops = r.trajectoryHealth?.hasLoop ?? '';
255
+ const dur = r.durationMs ?? '';
256
+ const llmMs = r.latencyBreakdown?.llmMs ?? '';
257
+ const toolMs = r.latencyBreakdown?.toolMs ?? '';
258
+ const tps = r.latencyBreakdown?.decodeTps ?? '';
259
+ const qEsc = `"${(r.question || '').replace(/"/g, '""')}"`;
260
+ lines.push([
261
+ r.id || '',
262
+ r.category || '',
263
+ r.language || '',
264
+ r.status || '',
265
+ allMatched,
266
+ matched,
267
+ total,
268
+ toolCorrect,
269
+ loops,
270
+ dur,
271
+ llmMs,
272
+ toolMs,
273
+ tps,
274
+ qEsc
275
+ ].join(','));
276
+ }
277
+ return lines.join('\r\n');
278
+ }
279
+
280
+ function isErrorRow(r) {
281
+ if (!r) return false;
282
+ return r.status === 'error' || Boolean(r.error) || Boolean(r.timedOut);
283
+ }
284
+
285
+ function isAnomalyRow(r) {
286
+ if (!r) return false;
287
+ if (r.status === 'error' || r.error) return true;
288
+ if (r.timedOut) return true;
289
+ if (r.toolCorrectness && r.toolCorrectness.isCorrect === false) return true;
290
+ if (r.trajectoryHealth?.hasLoop) return true;
291
+ if (r.groundedness && r.groundedness.isGrounded === false) return true;
292
+ if (r.coverage && r.coverage.allMatched === false) return true;
293
+ if (r.passAt1 === false || (typeof r.passAt1 === 'number' && r.passAt1 === 0)) return true;
294
+ return false;
295
+ }
296
+
297
+ function getAnomalyReason(r) {
298
+ if (!r) return 'Unknown anomaly';
299
+ if (r.status === 'error' || r.error) return `Error: ${r.error || 'Execution failed'}`;
300
+ if (r.timedOut) return 'Timeout exceeded';
301
+ if (r.trajectoryHealth?.hasLoop) return 'Infinite Re-planning Loop';
302
+ if (r.toolCorrectness && r.toolCorrectness.isCorrect === false) {
303
+ const missing = r.toolCorrectness.missing?.length ? `missing [${r.toolCorrectness.missing.join(', ')}]` : '';
304
+ const unexpected = r.toolCorrectness.unexpected?.length ? `unexpected [${r.toolCorrectness.unexpected.join(', ')}]` : '';
305
+ return `Tool Mismatch: ${[missing, unexpected].filter(Boolean).join('; ')}`;
306
+ }
307
+ if (r.coverage && r.coverage.allMatched === false) {
308
+ return `Coverage Incomplete: missing [${(r.coverage.missing || []).join(', ')}]`;
309
+ }
310
+ if (r.groundedness && r.groundedness.isGrounded === false) return 'Hallucination / Ungrounded Claim';
311
+ return 'Behavioral Anomaly (Pass@1: 0)';
312
+ }
313
+
314
+ function generateDeterministicSummary({ report, totalCases, passCount, passRate, anomalies, categoryStats, avgDurationMs }) {
315
+ const modelName = report.metadata?.model || DEFAULTS.model;
316
+ const isHealthy = passRate >= 75.0 && anomalies.length === 0;
317
+ const verdictBadge = isHealthy ? 'PRODUCTION READY (PASSED)' : (passRate >= 65.0 ? 'MARGINAL / NEEDS OPTIMIZATION' : 'CRITICAL REGRESSIONS DETECTED');
318
+ const errorCount = (report.rows || []).filter(r => r.status === 'error' || r.error).length;
319
+
320
+ let md = `# Kiku Agent Evaluation — Executive Benchmark Report\n\n`;
321
+ md += `**Model Evaluated:** \`${modelName}\` | **Generated:** ${new Date().toLocaleString()} | **Verdict:** **${verdictBadge}**\n\n`;
322
+
323
+ md += `## 1. Executive Verdict & Health Overview\n`;
324
+ md += `- **Overall Task Success (Pass@1):** **${passRate}%** (${passCount} / ${totalCases} cases passed)\n`;
325
+ md += `- **Benchmark Standard Target:** >= 75.0% Pass@1\n`;
326
+ md += `- **Execution Errors:** **${errorCount}** (${errorCount === 0 ? 'Zero crashes or timeouts' : `${errorCount} cases`})\n`;
327
+ md += `- **Detected Anomalies / Behavioral Deviations:** **${anomalies.length}** cases (${((anomalies.length / Math.max(1, totalCases)) * 100).toFixed(1)}% of test suite)\n`;
328
+ md += `- **Average Latency:** **${(avgDurationMs / 1000).toFixed(1)}s** (${avgDurationMs.toLocaleString()}ms) per test case\n`;
329
+ md += `- **Evaluation Status:** ${isHealthy ? 'The model meets or exceeds all standard quality and safety gates.' : 'The model has identified regressions or anomalies that require targeted remediation before deployment.'}\n\n`;
330
+
331
+ md += `## 2. Category Breakdown Matrix\n\n`;
332
+ md += `| Category | Total Cases | Passed | Pass Rate | Errors | Tool Failures | Health Status |\n`;
333
+ md += `| :--- | :---: | :---: | :---: | :---: | :---: | :---: |\n`;
334
+ for (const [cat, stats] of Object.entries(categoryStats)) {
335
+ const rate = stats.total > 0 ? Number(((stats.passed / stats.total) * 100).toFixed(1)) : 0;
336
+ const statusIcon = rate >= 85 ? 'Excellent' : (rate >= 70 ? 'Moderate' : 'Needs Attention');
337
+ md += `| **${cat}** | ${stats.total} | ${stats.passed} | ${rate}% | ${stats.errors} | ${stats.toolFails} | ${statusIcon} |\n`;
338
+ }
339
+ md += `\n`;
340
+
341
+ md += `## 3. Anomaly & Root Cause Diagnosis\n`;
342
+ if (anomalies.length === 0) {
343
+ md += `Zero anomalies detected. All ${totalCases} evaluation cases executed cleanly with full coverage and zero loop violations.\n\n`;
344
+ } else {
345
+ md += `A total of **${anomalies.length}** anomalies were flagged across the benchmark run:\n\n`;
346
+ for (const a of anomalies.slice(0, 10)) {
347
+ md += `- **[${a.id}]** \`${a.category}\`: *"${a.question || 'N/A'}"*\n`;
348
+ md += ` ↳ **Root Cause:** ${getAnomalyReason(a)} *(Latency: ${a.durationMs || 0}ms)*\n`;
349
+ }
350
+ if (anomalies.length > 10) {
351
+ md += `- *...and ${anomalies.length - 10} more anomalous cases (use the Anomalies filter in the workbench to inspect all).*\n`;
352
+ }
353
+ md += `\n`;
354
+ }
355
+
356
+ md += `## 4. Operational & Latency Insights\n`;
357
+ md += `- **Speed Benchmark:** Average query execution time was **${(avgDurationMs / 1000).toFixed(2)}s**.\n`;
358
+ const slowCases = [...report.rows].sort((a, b) => (b.durationMs || 0) - (a.durationMs || 0)).slice(0, 3);
359
+ if (slowCases.length > 0 && slowCases[0].durationMs > 15000) {
360
+ md += `- **High Latency Outliers:**\n`;
361
+ for (const s of slowCases) {
362
+ md += ` * [${s.id}] \`${s.category}\`: ${(s.durationMs / 1000).toFixed(1)}s — *"${(s.question || '').slice(0, 60)}..."*\n`;
363
+ }
364
+ }
365
+ md += `\n`;
366
+
367
+ md += `## 5. Actionable Engineering Recommendations\n`;
368
+ if (errorCount > 0) {
369
+ md += `1. **Click "Retry"**: Re-run ${errorCount} status error cases to verify transient network or timeout issues.\n`;
370
+ } else {
371
+ md += `1. **Zero Status Errors Remediated**: All 500 evaluation cases completed cleanly without crashes or timeouts.\n`;
372
+ }
373
+ md += `2. **Tool Routing Refinements**: For categories with tool mismatches, adjust prompt routing definitions in \`server/agent/tools.js\`.\n`;
374
+ md += `3. **Coverage Enhancements**: Ensure catalog entity aliases in \`src/data/*.js\` and \`server/mcp/data/*.js\` match the gold queries.\n`;
375
+ md += `4. **Loop Protection**: Verify sliding window and ReAct trajectory cutoff limits in \`server/agent/agentCore.js\`.\n`;
376
+
377
+ return md;
378
+ }
379
+
380
+ export async function handleEvalApiRequest(req, res) {
381
+ const urlObj = new URL(req.url, 'http://localhost:3001');
382
+ const pathname = urlObj.pathname;
383
+
384
+ if (req.method === 'OPTIONS') {
385
+ res.writeHead(204, {
386
+ 'Access-Control-Allow-Origin': '*',
387
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
388
+ 'Access-Control-Allow-Headers': 'Content-Type'
389
+ });
390
+ res.end();
391
+ return true;
392
+ }
393
+
394
+ if (pathname.startsWith('/fonts/') || pathname === '/favicon.ico') {
395
+ const publicPath = path.resolve(REPO_ROOT, 'public', pathname.replace(/^\/+/, ''));
396
+ try {
397
+ const data = await fs.readFile(publicPath);
398
+ const ext = path.extname(publicPath).toLowerCase();
399
+ const mimeMap = {
400
+ '.css': 'text/css; charset=utf-8',
401
+ '.woff2': 'font/woff2',
402
+ '.woff': 'font/woff',
403
+ '.ttf': 'font/ttf',
404
+ '.ico': 'image/x-icon',
405
+ '.png': 'image/png',
406
+ '.svg': 'image/svg+xml'
407
+ };
408
+ res.writeHead(200, {
409
+ 'Content-Type': mimeMap[ext] || 'application/octet-stream',
410
+ 'Cache-Control': 'public, max-age=86400',
411
+ 'Access-Control-Allow-Origin': '*'
412
+ });
413
+ res.end(data);
414
+ return true;
415
+ } catch {}
416
+ }
417
+
418
+ if (pathname === '/' || pathname === '/eval' || pathname === '/eval/' || pathname === '/index.html') {
419
+ res.writeHead(200, {
420
+ 'Content-Type': 'text/html; charset=utf-8',
421
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
422
+ 'Pragma': 'no-cache',
423
+ 'Expires': '0'
424
+ });
425
+ const gates = await loadGatesFile(DEFAULTS.gatesFile);
426
+ res.end(getEvalUiHtml(gates));
427
+ return true;
428
+ }
429
+
430
+ if (pathname === '/dashboard' || pathname === '/eval/dashboard' || pathname === '/dashboard.html') {
431
+ try {
432
+ const rep = await getEffectiveReport();
433
+ const { generateDashboardHtml } = await import('../reporting/dashboard.js');
434
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
435
+ res.end(generateDashboardHtml(rep));
436
+ return true;
437
+ } catch (e) {
438
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
439
+ res.end(String(e));
440
+ return true;
441
+ }
442
+ }
443
+
444
+ if (pathname === '/api/eval/events') {
445
+ res.writeHead(200, {
446
+ 'Content-Type': 'text/event-stream',
447
+ 'Cache-Control': 'no-cache, no-transform',
448
+ 'Connection': 'keep-alive',
449
+ 'Access-Control-Allow-Origin': '*'
450
+ });
451
+ res.write(': connected\n\n');
452
+ sseClients.add(res);
453
+ ensureLiveWatcher();
454
+ getEffectiveReport().then(rep => {
455
+ const syncData = {
456
+ running: isRunning,
457
+ activeRun: activeRunMeta,
458
+ activeCase: activeCaseMeta,
459
+ report: rep
460
+ };
461
+ try {
462
+ res.write(`event: sync_state\ndata: ${JSON.stringify(syncData)}\n\n`);
463
+ } catch {}
464
+ });
465
+ req.on('close', () => {
466
+ sseClients.delete(res);
467
+ });
468
+ return true;
469
+ }
470
+
471
+ if (pathname === '/api/eval/lms/stream') {
472
+ res.writeHead(200, {
473
+ 'Content-Type': 'text/event-stream',
474
+ 'Cache-Control': 'no-cache, no-transform',
475
+ 'Connection': 'keep-alive',
476
+ 'Access-Control-Allow-Origin': '*'
477
+ });
478
+ res.write(': connected\n\n');
479
+ lmsStreamClients.add(res);
480
+ startLmsStream();
481
+ if (lastKnownLmsEvents.length > 0) {
482
+ for (const ev of lastKnownLmsEvents) {
483
+ try {
484
+ res.write(`event: lms_log\ndata: ${JSON.stringify(ev)}\n\n`);
485
+ } catch {}
486
+ }
487
+ } else {
488
+ res.write(`event: lms_log\ndata: ${JSON.stringify({ info: 'Connected to LM Studio event stream (google/gemma-4-e4b)', timestamp: Date.now() })}\n\n`);
489
+ }
490
+ req.on('close', () => {
491
+ lmsStreamClients.delete(res);
492
+ if (lmsStreamClients.size === 0) {
493
+ if (lmsStopTimer) clearTimeout(lmsStopTimer);
494
+ lmsStopTimer = setTimeout(() => {
495
+ if (lmsStreamClients.size === 0) {
496
+ stopLmsStream();
497
+ }
498
+ }, 60000);
499
+ }
500
+ });
501
+ return true;
502
+ }
503
+
504
+ if (pathname === '/api/eval/lms/status' && req.method === 'GET') {
505
+ sendJson(res, 200, {
506
+ active: Boolean(lmsProcess),
507
+ clientCount: lmsStreamClients.size
508
+ });
509
+ return true;
510
+ }
511
+
512
+ if (pathname === '/api/eval/status' && req.method === 'GET') {
513
+ const rep = await getEffectiveReport();
514
+ sendJson(res, 200, {
515
+ running: isRunning,
516
+ hasReport: Array.isArray(rep?.rows) && rep.rows.length > 0,
517
+ currentReport: rep,
518
+ activeRun: activeRunMeta,
519
+ activeCase: activeCaseMeta
520
+ });
521
+ return true;
522
+ }
523
+
524
+ if (pathname === '/api/eval/suites' && req.method === 'GET') {
525
+ const full = [...new Set([...allCases, ...(expandedCases || [])])];
526
+ const catalogCount = cases.length;
527
+ const expandedCount = expandedCases ? expandedCases.length : 0;
528
+ const securityCount = securityCases.length;
529
+ const legacyCount = legacyCases.length;
530
+ const totalCount = full.length;
531
+ sendJson(res, 200, {
532
+ single: { id: 'single', name: 'Single Case', count: 1 },
533
+ quick: { id: 'quick', name: 'Quick Smoke', count: Math.min(3, totalCount) },
534
+ catalog: { id: 'catalog', name: 'Catalog QA', count: catalogCount },
535
+ expanded: { id: 'expanded', name: 'New Cases', count: expandedCount },
536
+ security: { id: 'security', name: 'Security Resilience', count: securityCount },
537
+ legacy: { id: 'legacy', name: 'Legacy Case IDs', count: legacyCount },
538
+ all: { id: 'all', name: 'All Comprehensive', count: totalCount },
539
+ total: totalCount
540
+ });
541
+ return true;
542
+ }
543
+
544
+ if (pathname === '/api/eval/cases' && req.method === 'GET') {
545
+ const full = [...new Set([...allCases, ...(expandedCases || [])])];
546
+ const list = full.map(c => ({
547
+ id: c.id,
548
+ category: c.category,
549
+ dimension: c.dimension || null,
550
+ difficulty: c.difficulty || null,
551
+ language: c.language,
552
+ question: c.question,
553
+ expectedTools: c.expectedTools || []
554
+ }));
555
+ sendJson(res, 200, list);
556
+ return true;
557
+ }
558
+
559
+ if (pathname === '/api/eval/report' && req.method === 'GET') {
560
+ const rep = await getEffectiveReport();
561
+ sendJson(res, 200, rep);
562
+ return true;
563
+ }
564
+
565
+ if (pathname === '/api/eval/start' && req.method === 'POST') {
566
+ if (isRunning) {
567
+ sendJson(res, 409, { error: 'Evaluation is already in progress' });
568
+ return true;
569
+ }
570
+
571
+ const payload = await readRequestBody(req);
572
+
573
+ if (payload.retryErrors || payload.retryAnomalies || payload.retryFailedOnly) {
574
+ const diskReport = await loadReportWithBackups(path.resolve(REPO_ROOT, DEFAULTS.out));
575
+ if (diskReport) currentReport = diskReport;
576
+ const isErrorOnly = payload.retryErrors !== false;
577
+ const targetCases = (currentReport?.rows || []).filter(r => {
578
+ if (isErrorOnly) return isErrorRow(r);
579
+ return isAnomalyRow(r);
580
+ });
581
+ if (!targetCases.length) {
582
+ sendJson(res, 400, { error: isErrorOnly ? 'No status error cases found in the current report to retry.' : 'No error or anomalous cases found in the current report to retry.' });
583
+ return true;
584
+ }
585
+ payload.ids = targetCases.map(c => c.id).join(',');
586
+ payload.limit = targetCases.length;
587
+ payload.suite = 'all';
588
+ }
589
+
590
+ const isTargetedRun = Boolean(payload.ids || payload.retryErrors || payload.retryAnomalies || payload.retryFailedOnly);
591
+
592
+ isRunning = true;
593
+ activeAbortController = new AbortController();
594
+ activeRunMeta = {
595
+ suite: payload.suite || DEFAULTS.suite,
596
+ model: payload.model || DEFAULTS.model,
597
+ baseUrl: payload.baseUrl || DEFAULTS.baseUrl,
598
+ limit: payload.limit,
599
+ startedAt: Date.now(),
600
+ planned: 0,
601
+ total: 0,
602
+ initialRows: currentReport?.rows?.length || 0
603
+ };
604
+ activeCaseMeta = null;
605
+
606
+ if (!payload.resume && !isTargetedRun) {
607
+ currentReport = { metadata: {}, rows: [], summary: {} };
608
+ } else if (!Array.isArray(currentReport?.rows) || currentReport.rows.length === 0) {
609
+ const diskReport = await loadReportWithBackups(path.resolve(REPO_ROOT, DEFAULTS.out));
610
+ currentReport = diskReport || { metadata: {}, rows: [], summary: {} };
611
+ }
612
+
613
+ sendJson(res, 200, { started: true });
614
+
615
+ (async () => {
616
+ try {
617
+ const report = await runSuite({
618
+ suite: payload.suite || DEFAULTS.suite,
619
+ ids: payload.ids,
620
+ limit: payload.limit,
621
+ model: payload.model || DEFAULTS.model,
622
+ baseUrl: payload.baseUrl || DEFAULTS.baseUrl,
623
+ judgeModel: payload.judgeModel,
624
+ resume: Boolean(payload.resume),
625
+ retryErrors: Boolean(payload.retryErrors),
626
+ retryFailed: Boolean(payload.retryFailedOnly || payload.retryAnomalies),
627
+ preservePrior: isTargetedRun,
628
+ signal: activeAbortController.signal,
629
+ onStart: data => {
630
+ if (activeRunMeta) {
631
+ activeRunMeta.planned = data.planned || 0;
632
+ activeRunMeta.total = data.total || data.planned || 0;
633
+ activeRunMeta.initialRows = currentReport?.rows?.length || 0;
634
+ }
635
+ broadcastSSE('run_start', {
636
+ ...data,
637
+ overallTotal: activeRunMeta?.total || data.total,
638
+ initialRows: activeRunMeta?.initialRows || 0
639
+ });
640
+ },
641
+ onCaseStart: (c, meta) => {
642
+ const initial = activeRunMeta?.initialRows || 0;
643
+ const overallTotal = activeRunMeta?.total || meta.total;
644
+ const overallIndex = Math.min(overallTotal, initial + meta.current);
645
+ activeCaseMeta = {
646
+ caseId: c.id,
647
+ category: c.category,
648
+ question: c.question,
649
+ index: meta.current,
650
+ total: meta.total,
651
+ overallIndex,
652
+ overallTotal,
653
+ tool: null,
654
+ startedAt: Date.now()
655
+ };
656
+ broadcastSSE('case_start', activeCaseMeta);
657
+ },
658
+ onStep: stepData => {
659
+ if (activeCaseMeta && (stepData.event === 'tool_start' || stepData.event === 'tool_result')) {
660
+ activeCaseMeta.tool = stepData.name;
661
+ }
662
+ broadcastSSE('case_step', stepData);
663
+ },
664
+ onCaseComplete: (c, result, meta) => {
665
+ activeCaseMeta = null;
666
+ if (!Array.isArray(currentReport.rows)) currentReport.rows = [];
667
+ const idx = currentReport.rows.findIndex(r => r.id === c.id);
668
+ if (idx >= 0) {
669
+ currentReport.rows[idx] = result;
670
+ } else {
671
+ currentReport.rows.push(result);
672
+ }
673
+ if (meta.report?.summary) {
674
+ currentReport.summary = meta.report.summary;
675
+ }
676
+ const overallTotal = activeRunMeta?.total || meta.report?.metadata?.caseIds?.length || currentReport.rows.length;
677
+ const overallCurrent = Math.min(overallTotal, currentReport.rows.length);
678
+ broadcastSSE('case_complete', {
679
+ caseId: c.id,
680
+ result,
681
+ current: meta.current,
682
+ total: meta.total,
683
+ overallCurrent,
684
+ overallTotal,
685
+ summary: currentReport.summary
686
+ });
687
+ }
688
+ });
689
+
690
+ currentReport = report;
691
+ isRunning = false;
692
+ activeRunMeta = null;
693
+ activeCaseMeta = null;
694
+ const gates = await loadGatesFile(DEFAULTS.gatesFile);
695
+ const gatesPassed = evaluateGates(currentReport.summary || {}, gates).passed;
696
+
697
+ broadcastSSE('run_complete', {
698
+ report: currentReport,
699
+ gatesPassed
700
+ });
701
+ } catch (error) {
702
+ isRunning = false;
703
+ activeRunMeta = null;
704
+ activeCaseMeta = null;
705
+ currentReport = await getEffectiveReport();
706
+ broadcastSSE('run_complete', {
707
+ report: currentReport,
708
+ error: error.message,
709
+ gatesPassed: false
710
+ });
711
+ } finally {
712
+ isRunning = false;
713
+ activeAbortController = null;
714
+ activeRunMeta = null;
715
+ activeCaseMeta = null;
716
+ }
717
+ })();
718
+
719
+ return true;
720
+ }
721
+
722
+ if (pathname === '/api/eval/stop' && req.method === 'POST') {
723
+ if (activeAbortController) {
724
+ activeAbortController.abort();
725
+ }
726
+ isRunning = false;
727
+ activeRunMeta = null;
728
+ activeCaseMeta = null;
729
+ sendJson(res, 200, { stopped: true });
730
+ return true;
731
+ }
732
+
733
+ if (pathname === '/api/eval/history' && req.method === 'GET') {
734
+ const snapshotsDir = path.resolve(REPO_ROOT, DEFAULTS.snapshotsDir);
735
+ const defaultOut = path.resolve(REPO_ROOT, DEFAULTS.out);
736
+ const historyDir = path.join(path.dirname(defaultOut), 'history');
737
+ const set = new Set();
738
+ try {
739
+ await fs.access(defaultOut);
740
+ set.add('live');
741
+ } catch {}
742
+ if (currentReport?.rows?.length > 0) set.add('live');
743
+ try {
744
+ const files = await fs.readdir(snapshotsDir);
745
+ for (const f of files) {
746
+ if (f.endsWith('.json')) set.add(f.replace(/\.json$/, ''));
747
+ }
748
+ } catch {}
749
+ try {
750
+ const files = await fs.readdir(historyDir);
751
+ for (const f of files) {
752
+ if (f.startsWith('run_') && f.endsWith('.json')) set.add(`history/${f.replace(/\.json$/, '')}`);
753
+ }
754
+ } catch {}
755
+ sendJson(res, 200, Array.from(set).sort());
756
+ return true;
757
+ }
758
+
759
+ if (pathname === '/api/eval/snapshot' && req.method === 'POST') {
760
+ const rep = await getEffectiveReport();
761
+ const payload = await readRequestBody(req);
762
+ const name = String(payload.name || `snapshot_${Date.now()}`).trim().replace(/[^a-zA-Z0-9_\-]/g, '_');
763
+ const snapshotDir = path.resolve(REPO_ROOT, DEFAULTS.snapshotsDir);
764
+ await fs.mkdir(snapshotDir, { recursive: true });
765
+ const snapshotPath = path.join(snapshotDir, `${name}.json`);
766
+ await fs.writeFile(snapshotPath, JSON.stringify(rep, null, 2), 'utf8');
767
+ sendJson(res, 200, { saved: true, name, path: snapshotPath });
768
+ return true;
769
+ }
770
+
771
+ if (pathname === '/api/eval/compare' && req.method === 'GET') {
772
+ const baseId = urlObj.searchParams.get('baseline') || 'live';
773
+ const candId = urlObj.searchParams.get('candidate') || 'live';
774
+ const baseReport = await loadReportByIdentifier(baseId);
775
+ const candReport = await loadReportByIdentifier(candId);
776
+ if (!baseReport || !candReport) {
777
+ sendJson(res, 404, { error: 'One or both snapshot reports could not be found.' });
778
+ return true;
779
+ }
780
+ const diff = diffReports(baseReport, candReport);
781
+ sendJson(res, 200, diff);
782
+ return true;
783
+ }
784
+
785
+ if (pathname === '/api/eval/playground' && req.method === 'POST') {
786
+ const payload = await readRequestBody(req);
787
+ const caseId = payload.caseId || 'p01';
788
+ const customQuestion = payload.customQuestion || payload.question;
789
+ const model = payload.model || DEFAULTS.model;
790
+ const baseUrl = payload.baseUrl || DEFAULTS.baseUrl;
791
+ const timeoutMs = Number(payload.timeoutMs) || 60000;
792
+ try {
793
+ const result = await runSingleCase(caseId, { model, baseUrl, timeoutMs, customQuestion });
794
+ sendJson(res, 200, { ok: true, caseId, result });
795
+ } catch (err) {
796
+ sendJson(res, 500, { ok: false, error: err.message });
797
+ }
798
+ return true;
799
+ }
800
+
801
+ if (pathname === '/api/eval/export/png' && req.method === 'GET') {
802
+ const rep = await getEffectiveReport();
803
+ try {
804
+ const pngBuffer = renderBenchmarkPng(rep.rows || [], rep.summary || {}, { width: 1440 });
805
+ const filename = `${randomUUID()}.png`;
806
+ res.writeHead(200, {
807
+ 'Content-Type': 'image/png',
808
+ 'Content-Disposition': `attachment; filename="${filename}"`,
809
+ 'Content-Length': pngBuffer.length,
810
+ 'Access-Control-Allow-Origin': '*'
811
+ });
812
+ res.end(pngBuffer);
813
+ } catch (err) {
814
+ sendJson(res, 500, { error: err.message });
815
+ }
816
+ return true;
817
+ }
818
+
819
+ if (pathname === '/api/eval/export/csv' && req.method === 'GET') {
820
+ const rep = await getEffectiveReport();
821
+ const csvContent = generateCsv(rep);
822
+ const buf = Buffer.from(csvContent, 'utf8');
823
+ const filename = `${randomUUID()}.csv`;
824
+ res.writeHead(200, {
825
+ 'Content-Type': 'text/csv; charset=utf-8',
826
+ 'Content-Disposition': `attachment; filename="${filename}"`,
827
+ 'Content-Length': buf.length,
828
+ 'Access-Control-Allow-Origin': '*'
829
+ });
830
+ res.end(buf);
831
+ return true;
832
+ }
833
+
834
+ if (pathname === '/api/eval/export/json' && req.method === 'GET') {
835
+ const rep = await getEffectiveReport();
836
+ const jsonStr = JSON.stringify(rep, null, 2);
837
+ const buf = Buffer.from(jsonStr, 'utf8');
838
+ const filename = `${randomUUID()}.json`;
839
+ res.writeHead(200, {
840
+ 'Content-Type': 'application/json; charset=utf-8',
841
+ 'Content-Disposition': `attachment; filename="${filename}"`,
842
+ 'Content-Length': buf.length,
843
+ 'Access-Control-Allow-Origin': '*'
844
+ });
845
+ res.end(buf);
846
+ return true;
847
+ }
848
+
849
+ if (pathname === '/api/eval/custom-suites' && req.method === 'GET') {
850
+ const suitePath = path.resolve(REPO_ROOT, 'artifacts/eval/custom-suites.json');
851
+ try {
852
+ const data = JSON.parse(await fs.readFile(suitePath, 'utf8'));
853
+ sendJson(res, 200, data);
854
+ } catch {
855
+ const defaultSuites = [
856
+ { id: 'suite_thai_catalog', name: 'Thai Catalog Focus', description: 'Core questions evaluating Thai language responses across music & drama', caseIds: ['p01', 'w01', 'w02', 'm01', 'm02', 'b01', 'a01'] },
857
+ { id: 'suite_security_hard', name: 'High-Impact Security Suite', description: 'Strict guardrail probes against injection, jailbreaks, and sensitive data leakage', caseIds: ['s01', 's02', 's03', 's04', 's05', 's10', 's15'] },
858
+ { id: 'suite_discography_heavy', name: 'Full Discography & Audio', description: 'Music release dates, lyricists, team songs, and OST catalog fidelity', caseIds: ['m01', 'm02', 'm03', 'm04', 'm05', 'm06', 'm07', 'm08'] }
859
+ ];
860
+ sendJson(res, 200, defaultSuites);
861
+ }
862
+ return true;
863
+ }
864
+
865
+ if (pathname === '/api/eval/custom-suites' && req.method === 'POST') {
866
+ const payload = await readRequestBody(req);
867
+ const suitePath = path.resolve(REPO_ROOT, 'artifacts/eval/custom-suites.json');
868
+ await fs.mkdir(path.dirname(suitePath), { recursive: true });
869
+ let existing = [];
870
+ try {
871
+ existing = JSON.parse(await fs.readFile(suitePath, 'utf8'));
872
+ } catch {}
873
+ if (!Array.isArray(existing)) existing = [];
874
+ const newSuite = {
875
+ id: payload.id || `suite_${Date.now()}`,
876
+ name: payload.name || 'Custom Suite',
877
+ description: payload.description || '',
878
+ caseIds: Array.isArray(payload.caseIds) ? payload.caseIds : []
879
+ };
880
+ const idx = existing.findIndex(s => s.id === newSuite.id);
881
+ if (idx >= 0) existing[idx] = newSuite;
882
+ else existing.push(newSuite);
883
+ await fs.writeFile(suitePath, JSON.stringify(existing, null, 2), 'utf8');
884
+ sendJson(res, 200, { ok: true, suite: newSuite, suites: existing });
885
+ return true;
886
+ }
887
+
888
+ if (pathname === '/api/eval/matrix' && req.method === 'GET') {
889
+ const snapshotsDir = path.resolve(REPO_ROOT, DEFAULTS.snapshotsDir);
890
+ const entries = [];
891
+ if (currentReport?.rows?.length > 0) {
892
+ entries.push({
893
+ id: 'live',
894
+ name: 'Live Active Run',
895
+ model: currentReport.metadata?.model || 'Live Model',
896
+ summary: currentReport.summary || {},
897
+ caseCount: currentReport.rows.length,
898
+ timestamp: currentReport.metadata?.finishedAt || currentReport.metadata?.startedAt || new Date().toISOString()
899
+ });
900
+ }
901
+ try {
902
+ const files = await fs.readdir(snapshotsDir);
903
+ for (const file of files) {
904
+ if (!file.endsWith('.json')) continue;
905
+ const id = file.replace(/\.json$/, '');
906
+ if (id === 'live') continue;
907
+ try {
908
+ const content = JSON.parse(await fs.readFile(path.join(snapshotsDir, file), 'utf8'));
909
+ entries.push({
910
+ id,
911
+ name: id,
912
+ model: content.metadata?.model || id,
913
+ summary: content.summary || {},
914
+ caseCount: Array.isArray(content.rows) ? content.rows.length : 0,
915
+ timestamp: content.metadata?.finishedAt || content.metadata?.startedAt || ''
916
+ });
917
+ } catch {}
918
+ }
919
+ } catch {}
920
+ sendJson(res, 200, entries);
921
+ return true;
922
+ }
923
+
924
+ if (pathname === '/api/eval/judge-rubric' && req.method === 'GET') {
925
+ sendJson(res, 200, {
926
+ scale: '0 to 4 integers (0=completely wrong, 1=major errors, 2=material omission, 3=mostly correct with minor issue, 4=fully correct)',
927
+ criteria: [
928
+ { name: 'Correctness', description: 'Accuracy against frozen ground truth reference and catalog evidence.' },
929
+ { name: 'Grounding', description: 'Every concrete factual claim must be directly supported by observed tool outputs.' },
930
+ { name: 'Instruction Following', description: 'Adheres to requested language (Thai, English, Chinese) and formatting constraints.' },
931
+ { name: 'Unsupported Claims', description: 'Identifies any hallucinated or extrapolated claims not verified in the catalog.' }
932
+ ]
933
+ });
934
+ return true;
935
+ }
936
+
937
+ if (pathname === '/api/eval/judge' && req.method === 'POST') {
938
+ if (isRunning) {
939
+ sendJson(res, 409, { error: 'An evaluation run is in progress. Wait for it to finish before judging.' });
940
+ return true;
941
+ }
942
+ const payload = await readRequestBody(req);
943
+ const judgeModel = payload.judgeModel || DEFAULTS.model;
944
+ const baseUrl = payload.baseUrl || DEFAULTS.baseUrl;
945
+ const timeoutMs = Number(payload.timeoutMs) || 60000;
946
+ if (!currentReport || !Array.isArray(currentReport.rows) || currentReport.rows.length === 0) {
947
+ sendJson(res, 400, { error: 'No completed report to judge. Run an evaluation first.' });
948
+ return true;
949
+ }
950
+ const defaultOut = path.resolve(REPO_ROOT, DEFAULTS.out);
951
+ const save = createSaver(defaultOut, currentReport);
952
+ let lock = null;
953
+ try {
954
+ lock = await acquireRunLock(defaultOut);
955
+ await judgeReport(currentReport, { judgeModel, baseUrl, timeoutMs }, save);
956
+ broadcastSSE('report_judged', { report: currentReport });
957
+ sendJson(res, 200, { ok: true, report: currentReport });
958
+ } catch (err) {
959
+ sendJson(res, err.status === 409 ? 409 : 500, { error: err.message });
960
+ } finally {
961
+ if (lock) await lock.release();
962
+ }
963
+ return true;
964
+ }
965
+
966
+ if (pathname === '/api/eval/ai-summary' && (req.method === 'POST' || req.method === 'GET')) {
967
+ let payload = {};
968
+ if (req.method === 'POST') {
969
+ try { payload = await readRequestBody(req); } catch {}
970
+ }
971
+
972
+ let report = payload.report || currentReport;
973
+ if (!report?.rows?.length) {
974
+ try {
975
+ const livePath = path.resolve(REPO_ROOT, DEFAULTS.out);
976
+ const content = await fs.readFile(livePath, 'utf8');
977
+ report = JSON.parse(content);
978
+ } catch {}
979
+ }
980
+
981
+ if (!report || !Array.isArray(report.rows) || report.rows.length === 0) {
982
+ sendJson(res, 400, { error: 'No benchmark evaluation report available to summarize. Run an evaluation first.' });
983
+ return true;
984
+ }
985
+
986
+ const rows = report.rows;
987
+ const totalCases = rows.length;
988
+ const errorRows = rows.filter(r => r.status === 'error' || r.error);
989
+ const anomalies = rows.filter(isAnomalyRow);
990
+ const passCount = rows.filter(r => r.coverage?.allMatched || (r.status === 'completed' && !isAnomalyRow(r))).length;
991
+ const passRate = totalCases > 0 ? Number(((passCount / totalCases) * 100).toFixed(1)) : 0;
992
+
993
+ const categoryStats = {};
994
+ for (const r of rows) {
995
+ const cat = r.category || 'general';
996
+ if (!categoryStats[cat]) categoryStats[cat] = { total: 0, passed: 0, errors: 0, loopFails: 0, toolFails: 0 };
997
+ categoryStats[cat].total += 1;
998
+ const isPass = Boolean(r.coverage?.allMatched || (r.status === 'completed' && !isAnomalyRow(r)));
999
+ if (isPass) categoryStats[cat].passed += 1;
1000
+ if (r.status === 'error' || r.error) categoryStats[cat].errors += 1;
1001
+ if (r.trajectoryHealth?.hasLoop) categoryStats[cat].loopFails += 1;
1002
+ if (r.toolCorrectness && r.toolCorrectness.isCorrect === false) categoryStats[cat].toolFails += 1;
1003
+ }
1004
+
1005
+ const anomalyDetails = anomalies.slice(0, 8).map(a => ({
1006
+ id: a.id,
1007
+ category: a.category,
1008
+ question: a.question,
1009
+ reason: getAnomalyReason(a),
1010
+ durationMs: a.durationMs
1011
+ }));
1012
+
1013
+ const avgDurationMs = Math.round(rows.reduce((acc, r) => acc + (Number(r.durationMs) || 0), 0) / Math.max(1, totalCases));
1014
+
1015
+ const prompt = `You are the Lead AI Benchmark Evaluation Architect for the Kiku Agent system.
1016
+ Analyze these evaluation results and provide an executive-level, structured summary in Markdown. Keep sections concise, high-impact, and actionable.
1017
+ CRITICAL INSTRUCTION: DO NOT output any <think> tags, internal thoughts, or scratchpads. Return ONLY the final Markdown summary directly.
1018
+
1019
+ Benchmark Summary Data:
1020
+ - Model Evaluated: ${report.metadata?.model || DEFAULTS.model}
1021
+ - Total Cases: ${totalCases}
1022
+ - Overall Pass Rate: ${passRate}% (${passCount}/${totalCases} passed)
1023
+ - Anomalies / Failures: ${anomalies.length} (${errorRows.length} hard errors, ${anomalies.length - errorRows.length} behavioral/coverage failures)
1024
+ - Average Latency: ${avgDurationMs}ms
1025
+ - Category Performance:
1026
+ ${Object.entries(categoryStats).map(([k, v]) => ` * ${k}: ${v.passed}/${v.total} passed (${((v.passed / v.total) * 100).toFixed(1)}%), errors: ${v.errors}, toolFails: ${v.toolFails}`).join('\n')}
1027
+
1028
+ Top Anomaly Cases:
1029
+ ${anomalyDetails.map(d => `- [${d.id}] (${d.category}): "${d.question}" -> Cause: ${d.reason} (${d.durationMs}ms)`).join('\n')}
1030
+
1031
+ Report Structure to follow:
1032
+ ## 1. Executive Verdict & Health Overview
1033
+ ## 2. Category Breakdown Matrix
1034
+ ## 3. Anomaly & Root Cause Diagnosis
1035
+ ## 4. Operational & Latency Insights
1036
+ ## 5. Actionable Engineering Recommendations
1037
+ `;
1038
+
1039
+ let cleanSummary = '';
1040
+ let engineType = 'ai';
1041
+ let engineModel = report.metadata?.model || DEFAULTS.model;
1042
+ try {
1043
+ const { LLMClient } = await import('../../../llmClient.js');
1044
+ let client = new LLMClient({ timeout: 120000 });
1045
+ let health = await client.healthCheck();
1046
+ if (!health.connected && client.provider !== 'lmstudio') {
1047
+ const fallbackClient = new LLMClient({ provider: 'lmstudio', timeout: 120000 });
1048
+ const fbHealth = await fallbackClient.healthCheck();
1049
+ if (fbHealth.connected) {
1050
+ client = fallbackClient;
1051
+ }
1052
+ }
1053
+
1054
+ const response = await client.chat({
1055
+ messages: [
1056
+ {
1057
+ role: 'system',
1058
+ content: 'You are the Lead AI Benchmark Evaluation Architect for Kiku Agent. Provide an executive-level, structured evaluation report in Markdown. Highlight strengths, root-cause all anomalies, and give actionable fixes. CRITICAL: DO NOT output any <think> tags or reasoning thoughts. Return ONLY the final Markdown report.'
1059
+ },
1060
+ { role: 'user', content: prompt }
1061
+ ],
1062
+ temperature: 0.2,
1063
+ maxTokens: 1500,
1064
+ stream: false
1065
+ });
1066
+
1067
+ cleanSummary = String(response.content || '');
1068
+ cleanSummary = cleanSummary.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
1069
+ cleanSummary = cleanSummary.replace(/^thought[\s\S]*?\n\n/i, '').trim();
1070
+ if (cleanSummary) {
1071
+ engineModel = client.model || engineModel;
1072
+ }
1073
+ } catch {
1074
+ engineType = 'deterministic_fallback';
1075
+ cleanSummary = generateDeterministicSummary({ report, totalCases, passCount, passRate, anomalies, categoryStats, avgDurationMs });
1076
+ }
1077
+
1078
+ if (!cleanSummary) {
1079
+ engineType = 'deterministic_fallback';
1080
+ cleanSummary = generateDeterministicSummary({ report, totalCases, passCount, passRate, anomalies, categoryStats, avgDurationMs });
1081
+ }
1082
+
1083
+ sendJson(res, 200, {
1084
+ status: 'success',
1085
+ summary: cleanSummary,
1086
+ metadata: {
1087
+ model: engineModel,
1088
+ engine: engineType,
1089
+ totalCases,
1090
+ passRate: `${passRate}%`,
1091
+ anomaliesCount: anomalies.length,
1092
+ generatedAt: new Date().toISOString()
1093
+ }
1094
+ });
1095
+ return true;
1096
+ }
1097
+
1098
+ return false;
1099
+ }