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,288 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { applyGates } from '../analysis/gates.js';
6
+ import { judgeReport } from '../analysis/judge.js';
7
+ import { DEFAULTS, REPO_ROOT } from '../core/config.js';
8
+ import {
9
+ acquireRunLock,
10
+ assertResumeCompatible,
11
+ buildMetadata,
12
+ createSaver,
13
+ finalizeMetadata,
14
+ loadReport,
15
+ loadReportWithBackups,
16
+ rotateBackups,
17
+ verifyCorpusFreshness,
18
+ writeBlockedReport,
19
+ } from '../core/store.js';
20
+ import { writeDashboardReport } from '../reporting/dashboard.js';
21
+ import {
22
+ allCases,
23
+ cases,
24
+ expandedCases,
25
+ legacyCases,
26
+ securityCases,
27
+ validateCases,
28
+ } from './cases.js';
29
+
30
+ const cliEntry = fileURLToPath(new URL('../core/cli.js', import.meta.url));
31
+
32
+ export function selectCases({ suite = DEFAULTS.suite, ids, limit } = {}) {
33
+ const full = [...new Set([...allCases, ...(expandedCases || [])])];
34
+ const suiteMap = {
35
+ catalog: cases,
36
+ expanded: expandedCases || [],
37
+ security: securityCases,
38
+ legacy: legacyCases,
39
+ all: full,
40
+ full,
41
+ };
42
+ const idList = Array.isArray(ids)
43
+ ? ids.map(s => String(s).trim()).filter(Boolean)
44
+ : (typeof ids === 'string' ? ids.split(',').map(s => s.trim()).filter(Boolean) : null);
45
+ const corpus = (idList && idList.length > 0) ? full : (suiteMap[suite] || full);
46
+ if (idList?.some(id => !corpus.some(c => c.id === id))) throw new Error('Unknown case id');
47
+ const count = limit === undefined ? corpus.length : Number(limit);
48
+ if (!Number.isInteger(count) || count < 1) throw new Error('limit must be a positive integer');
49
+ return corpus.filter(c => !idList || idList.includes(c.id)).slice(0, count);
50
+ }
51
+
52
+ export async function probeModel({ baseUrl, model }) {
53
+ const isLocal = baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost');
54
+ if (isLocal) {
55
+ const { wakeUpLmStudio } = await import('../../../lmStudioWakeup.js');
56
+ await wakeUpLmStudio({ baseUrl, model });
57
+ }
58
+ const response = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, { signal: AbortSignal.timeout(8000) });
59
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
60
+ const models = await response.json();
61
+ if (!models.data?.some(m => m.id === model)) throw new Error(`Model not advertised: ${model}`);
62
+ }
63
+
64
+ function spawnWorker(caseId, { model, baseUrl, timeoutMs, workerGraceMs, signal, onStep }) {
65
+ return new Promise(resolve => {
66
+ const template = allCases.find(c => c.id === caseId) || expandedCases?.find(c => c.id === caseId);
67
+ if (signal?.aborted) {
68
+ resolve({ id: template.id, category: template.category, language: template.language, question: template.question, status: 'error', error: 'Aborted' });
69
+ return;
70
+ }
71
+ const child = spawn(
72
+ process.execPath,
73
+ [cliEntry, '__worker', '--id', caseId, '--model', model, '--base-url', baseUrl, '--timeout-ms', String(timeoutMs)],
74
+ { cwd: REPO_ROOT, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }
75
+ );
76
+ let stdout = '', stderr = '';
77
+ let timedOut = false;
78
+ const timer = setTimeout(() => { timedOut = true; child.kill(); }, timeoutMs + workerGraceMs);
79
+ const onAbort = () => {
80
+ try { child.kill(); } catch {}
81
+ };
82
+ if (signal) signal.addEventListener('abort', onAbort, { once: true });
83
+ child.stdout.on('data', b => {
84
+ const text = b.toString();
85
+ stdout += text;
86
+ if (onStep) {
87
+ const lines = text.split(/\r?\n/);
88
+ for (const l of lines) {
89
+ if (l.startsWith('EVAL_STEP=')) {
90
+ try {
91
+ onStep({ caseId, ...JSON.parse(l.slice('EVAL_STEP='.length)) });
92
+ } catch {}
93
+ }
94
+ }
95
+ }
96
+ });
97
+ child.stderr.on('data', b => { stderr = (stderr + b).slice(-5000); });
98
+ child.on('error', error => {
99
+ clearTimeout(timer);
100
+ if (signal) signal.removeEventListener('abort', onAbort);
101
+ resolve({ id: template.id, category: template.category, language: template.language, question: template.question, status: 'error', error: error.message });
102
+ });
103
+ child.on('close', () => {
104
+ clearTimeout(timer);
105
+ if (signal) signal.removeEventListener('abort', onAbort);
106
+ const line = stdout.split(/\r?\n/).find(l => l.startsWith('EVAL_RESULT='));
107
+ try {
108
+ if (!line) throw new Error(timedOut ? 'Worker hard timeout' : (signal?.aborted ? 'Aborted' : `Worker failed: ${stderr}`));
109
+ resolve(JSON.parse(line.slice('EVAL_RESULT='.length)));
110
+ } catch (error) { resolve({ id: template.id, category: template.category, language: template.language, question: template.question, status: 'error', error: error.message }); }
111
+ });
112
+ });
113
+ }
114
+
115
+ export async function runSuite(options = {}) {
116
+ const {
117
+ suite = DEFAULTS.suite,
118
+ ids,
119
+ limit,
120
+ model = DEFAULTS.model,
121
+ baseUrl = DEFAULTS.baseUrl,
122
+ timeoutMs = DEFAULTS.timeoutMs,
123
+ workerGraceMs = DEFAULTS.workerGraceMs,
124
+ out = DEFAULTS.out,
125
+ from,
126
+ resume = false,
127
+ retryFailed = false,
128
+ retryErrors = false,
129
+ preservePrior = false,
130
+ judgeModel,
131
+ judgeOnly = false,
132
+ gate = false,
133
+ gatesFile = DEFAULTS.gatesFile,
134
+ saveAs,
135
+ dashboard = false,
136
+ dashboardOut = DEFAULTS.dashboardOut,
137
+ signal,
138
+ onStart,
139
+ onCaseStart,
140
+ onCaseComplete,
141
+ onStep,
142
+ } = options;
143
+
144
+ validateCases();
145
+
146
+ const outPath = path.isAbsolute(out) ? out : path.resolve(REPO_ROOT, out);
147
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
148
+
149
+ let report;
150
+ let save = null;
151
+ if (judgeOnly) {
152
+ if (!judgeModel) throw new Error('judge-only requires judge-model');
153
+ report = await loadReport(outPath);
154
+ await verifyCorpusFreshness(report);
155
+ save = createSaver(outPath, report);
156
+ } else {
157
+ let selected = selectCases({ suite, ids, limit });
158
+ let prior = null;
159
+ const fromPath = from ? (path.isAbsolute(from) ? from : path.resolve(REPO_ROOT, from)) : outPath;
160
+ prior = await loadReportWithBackups(fromPath);
161
+ if (!prior && fromPath !== path.resolve(REPO_ROOT, DEFAULTS.out)) {
162
+ prior = await loadReportWithBackups(path.resolve(REPO_ROOT, DEFAULTS.out));
163
+ }
164
+ if (fromPath === outPath && prior) {
165
+ await rotateBackups(outPath, 5);
166
+ }
167
+ const doRetry = retryFailed || retryErrors || preservePrior;
168
+ if (doRetry && prior) {
169
+ if (!ids) {
170
+ const targetIds = new Set(
171
+ prior.rows
172
+ .filter(r => {
173
+ if (r.status === 'error' || r.error || r.timedOut) return true;
174
+ if (retryErrors) return false;
175
+ return (r.coverage && r.coverage.matched < r.coverage.total) || r.pass === false;
176
+ })
177
+ .map(r => r.id)
178
+ );
179
+ selected = selected.filter(c => targetIds.has(c.id));
180
+ }
181
+ }
182
+ try {
183
+ await probeModel({ baseUrl, model });
184
+ } catch (error) {
185
+ await writeBlockedReport(outPath, { reason: error.message, planned: selected.length });
186
+ throw error;
187
+ }
188
+ const metadata = buildMetadata({ model, baseUrl, timeoutMs, caseIds: selected.map(c => c.id) });
189
+ await finalizeMetadata(metadata);
190
+ report = { metadata, rows: [] };
191
+ if (doRetry && prior) {
192
+ report.metadata = {
193
+ ...prior.metadata,
194
+ resumedAt: new Date().toISOString(),
195
+ model: model || prior.metadata.model,
196
+ baseUrl: baseUrl || prior.metadata.baseUrl
197
+ };
198
+ await finalizeMetadata(report.metadata);
199
+ delete report.metadata.archivedAt;
200
+ report.rows = [...prior.rows];
201
+ } else if (resume && prior) {
202
+ assertResumeCompatible(prior, report);
203
+ report.rows = prior.rows.filter(r => selected.some(c => c.id === r.id));
204
+ report.metadata.startedAt = prior.metadata.startedAt;
205
+ report.metadata.resumedAt = new Date().toISOString();
206
+ }
207
+ save = createSaver(outPath, report);
208
+ await save();
209
+ const casesToRun = resume ? selected.filter(c => !report.rows.some(r => r.id === c.id)) : selected;
210
+ if (onStart) onStart({ planned: casesToRun.length, total: selected.length, model, suite, caseIds: casesToRun.map(c => c.id) });
211
+ const lock = await acquireRunLock(outPath);
212
+ try {
213
+ let doneCases = 0;
214
+ let consecutiveErrors = 0;
215
+ for (const c of casesToRun) {
216
+ if (signal?.aborted) break;
217
+ doneCases++;
218
+ if (onCaseStart) onCaseStart(c, { current: doneCases, total: casesToRun.length });
219
+ const result = await spawnWorker(c.id, { model, baseUrl, timeoutMs, workerGraceMs, signal, onStep });
220
+ result.model = model;
221
+ const existingIdx = report.rows.findIndex(r => r.id === c.id);
222
+ if (existingIdx >= 0) {
223
+ report.rows[existingIdx] = result;
224
+ } else {
225
+ report.rows.push(result);
226
+ }
227
+ await save(result);
228
+ await lock.touch();
229
+ if (onCaseComplete) onCaseComplete(c, result, { current: doneCases, total: casesToRun.length, report });
230
+ console.log(`${doneCases}/${casesToRun.length} ${c.id}: ${result.status}; ${result.durationMs ?? '?'}ms; coverage ${result.coverage?.matched ?? '-'}/${result.coverage?.total ?? '-'}`);
231
+ if (result.status === 'error') {
232
+ consecutiveErrors++;
233
+ if (consecutiveErrors >= 3) {
234
+ const abortMsg = `Evaluation aborted: 3 consecutive errors encountered (${result.error || 'Connection failure'}). Please check LM Studio manually. (เกิดข้อผิดพลาดติดต่อกัน 3 ครั้ง กรุณาตรวจสอบสถานะของ LM Studio หรือการตั้งค่าด้วยตนเอง)`;
235
+ console.error(abortMsg);
236
+ report.metadata.abortReason = abortMsg;
237
+ break;
238
+ }
239
+ } else {
240
+ consecutiveErrors = 0;
241
+ }
242
+ if (signal?.aborted) break;
243
+ }
244
+ report.metadata.finishedAt = new Date().toISOString();
245
+ await save();
246
+ } finally {
247
+ await lock.release();
248
+ }
249
+ if (report.metadata.abortReason) {
250
+ throw new Error(report.metadata.abortReason);
251
+ }
252
+ }
253
+
254
+ if (!save) save = createSaver(outPath, report);
255
+ const postRunLock = await acquireRunLock(outPath);
256
+ try {
257
+ if (judgeModel) {
258
+ await judgeReport(report, { judgeModel, baseUrl, timeoutMs }, save);
259
+ }
260
+ await save();
261
+
262
+ if (gate) {
263
+ const gateEval = await applyGates(report, gatesFile, save);
264
+ if (!gateEval.passed) process.exitCode = 2;
265
+ }
266
+ } finally {
267
+ await postRunLock.release();
268
+ }
269
+
270
+ if (saveAs) {
271
+ const snapshotPath = path.resolve(REPO_ROOT, DEFAULTS.snapshotsDir, `${saveAs}.json`);
272
+ await fs.mkdir(path.dirname(snapshotPath), { recursive: true });
273
+ await fs.writeFile(snapshotPath, JSON.stringify(report, null, 2));
274
+ console.log(`Snapshot saved: ${snapshotPath}`);
275
+ }
276
+
277
+ if (dashboard) {
278
+ const dashPath = path.isAbsolute(dashboardOut) ? dashboardOut : path.resolve(REPO_ROOT, dashboardOut);
279
+ await writeDashboardReport(report, dashPath);
280
+ console.log(`Performance dashboard generated: ${dashPath}`);
281
+ }
282
+
283
+ console.log(JSON.stringify(report.summary, null, 2));
284
+ if (report.summary.errors || report.summary.judgeErrors) {
285
+ process.exitCode = 1;
286
+ }
287
+ return report;
288
+ }
@@ -0,0 +1,363 @@
1
+ import { computeBenchmarkSummary } from '../reporting/benchmark.js';
2
+ const normalize = s => String(s).normalize('NFKC').toLowerCase().replace(/\s+/g, ' ');
3
+
4
+ function aliasMatches(text, alias) {
5
+ const normalizedText = normalize(text);
6
+ const normalizedAlias = normalize(alias);
7
+ if (!normalizedAlias) return false;
8
+ if (!/[\p{N}]/u.test(normalizedAlias)) return normalizedText.includes(normalizedAlias);
9
+ const timeMatch = normalizedAlias.match(/^(\d{1,2}):(\d{2})$/);
10
+ const escaped = timeMatch
11
+ ? `0?${timeMatch[1]}:${timeMatch[2]}`
12
+ : normalizedAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13
+ return new RegExp(`(?<![\\p{N}])${escaped}(?![\\p{N}])`, 'u').test(normalizedText);
14
+ }
15
+
16
+ function expandAnswerEquivalences(s) {
17
+ let text = String(s);
18
+ text = text.replace(/(?:พ\.ศ\.|พุทธศักราช)\s*(\d{4})/g, (match, beYear) => {
19
+ const ce = parseInt(beYear, 10) - 543;
20
+ return `${match} (${ce}) ${ce}`;
21
+ });
22
+ text = text.replace(/(\d+)\s*(?:minutes?|mins?|นาที)\s*(?:and|กับ|และ)?\s*(\d+)\s*(?:seconds?|secs?|วินาที)/gi, (match, m, s) => {
23
+ const secPadded = String(s).padStart(2, '0');
24
+ return `${match} ${m}:${secPadded}`;
25
+ });
26
+ return text;
27
+ }
28
+
29
+ export function scoreCoverage(c, answer) {
30
+ if (!c.groups.length) return { matched: 0, total: 0, allMatched: null, missing: [] };
31
+ const text = normalize(expandAnswerEquivalences(answer));
32
+ const missing = c.groups.filter(group => !group.some(alias => aliasMatches(text, alias)));
33
+ return { matched: c.groups.length - missing.length, total: c.groups.length, allMatched: missing.length === 0, missing };
34
+ }
35
+
36
+ export function parseJudgment(content) {
37
+ const clean = String(content).trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
38
+ const result = JSON.parse(clean);
39
+ for (const field of ['correctness', 'grounding', 'instructionFollowing']) {
40
+ if (!Number.isInteger(result[field]) || result[field] < 0 || result[field] > 4) throw new Error(`Invalid judge ${field}`);
41
+ }
42
+ if (typeof result.reason !== 'string' || !result.reason.trim() || !Array.isArray(result.unsupportedClaims) || result.unsupportedClaims.some(x => typeof x !== 'string')) throw new Error('Invalid judge explanation');
43
+ return result;
44
+ }
45
+
46
+ export function evaluateToolCorrectness(c, trace = []) {
47
+ const actualTools = [...new Set(trace.filter(t => t.event === 'start' && t.name).map(t => t.name))];
48
+ const expectedTools = Array.isArray(c?.expectedTools) ? [...new Set(c.expectedTools)] : [];
49
+
50
+ if (!expectedTools.length) {
51
+ return {
52
+ expected: [],
53
+ actual: actualTools,
54
+ matched: [],
55
+ missing: [],
56
+ unexpected: actualTools,
57
+ precision: null,
58
+ recall: null,
59
+ f1: null,
60
+ isCorrect: null,
61
+ notApplicable: true,
62
+ toolsUsed: actualTools
63
+ };
64
+ }
65
+
66
+ const matched = actualTools.filter(t => expectedTools.includes(t));
67
+ const missing = expectedTools.filter(t => !actualTools.includes(t));
68
+ const unexpected = actualTools.filter(t => !expectedTools.includes(t));
69
+
70
+ const precision = actualTools.length ? matched.length / actualTools.length : 0.0;
71
+ const recall = expectedTools.length ? matched.length / expectedTools.length : 0.0;
72
+ const f1 = (precision + recall) > 0 ? (2 * precision * recall) / (precision + recall) : 0.0;
73
+
74
+ const isCorrect = matched.length > 0 && recall >= 0.5 && precision >= 0.5;
75
+
76
+ return {
77
+ expected: expectedTools,
78
+ actual: actualTools,
79
+ matched,
80
+ missing,
81
+ unexpected,
82
+ precision: Number(precision.toFixed(3)),
83
+ recall: Number(recall.toFixed(3)),
84
+ f1: Number(f1.toFixed(3)),
85
+ isCorrect,
86
+ notApplicable: false,
87
+ toolsUsed: actualTools
88
+ };
89
+ }
90
+
91
+ export function detectTrajectoryLoops(trace = []) {
92
+ const toolStarts = trace.filter(t => t.event === 'start' && t.name);
93
+ if (toolStarts.length < 2) {
94
+ return { hasLoop: false, loopCount: 0, duplicateCalls: [], isThrashing: false };
95
+ }
96
+
97
+ const calls = toolStarts.map(t => {
98
+ let paramKey = '';
99
+ try {
100
+ paramKey = JSON.stringify(t.parameters || {});
101
+ } catch {
102
+ paramKey = String(t.parameters);
103
+ }
104
+ return `${t.name}:${paramKey}`;
105
+ });
106
+
107
+ const duplicateCalls = [];
108
+ let loopCount = 0;
109
+
110
+ for (let i = 1; i < calls.length; i++) {
111
+ if (calls[i] === calls[i - 1]) {
112
+ loopCount++;
113
+ duplicateCalls.push(calls[i]);
114
+ }
115
+ }
116
+
117
+ if (calls.length >= 4) {
118
+ for (let i = 3; i < calls.length; i++) {
119
+ if (calls[i] === calls[i - 2] && calls[i - 1] === calls[i - 3] && calls[i] !== calls[i - 1]) {
120
+ loopCount++;
121
+ duplicateCalls.push(`oscillating:${calls[i]}<->${calls[i - 1]}`);
122
+ }
123
+ }
124
+ }
125
+
126
+ const isThrashing = (loopCount >= 1 && duplicateCalls.some(d => d.startsWith('oscillating'))) || loopCount >= 2 || toolStarts.length > 8;
127
+
128
+ return {
129
+ hasLoop: loopCount > 0,
130
+ loopCount,
131
+ duplicateCalls: [...new Set(duplicateCalls)],
132
+ isThrashing
133
+ };
134
+ }
135
+
136
+ export function computeLatencyBreakdown(row) {
137
+ const trace = Array.isArray(row.trace) ? row.trace : [];
138
+ const totalDurationMs = Number.isFinite(row.durationMs) ? Math.max(0, row.durationMs) : 0;
139
+
140
+ const toolExecutionMs = trace
141
+ .filter(t => t.event === 'result' && Number.isFinite(t.durationMs))
142
+ .reduce((acc, t) => acc + Math.max(0, t.durationMs), 0);
143
+
144
+ const llmInferenceMs = Math.max(0, totalDurationMs - toolExecutionMs);
145
+ const toolPercentage = totalDurationMs > 0 ? Number(((toolExecutionMs / totalDurationMs) * 100).toFixed(1)) : 0;
146
+ const llmPercentage = totalDurationMs > 0 ? Number(((llmInferenceMs / totalDurationMs) * 100).toFixed(1)) : 0;
147
+
148
+ const ttftMs = row.metrics?.timing_ms?.ttft ?? row.telemetry?.timeToFirstTokenMs ?? row.metrics?.ttftMs ?? null;
149
+ const decodeTps = row.metrics?.rates?.decode_tps ?? row.telemetry?.generationTokensPerSecond ?? row.metrics?.decodeTps ?? null;
150
+ const prefillTps = row.metrics?.rates?.prefill_tps ?? row.telemetry?.promptTokensPerSecond ?? row.metrics?.prefillTps ?? null;
151
+ const outputTokens = row.metrics?.counts?.completion_tokens ?? row.telemetry?.outputTokens ?? row.metrics?.outputTokens ?? null;
152
+
153
+ return {
154
+ totalDurationMs,
155
+ toolExecutionMs,
156
+ llmInferenceMs,
157
+ toolPercentage,
158
+ llmPercentage,
159
+ ttftMs,
160
+ decodeTps,
161
+ prefillTps,
162
+ outputTokens
163
+ };
164
+ }
165
+
166
+ export function checkGroundedness(c, answer, trace = []) {
167
+ if (!answer || typeof answer !== 'string') return { isGrounded: false, supportRatio: 0, checkedFacts: 0, notApplicable: false };
168
+ const text = normalize(answer);
169
+
170
+ const expectedTools = Array.isArray(c?.expectedTools) ? c.expectedTools : [];
171
+ if (!expectedTools.length) {
172
+ return {
173
+ isGrounded: true,
174
+ supportRatio: 1.0,
175
+ notApplicable: true,
176
+ evidenceFound: false,
177
+ note: 'Tool observation not required for this case'
178
+ };
179
+ }
180
+
181
+ const toolOutputs = trace
182
+ .filter(t => t.event === 'result' && t.value)
183
+ .map(t => typeof t.value === 'object' ? JSON.stringify(t.value) : String(t.value))
184
+ .join(' ')
185
+ .toLowerCase();
186
+
187
+ if (!toolOutputs.trim()) {
188
+ return {
189
+ isGrounded: false,
190
+ supportRatio: 0.0,
191
+ notApplicable: false,
192
+ evidenceFound: false,
193
+ note: 'No tool observations found despite expected tools'
194
+ };
195
+ }
196
+
197
+ const groups = Array.isArray(c?.groups) ? c.groups : [];
198
+ if (!groups.length) {
199
+ return { isGrounded: true, supportRatio: 1.0, notApplicable: false, evidenceFound: true };
200
+ }
201
+
202
+ let matchedInAnswer = 0;
203
+ let groundedInTools = 0;
204
+
205
+ for (const group of groups) {
206
+ const presentInAnswer = group.some(alias => aliasMatches(text, alias));
207
+ if (presentInAnswer) {
208
+ matchedInAnswer++;
209
+ const presentInTools = group.some(alias => aliasMatches(toolOutputs, alias));
210
+ if (presentInTools) {
211
+ groundedInTools++;
212
+ }
213
+ }
214
+ }
215
+
216
+ if (matchedInAnswer === 0) {
217
+ return {
218
+ isGrounded: false,
219
+ supportRatio: 0.0,
220
+ notApplicable: false,
221
+ matchedInAnswer: 0,
222
+ groundedInTools: 0,
223
+ evidenceFound: true,
224
+ note: 'No extractable facts in answer to ground'
225
+ };
226
+ }
227
+
228
+ const supportRatio = Number((groundedInTools / matchedInAnswer).toFixed(2));
229
+ return {
230
+ isGrounded: supportRatio >= 0.8,
231
+ supportRatio,
232
+ notApplicable: false,
233
+ matchedInAnswer,
234
+ groundedInTools,
235
+ evidenceFound: true
236
+ };
237
+ }
238
+
239
+ export function summarize(rows, planned) {
240
+ const completed = rows.filter(r => r.status === 'completed');
241
+ const coverage = completed.filter(r => r.coverage && r.coverage.total > 0);
242
+ const judged = completed.filter(r => (r.judgeResult && !r.judgeResult.judgeError) || r.judgment);
243
+ const times = completed.map(r => r.durationMs).filter(Number.isFinite).sort((a, b) => a - b);
244
+ const fraction = (a, b) => b ? Number((a / b).toFixed(4)) : null;
245
+
246
+ const toolEligible = completed.filter(r => r.toolCorrectness && !r.toolCorrectness.notApplicable);
247
+ const toolCorrectRows = toolEligible.filter(r => r.toolCorrectness.isCorrect === true);
248
+
249
+ const loopFreeRows = completed.filter(r => !r.trajectoryHealth?.hasLoop);
250
+
251
+ const groundedEligible = completed.filter(r => r.groundedness && !r.groundedness.notApplicable);
252
+ const groundedRows = groundedEligible.filter(r => r.groundedness.isGrounded === true);
253
+
254
+ const llmTimes = completed.map(r => r.latencyBreakdown?.llmInferenceMs).filter(Number.isFinite);
255
+ const toolTimes = completed.map(r => r.latencyBreakdown?.toolExecutionMs).filter(Number.isFinite);
256
+ const decodeTpsList = completed.map(r => r.latencyBreakdown?.decodeTps).filter(Number.isFinite);
257
+
258
+ const mean = list => list.length ? Number((list.reduce((a, b) => a + b, 0) / list.length).toFixed(1)) : null;
259
+
260
+ const categoryStats = {};
261
+ for (const r of rows) {
262
+ const cat = r.category || 'unknown';
263
+ if (!categoryStats[cat]) {
264
+ categoryStats[cat] = { total: 0, completed: 0, factsMatched: 0, toolsCorrect: 0, toolEligible: 0, grounded: 0, groundedEligible: 0 };
265
+ }
266
+ categoryStats[cat].total++;
267
+ if (r.status === 'completed') {
268
+ categoryStats[cat].completed++;
269
+ if (r.coverage?.allMatched) categoryStats[cat].factsMatched++;
270
+ if (r.toolCorrectness && !r.toolCorrectness.notApplicable) {
271
+ categoryStats[cat].toolEligible++;
272
+ if (r.toolCorrectness.isCorrect) categoryStats[cat].toolsCorrect++;
273
+ }
274
+ if (r.groundedness && !r.groundedness.notApplicable) {
275
+ categoryStats[cat].groundedEligible++;
276
+ if (r.groundedness.isGrounded) categoryStats[cat].grounded++;
277
+ }
278
+ }
279
+ }
280
+
281
+ const languageStats = {};
282
+ for (const r of rows) {
283
+ const lang = r.language || 'unknown';
284
+ if (!languageStats[lang]) {
285
+ languageStats[lang] = { total: 0, completed: 0, factsMatched: 0, toolsCorrect: 0, toolEligible: 0, grounded: 0, groundedEligible: 0, meanMs: null };
286
+ }
287
+ languageStats[lang].total++;
288
+ if (r.status === 'completed') {
289
+ languageStats[lang].completed++;
290
+ if (r.coverage?.allMatched) languageStats[lang].factsMatched++;
291
+ if (r.toolCorrectness && !r.toolCorrectness.notApplicable) {
292
+ languageStats[lang].toolEligible++;
293
+ if (r.toolCorrectness.isCorrect) languageStats[lang].toolsCorrect++;
294
+ }
295
+ if (r.groundedness && !r.groundedness.notApplicable) {
296
+ languageStats[lang].groundedEligible++;
297
+ if (r.groundedness.isGrounded) languageStats[lang].grounded++;
298
+ }
299
+ }
300
+ }
301
+ for (const lang of Object.keys(languageStats)) {
302
+ const langTimes = completed.filter(r => (r.language || 'unknown') === lang).map(r => r.durationMs).filter(Number.isFinite);
303
+ languageStats[lang].meanMs = mean(langTimes);
304
+ }
305
+
306
+ const isRowJudgePass = r => {
307
+ if (r.judgeResult && !r.judgeResult.judgeError) {
308
+ return r.judgeResult.score === 1.0 || r.judgeResult.verdict === 'PASS';
309
+ }
310
+ if (r.judgment) {
311
+ return r.judgment.correctness >= 3 && r.judgment.grounding >= 3 && r.judgment.instructionFollowing >= 3 && !r.judgment.unsupportedClaims?.length;
312
+ }
313
+ return false;
314
+ };
315
+
316
+ const judgedWithCoverage = judged.filter(r => r.coverage && r.coverage.total > 0);
317
+ const judgeAgreed = judgedWithCoverage.filter(r => {
318
+ const judgePass = isRowJudgePass(r);
319
+ return Boolean(judgePass) === Boolean(r.coverage.allMatched);
320
+ });
321
+ const judgeAgreementRate = judgedWithCoverage.length ? fraction(judgeAgreed.length, judgedWithCoverage.length) : null;
322
+ const benchmark = computeBenchmarkSummary(rows);
323
+
324
+ return {
325
+ planned,
326
+ attempted: rows.length,
327
+ completed: completed.length,
328
+ errors: rows.length - completed.length,
329
+ completionRate: fraction(completed.length, planned),
330
+ allFactsMatched: coverage.filter(r => r.coverage.allMatched).length,
331
+ coverageEligibleCompleted: coverage.length,
332
+ lexicalCoverageRate: fraction(coverage.filter(r => r.coverage.allMatched).length, coverage.length),
333
+
334
+ toolAccuracyRate: toolEligible.length ? fraction(toolCorrectRows.length, toolEligible.length) : null,
335
+ toolEligibleCompleted: toolEligible.length,
336
+
337
+ loopFreeRate: completed.length ? fraction(loopFreeRows.length, completed.length) : null,
338
+
339
+ groundednessRate: groundedEligible.length ? fraction(groundedRows.length, groundedEligible.length) : null,
340
+ groundednessEligibleCompleted: groundedEligible.length,
341
+
342
+ meanLlmMs: mean(llmTimes),
343
+ meanToolMs: mean(toolTimes),
344
+ meanDecodeTps: mean(decodeTpsList),
345
+ categoryStats,
346
+ languageStats,
347
+
348
+ judged: judged.length,
349
+ judgeErrors: rows.filter(r => r.judgeError || r.judgeResult?.judgeError).length,
350
+ judgePasses: judged.filter(r => isRowJudgePass(r)).length,
351
+ judgeFlaggedUnsupported: judged.filter(r => r.judgment?.unsupportedClaims?.length).length,
352
+ judgeAgreementRate,
353
+
354
+ medianMs: times.length ? times[Math.floor((times.length - 1) / 2)] : null,
355
+ p95Ms: times.length ? times[Math.ceil(times.length * .95) - 1] : null,
356
+ passAt1: benchmark.values.passAt1,
357
+ trajectoryOptimality: benchmark.values.trajectoryOptimality,
358
+ toolF1: benchmark.values.toolF1,
359
+ hallucinationRate: benchmark.values.hallucinationRate,
360
+ costPerTask: benchmark.values.costPerTask,
361
+ benchmark,
362
+ };
363
+ }