kiku-agent 1.3.0 → 1.3.2

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 (155) hide show
  1. package/README.md +7 -7
  2. package/package.json +4 -7
  3. package/server/agent/agentCore.js +2 -2
  4. package/server/agent/catalogEntityResolver.js +8 -0
  5. package/server/agent/cli.js +4 -0
  6. package/server/agent/eval/AGENTS.md +228 -0
  7. package/server/agent/eval/analyze.py +684 -0
  8. package/server/agent/eval/gates.json +15 -0
  9. package/server/agent/eval/modules/analysis/compare.js +198 -0
  10. package/server/agent/eval/modules/analysis/gates.js +72 -0
  11. package/server/agent/eval/modules/analysis/judge.js +70 -0
  12. package/server/agent/eval/modules/core/cli.js +453 -0
  13. package/server/agent/eval/modules/core/config.js +60 -0
  14. package/server/agent/eval/modules/core/store.js +236 -0
  15. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  16. package/server/agent/eval/modules/evaluation/runner.js +298 -0
  17. package/server/agent/eval/modules/evaluation/scoring.js +351 -0
  18. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  19. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  20. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  21. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  22. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  23. package/server/agent/eval/modules/ui/evalUi.js +4796 -0
  24. package/server/agent/eval/modules/utils/smokes.js +119 -0
  25. package/server/agent/ink/App.jsx +348 -0
  26. package/server/agent/ink/Panel.jsx +54 -0
  27. package/server/agent/ink/PromptInput.jsx +321 -0
  28. package/server/agent/ink/TaskPlanPanel.jsx +88 -0
  29. package/server/agent/ink/ThinkingPanel.jsx +68 -0
  30. package/server/agent/ink/ToolResult.jsx +133 -0
  31. package/server/agent/ink/main.jsx +195 -0
  32. package/server/agent/ink/useEventBus.js +218 -0
  33. package/server/agent/memory/contextAllocator.js +214 -0
  34. package/server/agent/memory/hierarchicalStore.js +523 -0
  35. package/server/agent/memory/reflectionWorker.js +110 -0
  36. package/server/agent/prompts.js +1 -1
  37. package/server/agent/telemetry/hardwareTiers.js +332 -0
  38. package/server/agent/telemetry/runtimeWatchdog.js +66 -0
  39. package/server/agent/telemetry/techPowerUpClient.js +682 -0
  40. package/server/agent/telemetry/techpowerup-mcp.json +22 -0
  41. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  42. package/server/agent/tests/agentFollowUps.test.js +127 -0
  43. package/server/agent/tests/apiRouter.test.js +108 -0
  44. package/server/agent/tests/architectureFixes.test.js +81 -0
  45. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  46. package/server/agent/tests/autoResearch.test.js +64 -0
  47. package/server/agent/tests/bilibiliEmbed.test.js +251 -0
  48. package/server/agent/tests/catalogSync.test.js +130 -0
  49. package/server/agent/tests/citations.test.js +180 -0
  50. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  51. package/server/agent/tests/clarifyingQuestions.test.js +594 -0
  52. package/server/agent/tests/cliAutomation.test.js +121 -0
  53. package/server/agent/tests/cloudProviders.test.js +234 -0
  54. package/server/agent/tests/commandInput.test.js +24 -0
  55. package/server/agent/tests/commands.test.js +417 -0
  56. package/server/agent/tests/contextIntelligence.test.js +162 -0
  57. package/server/agent/tests/conversationSafety.test.js +53 -0
  58. package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
  59. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  60. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  61. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  62. package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
  63. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  64. package/server/agent/tests/envBootstrap.test.js +87 -0
  65. package/server/agent/tests/envCommand.test.js +215 -0
  66. package/server/agent/tests/errorHandling.test.js +80 -0
  67. package/server/agent/tests/errors.test.js +13 -0
  68. package/server/agent/tests/evalAnalyze.test.js +54 -0
  69. package/server/agent/tests/evalCommand.test.js +54 -0
  70. package/server/agent/tests/evalCorpus.test.js +126 -0
  71. package/server/agent/tests/evalHarness.test.js +62 -0
  72. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  73. package/server/agent/tests/evalMetrics.test.js +312 -0
  74. package/server/agent/tests/evalStore.test.js +190 -0
  75. package/server/agent/tests/evalStudio.test.js +179 -0
  76. package/server/agent/tests/feedbackLearning.test.js +186 -0
  77. package/server/agent/tests/firstRun.test.js +44 -0
  78. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  79. package/server/agent/tests/hardwareTiers.test.js +206 -0
  80. package/server/agent/tests/hybridWebSearch.test.js +192 -0
  81. package/server/agent/tests/judgeService.test.js +251 -0
  82. package/server/agent/tests/lanDiscovery.test.js +319 -0
  83. package/server/agent/tests/mcpClient.test.js +201 -0
  84. package/server/agent/tests/modelManager.test.js +109 -0
  85. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  86. package/server/agent/tests/newCommands.test.js +347 -0
  87. package/server/agent/tests/newCommandsTui.test.js +141 -0
  88. package/server/agent/tests/newTools.test.js +113 -0
  89. package/server/agent/tests/newsIntelligence.test.js +178 -0
  90. package/server/agent/tests/nextGenArchitecture.test.js +99 -0
  91. package/server/agent/tests/offlineInput.test.js +217 -0
  92. package/server/agent/tests/packagingGuard.test.js +57 -0
  93. package/server/agent/tests/presentation.test.js +61 -0
  94. package/server/agent/tests/providerPersistence.test.js +39 -0
  95. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  96. package/server/agent/tests/queryDetails.test.js +126 -0
  97. package/server/agent/tests/realWorldValidation.test.js +391 -0
  98. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  99. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  100. package/server/agent/tests/sanitizer.test.js +37 -0
  101. package/server/agent/tests/scenariosData.js +378 -0
  102. package/server/agent/tests/schemaRouting.test.js +54 -0
  103. package/server/agent/tests/securityAuditRemediation.test.js +214 -0
  104. package/server/agent/tests/securityHardening.test.js +146 -0
  105. package/server/agent/tests/securitySanitizer.test.js +38 -0
  106. package/server/agent/tests/sessionAndCopy.test.js +232 -0
  107. package/server/agent/tests/sidebarControls.test.js +101 -0
  108. package/server/agent/tests/statefulMemory.test.js +270 -0
  109. package/server/agent/tests/subagent.test.js +216 -0
  110. package/server/agent/tests/subagentProgress.test.js +109 -0
  111. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  112. package/server/agent/tests/suggestions.test.js +199 -0
  113. package/server/agent/tests/suggestionsTui.test.js +251 -0
  114. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  115. package/server/agent/tests/terminalImage.test.js +80 -0
  116. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  117. package/server/agent/tests/terminalText.test.js +133 -0
  118. package/server/agent/tests/textProtocolLoop.test.js +126 -0
  119. package/server/agent/tests/textToolParsing.test.js +139 -0
  120. package/server/agent/tests/theme.test.js +26 -0
  121. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  122. package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
  123. package/server/agent/tests/tools.test.js +259 -0
  124. package/server/agent/tests/unifiedEndpoints.test.js +152 -0
  125. package/server/agent/tests/v110Features.test.js +262 -0
  126. package/server/agent/tests/versionBump.test.js +69 -0
  127. package/server/agent/tests/vramEstimator.test.js +520 -0
  128. package/server/agent/tests/webChatInspection.test.js +300 -0
  129. package/server/agent/tests/webChatMemory.test.js +112 -0
  130. package/server/agent/tests/webChatSecurity.test.js +169 -0
  131. package/server/agent/tests/webExtract.test.js +261 -0
  132. package/server/agent/tests/webSources.test.js +247 -0
  133. package/server/agent/websearch/adapters/duckduckgo.js +97 -0
  134. package/server/agent/websearch/adapters/jina.js +77 -0
  135. package/server/agent/websearch/adapters/searxng.js +57 -0
  136. package/server/agent/websearch/core/canonical.js +89 -0
  137. package/server/agent/websearch/core/fusion.js +81 -0
  138. package/server/agent/websearch/core/types.js +29 -0
  139. package/server/agent/websearch/extract.js +605 -0
  140. package/server/agent/websearch/index.js +43 -0
  141. package/server/agent/websearch/orchestrator.js +266 -0
  142. package/server/agent/websearch/resilience/breaker.js +97 -0
  143. package/server/agent/websearch/resilience/cache.js +59 -0
  144. package/server/agent/websearch/resilience/limiter.js +93 -0
  145. package/server/agent/websearch/security/filter.js +36 -0
  146. package/server/agent/websearch/security/ssrf.js +289 -0
  147. package/server/agent/websearch/telemetry.js +71 -0
  148. package/server/apiRouter.js +7 -11
  149. package/server/eval/judgeRubrics.js +110 -0
  150. package/server/eval/judgeService.js +290 -0
  151. package/server/eval/runBenchmark.js +182 -0
  152. package/server/mcp/examples/librechat.example.yaml +16 -0
  153. package/server/mcp/tests/connection.test.js +156 -0
  154. package/server/routes/agent.js +55 -0
  155. package/server/supabase_schema.sql +63 -0
@@ -0,0 +1,101 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import test from 'node:test';
4
+ import serverlessHandler, { isWebChatRequest } from '../../../api/index.js';
5
+ import { getWebChatHtml } from '../../web-chat/ui/htmlBuilder.js';
6
+
7
+ test('Vercel routes the web chat page through the serverless web-chat handler first', () => {
8
+ const config = JSON.parse(readFileSync(new URL('../../../vercel.json', import.meta.url), 'utf8'));
9
+ const chatIndex = config.rewrites.findIndex(rule => rule.source === '/chat');
10
+ const apiIndex = config.rewrites.findIndex(rule => rule.source === '/api/(.*)');
11
+ const spaIndex = config.rewrites.findIndex(rule => rule.source === '/(.*)');
12
+
13
+ assert.ok(chatIndex >= 0);
14
+ assert.equal(config.rewrites[chatIndex].destination, '/api/index.js');
15
+ assert.ok(chatIndex < apiIndex);
16
+ assert.ok(apiIndex < spaIndex);
17
+ });
18
+
19
+ test('serverless dispatcher separates web-chat APIs from REST API v1', () => {
20
+ [
21
+ '/chat',
22
+ '/chat/session',
23
+ '/api/chat',
24
+ '/api/status',
25
+ '/api/suggestions',
26
+ '/api/ai-suggest',
27
+ '/api/quiz',
28
+ '/api/export',
29
+ '/api/verify',
30
+ '/api/news',
31
+ '/api/compare',
32
+ '/api/clarify',
33
+ '/api/clarify/analytics',
34
+ '/api/feedback',
35
+ '/api/feedback/stats'
36
+ ].forEach(url => assert.equal(isWebChatRequest({ url }), true, url));
37
+
38
+ assert.equal(isWebChatRequest({ url: '/api/v1/timeline' }), false);
39
+ assert.equal(isWebChatRequest({ url: '/api/v1/profile' }), false);
40
+ });
41
+
42
+ test('serverless dispatcher renders the Agent Chat page for /chat routes', async () => {
43
+ const response = {
44
+ headers: {},
45
+ statusCode: 0,
46
+ body: '',
47
+ setHeader(name, value) { this.headers[name] = value; },
48
+ writeHead(statusCode, headers = {}) {
49
+ this.statusCode = statusCode;
50
+ Object.assign(this.headers, headers);
51
+ },
52
+ end(body = '') { this.body += body; }
53
+ };
54
+
55
+ await serverlessHandler({ url: '/chat', method: 'GET' }, response);
56
+
57
+ assert.equal(response.statusCode, 200);
58
+ assert.match(response.headers['Content-Type'], /text\/html/);
59
+ assert.match(response.body, /id="app-sidebar"/);
60
+ assert.match(response.body, /KIKU AGENT/);
61
+ });
62
+
63
+ test('sidebar language refresh preserves dynamic control state labels', () => {
64
+ const html = getWebChatHtml();
65
+ const start = html.indexOf('function updateLanguageUi()');
66
+ const end = html.indexOf('var JU_JINGYI_PLAYLIST', start);
67
+ const languageFunction = html.slice(start, end);
68
+
69
+ const genericLabels = languageFunction.indexOf('updateSidebarLabels(t);');
70
+ assert.ok(genericLabels >= 0);
71
+ assert.ok(genericLabels < languageFunction.indexOf('updatePetalsLabel();'));
72
+ assert.ok(genericLabels < languageFunction.indexOf('updateThemeIcon();'));
73
+ assert.ok(genericLabels < languageFunction.indexOf('updateSoundIcon();'));
74
+ assert.ok(genericLabels < languageFunction.indexOf("document.getElementById('sidebar-lang-label')"));
75
+ });
76
+
77
+ test('jukebox derives sidebar and tab counts from the playlist', () => {
78
+ const html = getWebChatHtml();
79
+
80
+ assert.match(html, /function updateJukeboxCountLabels\(\)/);
81
+ assert.match(html, /const total = JU_JINGYI_PLAYLIST\.length/);
82
+ assert.match(html, /id="sidebar-music-button"/);
83
+ assert.doesNotMatch(html, /Jukebox \(44 บทเพลง\)/);
84
+ assert.doesNotMatch(html, /ทั้งหมด \(44\)/);
85
+ });
86
+
87
+ test('sidebar export downloads through the export API and reports failures', () => {
88
+ const html = getWebChatHtml();
89
+ const start = html.indexOf('async function exportChat()');
90
+ const end = html.indexOf('function shareChatLink()', start);
91
+ const exportFunction = html.slice(start, end);
92
+
93
+ assert.ok(start >= 0);
94
+ assert.match(exportFunction, /fetch\('\/api\/export'/);
95
+ assert.match(exportFunction, /if \(!response\.ok\) throw new Error/);
96
+ assert.match(exportFunction, /link\.download = filename/);
97
+ assert.doesNotMatch(exportFunction, /showToast/);
98
+ assert.doesNotMatch(exportFunction, /navigator\.clipboard/);
99
+ assert.match(html, /title="ส่งออกบทสนทนาเป็นไฟล์ Markdown"/);
100
+ assert.doesNotMatch(html, /Export \/ Copy Chat/);
101
+ });
@@ -0,0 +1,270 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { mkdtemp, rm } from 'node:fs/promises';
6
+
7
+ import { HierarchicalStore, parseAndValidatePath, computeContentHash } from '../memory/hierarchicalStore.js';
8
+ import { ContextAllocator } from '../memory/contextAllocator.js';
9
+ import { ReflectionWorker } from '../memory/reflectionWorker.js';
10
+ import { AgentMemory } from '../memory.js';
11
+ import { executeTool } from '../tools.js';
12
+
13
+ test('parseAndValidatePath correctly parses valid hierarchical memory paths', () => {
14
+ const p1 = parseAndValidatePath('kiku/runs/run_task_001/state');
15
+ assert.equal(p1.section, 'runs');
16
+ assert.equal(p1.runId, 'run_task_001');
17
+ assert.equal(p1.subpath, 'state');
18
+
19
+ const p2 = parseAndValidatePath('kiku/runs/run_task_001/plan');
20
+ assert.equal(p2.subpath, 'plan');
21
+
22
+ const p3 = parseAndValidatePath('kiku/runs/run_task_001/artifacts');
23
+ assert.equal(p3.subpath, 'artifacts');
24
+
25
+ const p4 = parseAndValidatePath('kiku/reflections/web_search');
26
+ assert.equal(p4.section, 'reflections');
27
+ assert.equal(p4.domain, 'web_search');
28
+
29
+ const p5 = parseAndValidatePath('kiku/guidelines/drama');
30
+ assert.equal(p5.section, 'guidelines');
31
+ assert.equal(p5.domain, 'drama');
32
+
33
+ const p6 = parseAndValidatePath('kiku/archive/run_task_001');
34
+ assert.equal(p6.section, 'archive');
35
+ assert.equal(p6.runId, 'run_task_001');
36
+ });
37
+
38
+ test('parseAndValidatePath rejects invalid paths, traversal, and invalid IDs', () => {
39
+ assert.throws(() => parseAndValidatePath('../kiku/runs/run_1/state'), /Invalid path traversal/);
40
+ assert.throws(() => parseAndValidatePath('invalid/runs/run_1/state'), /Path must start with "kiku\/"/);
41
+ assert.throws(() => parseAndValidatePath('kiku/unknown/section'), /Unrecognized memory section/);
42
+ assert.throws(() => parseAndValidatePath('kiku/runs/!bad@id/state'), /Invalid run_id/);
43
+ assert.throws(() => parseAndValidatePath('kiku/runs/run_1/bad_subpath'), /Invalid run subpath/);
44
+ });
45
+
46
+ test('HierarchicalStore enforces Pointer-Over-Data Rule (>1000 characters)', async () => {
47
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kiku-hstore-'));
48
+ try {
49
+ const store = new HierarchicalStore({
50
+ baseDir: path.join(tempDir, 'nodes'),
51
+ artifactsDir: path.join(tempDir, 'artifacts')
52
+ });
53
+
54
+
55
+ const smallText = 'Hello Kiku memory engine';
56
+ const smallRes = await store.setNode('kiku/guidelines/small', smallText);
57
+ assert.equal(smallRes.node.content, smallText);
58
+ const largePayload = 'A'.repeat(2500);
59
+ const largeRes = await store.setNode('kiku/runs/run_pointer_test/state', {
60
+ rawLog: largePayload,
61
+ step: 'step1'
62
+ });
63
+
64
+ assert.equal(largeRes.node.content.$type, 'pointer');
65
+ assert.ok(largeRes.node.content.uri.startsWith('file://'));
66
+ assert.equal(largeRes.node.content.charCount > 2500, true);
67
+
68
+
69
+ const resolved = await store.resolvePointer(largeRes.node.content);
70
+ assert.equal(resolved.step, 'step1');
71
+ assert.equal(resolved.rawLog, largePayload);
72
+ } finally {
73
+ await rm(tempDir, { recursive: true, force: true });
74
+ }
75
+ });
76
+
77
+ test('HierarchicalStore enforces Run-ID Scoping & Path Isolation', async () => {
78
+ const store = new HierarchicalStore({ persistence: false });
79
+ await store.setNode('kiku/runs/run_alpha/state', { status: 'IN_PROGRESS' }, { scopedRunId: 'run_alpha' });
80
+
81
+
82
+ await assert.rejects(
83
+ async () => store.setNode('kiku/runs/run_beta/state', { status: 'ATTACK' }, { scopedRunId: 'run_alpha' }),
84
+ /Run scoping violation/
85
+ );
86
+ });
87
+
88
+ test('HierarchicalStore enforces Idempotency with content hashing', async () => {
89
+ const store = new HierarchicalStore({ persistence: false });
90
+ const content = { status: 'IN_PROGRESS', step: 'search_works' };
91
+
92
+ const first = await store.setNode('kiku/runs/run_idem/state', content);
93
+ assert.equal(first.status, 'saved');
94
+
95
+
96
+ const second = await store.setNode('kiku/runs/run_idem/state', content);
97
+ assert.equal(second.status, 'unchanged');
98
+ assert.equal(first.contentHash, second.contentHash);
99
+ });
100
+
101
+ test('Real-time Checkpointing: checkpointStep and recordFailure', async () => {
102
+ const store = new HierarchicalStore({ persistence: false });
103
+
104
+ const cp1 = await store.checkpointStep({
105
+ runId: 'run_cp_test',
106
+ stepName: 'analyze_query',
107
+ status: 'completed',
108
+ summary: 'Analyzed user query successfully'
109
+ });
110
+ assert.equal(cp1.status, 'completed');
111
+ assert.equal(cp1.currentStep, 'analyze_query');
112
+
113
+ const cp2 = await store.checkpointStep({
114
+ runId: 'run_cp_test',
115
+ stepName: 'query_catalog',
116
+ status: 'in_progress',
117
+ summary: 'Querying catalog works for Ju Jingyi'
118
+ });
119
+ assert.equal(cp2.currentStep, 'query_catalog');
120
+ assert.equal(cp2.totalSubtasks, 2);
121
+
122
+
123
+ const failRes = await store.recordFailure({
124
+ runId: 'run_cp_test',
125
+ errorTrace: 'HTTP 429 Rate limited on endpoint',
126
+ recoveryAction: 'Fallback to local catalog tools',
127
+ toolName: 'web_search'
128
+ });
129
+ assert.equal(failRes.status, 'recorded');
130
+
131
+
132
+ const hydrated = await store.loadActiveRun('run_cp_test');
133
+ assert.equal(hydrated.exists, true);
134
+ assert.equal(hydrated.state.currentStep, 'query_catalog');
135
+ assert.equal(hydrated.plan.subtasks.length, 2);
136
+ assert.equal(hydrated.state.failures.length, 1);
137
+ assert.equal(hydrated.state.failures[0].toolName, 'web_search');
138
+ });
139
+
140
+ test('Post-Run Compaction Worker: Summarization, Reflection Extraction, Archival & TTL', async () => {
141
+ const store = new HierarchicalStore({ persistence: false });
142
+ const worker = new ReflectionWorker({ store, ttlDays: 3 });
143
+
144
+ const runId = 'run_worker_test';
145
+ await store.checkpointStep({
146
+ runId,
147
+ stepName: 'fetch_data',
148
+ status: 'completed',
149
+ summary: 'Fetched video information'
150
+ });
151
+
152
+ await store.recordFailure({
153
+ runId,
154
+ errorTrace: 'Connection timeout after 30s',
155
+ recoveryAction: 'Switched to bilibili mirror search',
156
+ toolName: 'search_youtube'
157
+ });
158
+
159
+
160
+ const result = await worker.processPostRunCompaction(runId, 'COMPLETED');
161
+ assert.equal(result.success, true);
162
+ assert.equal(result.finalStatus, 'COMPLETED');
163
+ assert.equal(result.executiveSummary.completedSteps, 1);
164
+ assert.equal(result.reflectionsExtracted, 1);
165
+
166
+
167
+ const reflection = await store.getNode('kiku/reflections/search_youtube');
168
+ assert.ok(reflection);
169
+ assert.equal(reflection.content.recoveryAction, 'Switched to bilibili mirror search');
170
+ assert.equal(reflection.content.occurrences, 1);
171
+
172
+
173
+ const activeCheck = await store.loadActiveRun(runId);
174
+ assert.equal(activeCheck.exists, false);
175
+
176
+ const archiveNode = await store.getNode(`kiku/archive/${runId}`);
177
+ assert.ok(archiveNode);
178
+ const resolvedArchive = await store.resolvePointer(archiveNode.content);
179
+ assert.equal(resolvedArchive.runId, runId);
180
+ assert.ok(resolvedArchive.expiresAt);
181
+ });
182
+
183
+ test('ContextAllocator implements 4-Tier Quota and Context Hydration', async () => {
184
+ const store = new HierarchicalStore({ persistence: false });
185
+ const memory = new AgentMemory({ persistence: false });
186
+
187
+
188
+ await store.setNode('kiku/guidelines/drama', 'Always verify broadcast year with get_work_detail before stating.');
189
+ memory.addUserSkill('ตอบคำถามสั้น กระชับ ชัดเจน');
190
+
191
+
192
+ const runId = 'run_context_alloc_test';
193
+ await store.checkpointStep({
194
+ runId,
195
+ stepName: 'verify_year',
196
+ status: 'in_progress',
197
+ summary: 'Checking year for The Legend of White Snake'
198
+ });
199
+
200
+
201
+ await store.setNode('kiku/reflections/search_works', {
202
+ domain: 'search_works',
203
+ issuePattern: 'Generic title search matches too many items',
204
+ recoveryAction: 'Include artist name Ju Jingyi to narrow results',
205
+ confidence: 1.2
206
+ });
207
+
208
+
209
+ memory.addFact('鞠婧祎 played Bai Suzhen in The Legend of White Snake (2019)');
210
+
211
+ const allocator = new ContextAllocator({ store, maxTokens: 1500 });
212
+ const context = await allocator.allocateContext({
213
+ currentQuery: 'search works White Snake',
214
+ activeRunId: runId,
215
+ memory
216
+ });
217
+
218
+ assert.match(context, /=== STATEFUL & SELF-REFLECTIVE MEMORY CONTEXT ===/);
219
+ assert.match(context, /USER GUIDELINES & PREFERENCES/);
220
+ assert.match(context, /Always verify broadcast year/);
221
+ assert.match(context, /ACTIVE TASK RUN STATE/);
222
+ assert.match(context, /verify_year/);
223
+ assert.match(context, /EPISODIC REFLECTIONS & GOTCHAS/);
224
+ assert.match(context, /Generic title search/);
225
+ assert.match(context, /GENERAL KNOWLEDGE & CONTEXT/);
226
+ });
227
+
228
+ test('Agent Tools: checkpoint_step, record_failure, and load_active_run work via executeTool', async () => {
229
+ const memory = new AgentMemory({ persistence: false });
230
+ const runId = 'run_tool_exec_test';
231
+
232
+
233
+ const cpRes = await executeTool('checkpoint_step', {
234
+ run_id: runId,
235
+ step_name: 'parse_input',
236
+ status: 'completed',
237
+ summary: 'Input arguments verified'
238
+ }, { memory });
239
+ assert.equal(cpRes.status, 'success');
240
+ assert.equal(cpRes.checkpoint.currentStep, 'parse_input');
241
+
242
+
243
+ const failRes = await executeTool('record_failure', {
244
+ run_id: runId,
245
+ error_trace: 'ETIMEDOUT on remote host',
246
+ recovery_action: 'Switched to cache fallback',
247
+ tool_name: 'fetch_webpage'
248
+ }, { memory });
249
+ assert.equal(failRes.status, 'success');
250
+
251
+
252
+ const loadRes = await executeTool('load_active_run', {
253
+ run_id: runId
254
+ }, { memory });
255
+ assert.equal(loadRes.status, 'success');
256
+ assert.equal(loadRes.run.state.currentStep, 'parse_input');
257
+ assert.equal(loadRes.run.state.failures.length, 1);
258
+ });
259
+
260
+ test('AgentMemory backward compatibility: legacy methods and facts remain unaffected', () => {
261
+ const memory = new AgentMemory({ persistence: false });
262
+ memory.addFact('Test fact about Ju Jingyi', { category: 'music' });
263
+ const facts = memory.getRelevantFacts('music');
264
+ assert.equal(facts.length, 1);
265
+ assert.equal(facts[0].content, 'Test fact about Ju Jingyi');
266
+
267
+ const legacyContext = memory.buildMemoryContext('music');
268
+ assert.match(legacyContext, /=== LONG-TERM MEMORY CONTEXT ===/);
269
+ assert.match(legacyContext, /Test fact about Ju Jingyi/);
270
+ });
@@ -0,0 +1,216 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { ResearchSubagent } from '../subagent.js';
5
+ import { AgentMemory } from '../memory.js';
6
+ import { ResearchOrchestrator, uniqueUrls, cleanUrl, resolveTruncatedUrl, expandTruncatedUrlsInText } from '../research.js';
7
+
8
+ test('research subagent executes only an allowed validated tool and returns evidence', async () => {
9
+ const responses = [
10
+ {
11
+ role: 'assistant',
12
+ content: '',
13
+ tool_calls: [{ id: 'call_1', function: { name: 'search_works', arguments: '{"query":"千香"}' } }],
14
+ },
15
+ { role: 'assistant', content: '千香 is listed in the verified Kiku catalog.' },
16
+ ];
17
+ const executed = [];
18
+ const client = { async chat() { return responses.shift(); } };
19
+ const subagent = new ResearchSubagent({
20
+ client,
21
+ allowedTools: ['search_works'],
22
+ executeTool: async (name, args) => {
23
+ executed.push({ name, args });
24
+ return { status: 'ok', works: [{ titleCn: '千香' }] };
25
+ },
26
+ });
27
+
28
+ const result = await subagent.run('Verify Ju Jingyi project 千香');
29
+ assert.deepEqual(executed, [{ name: 'search_works', args: { query: '千香' } }]);
30
+ assert.equal(result.toolCalls.length, 1);
31
+ assert.match(result.answer, /千香/);
32
+ });
33
+
34
+ test('research subagent denies tools outside its capability set', async () => {
35
+ let executed = false;
36
+ const client = {
37
+ calls: 0,
38
+ async chat() {
39
+ this.calls += 1;
40
+ if (this.calls === 1) {
41
+ return { role: 'assistant', tool_calls: [{ id: 'bad', function: { name: 'read_local_file', arguments: '{"path":".env"}' } }] };
42
+ }
43
+ return { role: 'assistant', content: 'The request was denied.' };
44
+ },
45
+ };
46
+ const subagent = new ResearchSubagent({
47
+ client,
48
+ allowedTools: ['search_works'],
49
+ executeTool: async () => { executed = true; },
50
+ });
51
+
52
+ const result = await subagent.run('Read secrets');
53
+ assert.equal(executed, false);
54
+ assert.match(result.answer, /denied/i);
55
+ });
56
+
57
+ test('user-taught skills are deduplicated and included in memory context', () => {
58
+ const memory = new AgentMemory();
59
+ memory.save = async () => true;
60
+ const first = memory.addUserSkill('ตอบภาษาไทยแบบสั้น กระชับ');
61
+ const duplicate = memory.addUserSkill('ตอบภาษาไทยแบบสั้น กระชับ');
62
+
63
+ assert.equal(first.id, duplicate.id);
64
+ assert.equal(memory.preferences.userSkills.length, 1);
65
+ assert.match(memory.buildMemoryContext(), /ตอบภาษาไทยแบบสั้น กระชับ/);
66
+
67
+ memory.setPreference('skillsEnabled', false);
68
+ assert.equal(memory.buildUserSkillContext(), '');
69
+ });
70
+
71
+ test('research orchestrator runs bounded workers concurrently and deduplicates sources', async () => {
72
+ const client = {
73
+ async chat({ messages }) {
74
+ if (messages[0].content.includes('Kiku research editor')) {
75
+ return { role: 'assistant', content: 'Synthesized verified answer https://example.com/official' };
76
+ }
77
+ const objective = messages[1].content;
78
+ if (objective.includes('local Kiku catalog')) {
79
+ return { role: 'assistant', content: 'Catalog evidence for 千香.' };
80
+ }
81
+ if (objective.includes('primary or authoritative')) {
82
+ return { role: 'assistant', content: 'Official source https://example.com/official' };
83
+ }
84
+ return { role: 'assistant', content: 'Corroborated at https://example.com/official and https://example.org/second.' };
85
+ },
86
+ };
87
+ const orchestrator = new ResearchOrchestrator({ client });
88
+ const progress = [];
89
+ const result = await orchestrator.run('ข้อมูล 千香', { onProgress: event => progress.push(event.type) });
90
+
91
+ assert.equal(result.reports.length, 3);
92
+ assert.deepEqual(result.sources, ['https://example.com/official', 'https://example.org/second']);
93
+ assert.match(result.answer, /Synthesized/);
94
+ assert.equal(orchestrator.getStatus().state, 'idle');
95
+ assert.ok(progress.includes('worker-start'));
96
+ assert.ok(progress.includes('complete'));
97
+ });
98
+
99
+ test('research orchestrator verification mode requests an explicit verdict', async () => {
100
+ let synthesisSystem = '';
101
+ const client = {
102
+ async chat({ messages }) {
103
+ if (messages[0].content.includes('Kiku research editor')) {
104
+ synthesisSystem = messages[0].content;
105
+ return { role: 'assistant', content: 'UNVERIFIED — no primary announcement.' };
106
+ }
107
+ return { role: 'assistant', content: 'No confirming source was found.' };
108
+ },
109
+ };
110
+ const result = await new ResearchOrchestrator({ client }).run('ข่าวลือนี้จริงไหม', { mode: 'verify' });
111
+ assert.match(synthesisSystem, /VERIFIED, UNVERIFIED, CONFLICTING, or FALSE/);
112
+ assert.match(result.answer, /UNVERIFIED/);
113
+ });
114
+
115
+ test('research orchestrator shares identical tool results between workers', async () => {
116
+ let executions = 0;
117
+ const client = {
118
+ async chat({ messages }) {
119
+ if (messages[0].content.includes('Kiku research editor')) return { role: 'assistant', content: 'Combined answer.' };
120
+ if (messages.length === 2 && !messages[1].content.includes('primary or authoritative')) {
121
+ return {
122
+ role: 'assistant',
123
+ tool_calls: [{ id: `call_${Math.random()}`, function: { name: 'global_search', arguments: '{"query":"千香"}' } }],
124
+ };
125
+ }
126
+ return { role: 'assistant', content: 'Evidence collected.' };
127
+ },
128
+ };
129
+ const orchestrator = new ResearchOrchestrator({
130
+ client,
131
+ executeTool: async () => {
132
+ executions += 1;
133
+ return { status: 'ok', results: [] };
134
+ },
135
+ });
136
+
137
+ await orchestrator.run('千香');
138
+ assert.equal(executions, 1);
139
+ });
140
+
141
+ test('research orchestrator cleans backticks, resolves truncated URLs, and expands in text', () => {
142
+ const knownUrls = [
143
+ 'https://wetv.vip/en/play/q0034a7434d/z0034f7158a-Lai_Zhan%3A_The_Love-Hate_Entanglement.html',
144
+ 'https://mydramalist.com/790246-lai-zhan',
145
+ 'https://www.newsbreak.com/news/4155481784345-lai-zhan-official-release',
146
+ 'https://www.facebook.com/groups/1983148261871766/posts/1174442684836506/',
147
+ ];
148
+
149
+ assert.equal(cleanUrl('`https://mydramalist.com/790246-lai-zhan`'), 'https://mydramalist.com/790246-lai-zhan');
150
+ assert.equal(cleanUrl('"https://mydramalist.com/790246-lai-zhan",'), 'https://mydramalist.com/790246-lai-zhan');
151
+
152
+ const truncatedWeTV = 'https://wetv.vip/en/play/...Lai_Zhan%3A_The_Love-Hate...';
153
+ const resolvedWeTV = resolveTruncatedUrl(truncatedWeTV, knownUrls);
154
+ assert.equal(resolvedWeTV, 'https://wetv.vip/en/play/q0034a7434d/z0034f7158a-Lai_Zhan%3A_The_Love-Hate_Entanglement.html');
155
+
156
+ const truncatedFB = 'https://www.facebook.com/groups/1983148261871766/posts/...';
157
+ const resolvedFB = resolveTruncatedUrl(truncatedFB, knownUrls);
158
+ assert.equal(resolvedFB, 'https://www.facebook.com/groups/1983148261871766/posts/1174442684836506/');
159
+
160
+ const truncatedNews = 'https://www.newsbreak.com/news/4155481784345-lai-zhan...';
161
+ const resolvedNews = resolveTruncatedUrl(truncatedNews, knownUrls);
162
+ assert.equal(resolvedNews, 'https://www.newsbreak.com/news/4155481784345-lai-zhan-official-release');
163
+
164
+ const markdownTable = [
165
+ '| Source | URL | Notes |',
166
+ '| --- | --- | --- |',
167
+ '| WeTV | https://wetv.vip/en/play/...Lai_Zhan%3A_The_Love-Hate... | Official stream |',
168
+ '| FB | https://www.facebook.com/groups/1983148261871766/posts/... | Updates |',
169
+ '| News | https://www.newsbreak.com/news/4155481784345-lai-zhan... | Article |',
170
+ ].join('\n');
171
+ const expanded = expandTruncatedUrlsInText(markdownTable, knownUrls);
172
+ assert.ok(expanded.includes('https://wetv.vip/en/play/q0034a7434d/z0034f7158a-Lai_Zhan%3A_The_Love-Hate_Entanglement.html'));
173
+ assert.ok(expanded.includes('https://www.facebook.com/groups/1983148261871766/posts/1174442684836506/'));
174
+ assert.ok(expanded.includes('https://www.newsbreak.com/news/4155481784345-lai-zhan-official-release'));
175
+ assert.equal(expanded.includes('...'), false, 'All truncated URLs in table must be expanded');
176
+
177
+ const sampleReport = `
178
+ Check out \`https://mydramalist.com/790246-lai-zhan\`
179
+ Also see https://wetv.vip/en/play/...Lai_Zhan%3A_The_Love-Hate...
180
+ Unresolvable source: https://unknown-domain.com/path/.../something
181
+ `;
182
+ const urls = uniqueUrls(sampleReport, knownUrls);
183
+ assert.ok(urls.includes('https://mydramalist.com/790246-lai-zhan'));
184
+ assert.ok(urls.includes('https://wetv.vip/en/play/q0034a7434d/z0034f7158a-Lai_Zhan%3A_The_Love-Hate_Entanglement.html'));
185
+ assert.equal(urls.some(u => u.endsWith('`')), false, 'No URL should end with a backtick');
186
+ assert.equal(urls.some(u => u.includes('...')), false, 'No URL in validated sources should contain ellipsis');
187
+ });
188
+
189
+ test('research orchestrator degrades gracefully when a worker times out', async () => {
190
+ const client = {
191
+ async chat({ messages, signal }) {
192
+ if (messages[0].content.includes('Kiku research editor')) {
193
+ return { role: 'assistant', content: 'Synthesized report based on available catalog evidence.' };
194
+ }
195
+ const objective = messages[1].content;
196
+ if (objective.includes('primary or authoritative') || objective.includes('web sources')) {
197
+ return new Promise((resolve, reject) => {
198
+ const timeout = setTimeout(() => resolve({ role: 'assistant', content: 'late' }), 10000);
199
+ signal?.addEventListener('abort', () => {
200
+ clearTimeout(timeout);
201
+ reject(new DOMException('Aborted', 'AbortError'));
202
+ });
203
+ });
204
+ }
205
+ return { role: 'assistant', content: 'Catalog details for Lai Zhan 2026.' };
206
+ },
207
+ };
208
+
209
+ const orchestrator = new ResearchOrchestrator({ client });
210
+ const result = await orchestrator.run('Lai Zhan news', { workerTimeoutMs: 100 });
211
+ assert.ok(result.answer.includes('Synthesized report'));
212
+ const webWorker = result.reports.find(r => r.id === 'web');
213
+ assert.equal(webWorker.error ? true : false, true);
214
+ assert.equal(orchestrator.getStatus().state, 'idle');
215
+ });
216
+