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.
- package/README.md +69 -12
- package/package.json +5 -12
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +288 -0
- package/server/agent/eval/modules/evaluation/scoring.js +363 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4799 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +253 -0
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +296 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +627 -0
- package/server/agent/tests/cliAutomation.test.js +158 -0
- package/server/agent/tests/cloudProviders.test.js +322 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +506 -0
- package/server/agent/tests/contextIntelligence.test.js +209 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +284 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +320 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +194 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +324 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +141 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +101 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +44 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +221 -0
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +267 -0
- package/server/agent/tests/sidebarControls.test.js +135 -0
- package/server/agent/tests/statefulMemory.test.js +277 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +197 -0
- package/server/agent/tests/textToolParsing.test.js +187 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +159 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +595 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +551 -0
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +284 -0
- package/server/agent/tests/webSources.test.js +281 -0
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +162 -0
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +63 -0
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { AgentSession } from '../session.js';
|
|
5
|
+
import { parseDetailsRequest, getCopyableContent } from '../queryDetails.js';
|
|
6
|
+
import { savePersistedProviderKey, getPersistedProviderKeys, CLOUD_PROVIDERS } from '../config.js';
|
|
7
|
+
import { LLMClient } from '../llmClient.js';
|
|
8
|
+
import { routeCommand } from '../commands.js';
|
|
9
|
+
|
|
10
|
+
test('AgentSession records full conversation metadata, tools, reasoning, and exports', async () => {
|
|
11
|
+
const session = new AgentSession({ persistence: false });
|
|
12
|
+
|
|
13
|
+
const userMsg = session.createUserMessage('Who is Ju Jingyi?', { queryNumber: 1 });
|
|
14
|
+
assert.equal(userMsg.role, 'user');
|
|
15
|
+
assert.equal(userMsg.queryNumber, 1);
|
|
16
|
+
assert.equal(userMsg.parts[0].content, 'Who is Ju Jingyi?');
|
|
17
|
+
|
|
18
|
+
const assistantMsg = session.createAssistantMessage({
|
|
19
|
+
model: 'openai/gpt-oss-120b',
|
|
20
|
+
provider: 'Groq Cloud',
|
|
21
|
+
queryNumber: 1,
|
|
22
|
+
});
|
|
23
|
+
assert.equal(assistantMsg.role, 'assistant');
|
|
24
|
+
assert.equal(assistantMsg.model, 'openai/gpt-oss-120b');
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
session.addReasoningPart(assistantMsg, 'Analyzing query about Ju Jingyi profile...');
|
|
28
|
+
session.finalizeReasoningPart(assistantMsg);
|
|
29
|
+
assert.equal(assistantMsg.parts[0].type, 'reasoning');
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const toolPart = session.addToolPart(assistantMsg, 'get_profile_and_fandom', {});
|
|
33
|
+
session.finalizeToolPart(assistantMsg, toolPart.id, {
|
|
34
|
+
status: 'ok',
|
|
35
|
+
profile: { name: 'Ju Jingyi', fandom: 'Miju' },
|
|
36
|
+
});
|
|
37
|
+
assert.equal(assistantMsg.parts[1].type, 'tool');
|
|
38
|
+
assert.equal(assistantMsg.parts[1].status, 'ok');
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
session.addTextPart(assistantMsg, 'Ju Jingyi is a talented Chinese singer and actress.');
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
const stats = session.getToolStats();
|
|
45
|
+
assert.ok(stats.get_profile_and_fandom);
|
|
46
|
+
assert.equal(stats.get_profile_and_fandom.count, 1);
|
|
47
|
+
assert.equal(stats.get_profile_and_fandom.successCount, 1);
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const md = session.getTranscriptMarkdown();
|
|
51
|
+
assert.ok(md.includes('# Kiku AI Agent — Session Transcript'));
|
|
52
|
+
assert.ok(md.includes('get_profile_and_fandom'));
|
|
53
|
+
assert.ok(md.includes('Ju Jingyi is a talented Chinese singer'));
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
const jsonStr = session.getTranscriptJson();
|
|
57
|
+
const parsedJson = JSON.parse(jsonStr);
|
|
58
|
+
assert.equal(parsedJson.id, session.id);
|
|
59
|
+
assert.equal(parsedJson.messages.length, 2);
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
const textTranscript = session.getTranscriptText();
|
|
63
|
+
assert.ok(textTranscript.includes('Who is Ju Jingyi?'));
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
const expMd = await session.exportToMarkdown();
|
|
67
|
+
assert.equal(expMd.format, 'markdown');
|
|
68
|
+
assert.ok(expMd.filePath.endsWith('.md'));
|
|
69
|
+
|
|
70
|
+
const expJson = await session.exportToJson();
|
|
71
|
+
assert.equal(expJson.format, 'json');
|
|
72
|
+
assert.ok(expJson.filePath.endsWith('.json'));
|
|
73
|
+
|
|
74
|
+
const expTxt = await session.exportToText();
|
|
75
|
+
assert.equal(expTxt.format, 'text');
|
|
76
|
+
assert.ok(expTxt.filePath.endsWith('.txt'));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('Isolated Provider Keys persist and do not overwrite each other', () => {
|
|
80
|
+
|
|
81
|
+
savePersistedProviderKey('groq', 'fixture-groq-key');
|
|
82
|
+
assert.equal(CLOUD_PROVIDERS.groq.apiKey, 'fixture-groq-key');
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
savePersistedProviderKey('openrouter', 'fixture-openrouter-key');
|
|
86
|
+
assert.equal(CLOUD_PROVIDERS.openrouter.apiKey, 'fixture-openrouter-key');
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
assert.equal(CLOUD_PROVIDERS.groq.apiKey, 'fixture-groq-key');
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
const keys = getPersistedProviderKeys();
|
|
93
|
+
assert.equal(keys.groq, 'fixture-groq-key');
|
|
94
|
+
assert.equal(keys.openrouter, 'fixture-openrouter-key');
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
const client = new LLMClient({ provider: 'groq' });
|
|
98
|
+
client.setApiKey('sk-openai-custom-key', 'openai');
|
|
99
|
+
assert.equal(CLOUD_PROVIDERS.openai.apiKey, 'sk-openai-custom-key');
|
|
100
|
+
assert.equal(client.apiKey, 'fixture-groq-key');
|
|
101
|
+
|
|
102
|
+
savePersistedProviderKey('groq', '');
|
|
103
|
+
savePersistedProviderKey('openrouter', '');
|
|
104
|
+
savePersistedProviderKey('openai', '');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('queryDetails parses session and granular query copy requests', () => {
|
|
108
|
+
const reqSession = parseDetailsRequest('session');
|
|
109
|
+
assert.equal(reqSession.isSession, true);
|
|
110
|
+
assert.equal(reqSession.view, 'markdown');
|
|
111
|
+
|
|
112
|
+
const reqSessionJson = parseDetailsRequest('session json');
|
|
113
|
+
assert.equal(reqSessionJson.isSession, true);
|
|
114
|
+
assert.equal(reqSessionJson.view, 'json');
|
|
115
|
+
|
|
116
|
+
const reqQ1All = parseDetailsRequest('Q1 all');
|
|
117
|
+
assert.equal(reqQ1All.queryNumber, 1);
|
|
118
|
+
assert.equal(reqQ1All.view, 'all');
|
|
119
|
+
|
|
120
|
+
const reqQ1Tool = parseDetailsRequest('Q1 tool 2');
|
|
121
|
+
assert.equal(reqQ1Tool.queryNumber, 1);
|
|
122
|
+
assert.equal(reqQ1Tool.view, 'tool');
|
|
123
|
+
assert.equal(reqQ1Tool.toolIndex, 2);
|
|
124
|
+
|
|
125
|
+
const session = new AgentSession({ persistence: false });
|
|
126
|
+
session.createUserMessage('Test question');
|
|
127
|
+
const asst = session.createAssistantMessage({ model: 'gemma-4' });
|
|
128
|
+
session.addTextPart(asst, 'Test answer');
|
|
129
|
+
|
|
130
|
+
const copyableSession = getCopyableContent(null, reqSession, session);
|
|
131
|
+
assert.ok(copyableSession.text.includes('# Kiku AI Agent — Session Transcript'));
|
|
132
|
+
assert.ok(copyableSession.text.includes('Test question'));
|
|
133
|
+
|
|
134
|
+
const copyableJson = getCopyableContent(null, reqSessionJson, session);
|
|
135
|
+
const parsed = JSON.parse(copyableJson.text);
|
|
136
|
+
assert.equal(parsed.id, session.id);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('Commands /session export and /session clear execute cleanly', async () => {
|
|
140
|
+
let ended = false;
|
|
141
|
+
const mockAgent = {
|
|
142
|
+
session: new AgentSession({ persistence: false }),
|
|
143
|
+
endSession: async () => {
|
|
144
|
+
ended = true;
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
147
|
+
memory: {
|
|
148
|
+
getArchivedSessions: () => [],
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
mockAgent.session.createUserMessage('Sample query');
|
|
153
|
+
|
|
154
|
+
const ctxExport = {
|
|
155
|
+
agent: mockAgent,
|
|
156
|
+
sub: 'export',
|
|
157
|
+
extra: 'markdown',
|
|
158
|
+
};
|
|
159
|
+
const resExport = await routeCommand('/session export markdown', ctxExport);
|
|
160
|
+
assert.equal(resExport, 'HANDLED');
|
|
161
|
+
|
|
162
|
+
const ctxClear = {
|
|
163
|
+
agent: mockAgent,
|
|
164
|
+
sub: 'clear',
|
|
165
|
+
extra: '',
|
|
166
|
+
};
|
|
167
|
+
const resClear = await routeCommand('/session clear', ctxClear);
|
|
168
|
+
assert.equal(resClear, 'HANDLED');
|
|
169
|
+
assert.equal(ended, true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('Commands /copy routes answer, thinking, tools, all, and session variants', async () => {
|
|
173
|
+
let copiedText = '';
|
|
174
|
+
const mockDetails = {
|
|
175
|
+
number: 1,
|
|
176
|
+
prompt: 'Who is Ju Jingyi?',
|
|
177
|
+
answer: 'Ju Jingyi is a singer and actress.',
|
|
178
|
+
thinking: 'Querying catalog profile.',
|
|
179
|
+
tools: [{ name: 'get_profile', duration: 15, args: { query: 'Ju Jingyi' }, result: { count: 1 } }],
|
|
180
|
+
status: 'complete',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const session = new AgentSession({ persistence: false });
|
|
184
|
+
session.createUserMessage('Who is Ju Jingyi?');
|
|
185
|
+
const asst = session.createAssistantMessage({ model: 'gemma-4' });
|
|
186
|
+
session.addTextPart(asst, 'Ju Jingyi is a singer and actress.');
|
|
187
|
+
|
|
188
|
+
const ctx = {
|
|
189
|
+
agent: {
|
|
190
|
+
session,
|
|
191
|
+
lastQueryDetails: mockDetails,
|
|
192
|
+
},
|
|
193
|
+
copyQueryDetails: (ref) => {
|
|
194
|
+
const req = parseDetailsRequest(ref);
|
|
195
|
+
const copyable = getCopyableContent(mockDetails, req, session);
|
|
196
|
+
copiedText = copyable?.text || '';
|
|
197
|
+
return Boolean(copyable?.text);
|
|
198
|
+
},
|
|
199
|
+
sub: '',
|
|
200
|
+
extra: '',
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
await routeCommand('/copy', ctx);
|
|
205
|
+
assert.equal(copiedText, 'Ju Jingyi is a singer and actress.');
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
await routeCommand('/copy thinking', { ...ctx, sub: 'thinking', extra: '' });
|
|
209
|
+
assert.equal(copiedText, 'Querying catalog profile.');
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
await routeCommand('/copy think', { ...ctx, sub: 'think', extra: '' });
|
|
213
|
+
assert.equal(copiedText, 'Querying catalog profile.');
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
await routeCommand('/copy tools', { ...ctx, sub: 'tools', extra: '' });
|
|
217
|
+
assert.ok(copiedText.includes('get_profile'));
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
await routeCommand('/copy tool 1', { ...ctx, sub: 'tool', extra: '1' });
|
|
221
|
+
assert.ok(copiedText.includes('get_profile'));
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
await routeCommand('/copy all', { ...ctx, sub: 'all', extra: '' });
|
|
225
|
+
assert.ok(copiedText.includes('# Question 1'));
|
|
226
|
+
assert.ok(copiedText.includes('Ju Jingyi is a singer and actress.'));
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
await routeCommand('/copy session markdown', { ...ctx, sub: 'session', extra: 'markdown' });
|
|
230
|
+
assert.ok(copiedText.includes('# Kiku AI Agent — Session Transcript'));
|
|
231
|
+
assert.ok(copiedText.includes('Who is Ju Jingyi?'));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('AgentSession synchronizes totalToolCalls and /session info from message tool parts', async () => {
|
|
235
|
+
const session = new AgentSession({ persistence: false });
|
|
236
|
+
session.restoreFromSnapshot({
|
|
237
|
+
id: 'sess_test_sync',
|
|
238
|
+
createdAt: new Date(),
|
|
239
|
+
messages: [
|
|
240
|
+
{ role: 'user', parts: [{ type: 'text', content: 'hello' }] },
|
|
241
|
+
{
|
|
242
|
+
role: 'assistant',
|
|
243
|
+
parts: [
|
|
244
|
+
{ type: 'tool', name: 'search_works', args: { query: 'In Blossom' }, status: 'ok', durationMs: 40 },
|
|
245
|
+
{ type: 'text', content: 'In Blossom info' }
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
],
|
|
249
|
+
stats: {
|
|
250
|
+
totalQueries: 1,
|
|
251
|
+
totalToolCalls: 0,
|
|
252
|
+
totalDurationMs: 0
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
assert.equal(session.stats.totalToolCalls, 1);
|
|
257
|
+
assert.equal(session.getTotalToolCalls(), 1);
|
|
258
|
+
assert.equal(session.stats.totalDurationMs, 40);
|
|
259
|
+
|
|
260
|
+
const toolStats = session.getToolStats();
|
|
261
|
+
assert.equal(toolStats.search_works.count, 1);
|
|
262
|
+
|
|
263
|
+
const mockAgent = { session };
|
|
264
|
+
const res = await routeCommand('/session info', { agent: mockAgent, sub: 'info', extra: '' });
|
|
265
|
+
assert.equal(res, 'HANDLED');
|
|
266
|
+
});
|
|
267
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
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('quick-action-menu jukebox entry opens through a single-call wrapper', () => {
|
|
88
|
+
const html = getWebChatHtml();
|
|
89
|
+
|
|
90
|
+
assert.match(html, /function openMusicFromQuickActions\(\)/);
|
|
91
|
+
const wrapperStart = html.indexOf('function openMusicFromQuickActions()');
|
|
92
|
+
const wrapperBody = html.slice(wrapperStart, wrapperStart + 300);
|
|
93
|
+
assert.match(wrapperBody, /closeQuickActions\(\)/);
|
|
94
|
+
assert.match(wrapperBody, /openMusicModal\(\)/);
|
|
95
|
+
assert.ok(wrapperBody.indexOf('closeQuickActions()') < wrapperBody.indexOf('openMusicModal()'));
|
|
96
|
+
|
|
97
|
+
assert.match(html, /onclick="openMusicFromQuickActions\(\)"/);
|
|
98
|
+
assert.doesNotMatch(html, /onclick="openMusicModal\(\); closeQuickActions\(\)"/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('every static onclick handler in the chat shell is exposed on window', () => {
|
|
102
|
+
const html = getWebChatHtml();
|
|
103
|
+
|
|
104
|
+
for (const name of [
|
|
105
|
+
'openMusicModal',
|
|
106
|
+
'openMusicFromQuickActions',
|
|
107
|
+
'closeMusicModal',
|
|
108
|
+
'toggleAppSidebar',
|
|
109
|
+
'toggleQuickActions',
|
|
110
|
+
'closeQuickActions',
|
|
111
|
+
'openMiniPlayerFromCard',
|
|
112
|
+
'switchVoiceToDetectedLang',
|
|
113
|
+
'handleQuickChipClick',
|
|
114
|
+
'cycleSidebarAd'
|
|
115
|
+
]) {
|
|
116
|
+
assert.match(html, new RegExp('function ' + name + '\\('), name + ' is defined');
|
|
117
|
+
assert.match(html, new RegExp('\\b' + name + ','), name + ' is exposed on window');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('sidebar export downloads through the export API and reports failures', () => {
|
|
122
|
+
const html = getWebChatHtml();
|
|
123
|
+
const start = html.indexOf('async function exportChat()');
|
|
124
|
+
const end = html.indexOf('function shareChatLink()', start);
|
|
125
|
+
const exportFunction = html.slice(start, end);
|
|
126
|
+
|
|
127
|
+
assert.ok(start >= 0);
|
|
128
|
+
assert.match(exportFunction, /fetch\('\/api\/export'/);
|
|
129
|
+
assert.match(exportFunction, /if \(!response\.ok\) throw new Error/);
|
|
130
|
+
assert.match(exportFunction, /link\.download = filename/);
|
|
131
|
+
assert.doesNotMatch(exportFunction, /showToast/);
|
|
132
|
+
assert.doesNotMatch(exportFunction, /navigator\.clipboard/);
|
|
133
|
+
assert.match(html, /title="ส่งออกบทสนทนาเป็นไฟล์ Markdown"/);
|
|
134
|
+
assert.doesNotMatch(html, /Export \/ Copy Chat/);
|
|
135
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
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, extractWordTokens } 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('extractWordTokens segments Thai and CJK text without whitespace', () => {
|
|
14
|
+
const thaiTokens = extractWordTokens('จวีจิ้งอีผลงานเพลง');
|
|
15
|
+
const cjkTokens = extractWordTokens('鞠婧祎音乐作品');
|
|
16
|
+
assert.ok(thaiTokens.length >= 2, `expected Thai segmentation, got ${thaiTokens.join('|')}`);
|
|
17
|
+
assert.ok(cjkTokens.length >= 2, `expected CJK segmentation, got ${cjkTokens.join('|')}`);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('parseAndValidatePath correctly parses valid hierarchical memory paths', () => {
|
|
21
|
+
const p1 = parseAndValidatePath('kiku/runs/run_task_001/state');
|
|
22
|
+
assert.equal(p1.section, 'runs');
|
|
23
|
+
assert.equal(p1.runId, 'run_task_001');
|
|
24
|
+
assert.equal(p1.subpath, 'state');
|
|
25
|
+
|
|
26
|
+
const p2 = parseAndValidatePath('kiku/runs/run_task_001/plan');
|
|
27
|
+
assert.equal(p2.subpath, 'plan');
|
|
28
|
+
|
|
29
|
+
const p3 = parseAndValidatePath('kiku/runs/run_task_001/artifacts');
|
|
30
|
+
assert.equal(p3.subpath, 'artifacts');
|
|
31
|
+
|
|
32
|
+
const p4 = parseAndValidatePath('kiku/reflections/web_search');
|
|
33
|
+
assert.equal(p4.section, 'reflections');
|
|
34
|
+
assert.equal(p4.domain, 'web_search');
|
|
35
|
+
|
|
36
|
+
const p5 = parseAndValidatePath('kiku/guidelines/drama');
|
|
37
|
+
assert.equal(p5.section, 'guidelines');
|
|
38
|
+
assert.equal(p5.domain, 'drama');
|
|
39
|
+
|
|
40
|
+
const p6 = parseAndValidatePath('kiku/archive/run_task_001');
|
|
41
|
+
assert.equal(p6.section, 'archive');
|
|
42
|
+
assert.equal(p6.runId, 'run_task_001');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('parseAndValidatePath rejects invalid paths, traversal, and invalid IDs', () => {
|
|
46
|
+
assert.throws(() => parseAndValidatePath('../kiku/runs/run_1/state'), /Invalid path traversal/);
|
|
47
|
+
assert.throws(() => parseAndValidatePath('invalid/runs/run_1/state'), /Path must start with "kiku\/"/);
|
|
48
|
+
assert.throws(() => parseAndValidatePath('kiku/unknown/section'), /Unrecognized memory section/);
|
|
49
|
+
assert.throws(() => parseAndValidatePath('kiku/runs/!bad@id/state'), /Invalid run_id/);
|
|
50
|
+
assert.throws(() => parseAndValidatePath('kiku/runs/run_1/bad_subpath'), /Invalid run subpath/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('HierarchicalStore enforces Pointer-Over-Data Rule (>1000 characters)', async () => {
|
|
54
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kiku-hstore-'));
|
|
55
|
+
try {
|
|
56
|
+
const store = new HierarchicalStore({
|
|
57
|
+
baseDir: path.join(tempDir, 'nodes'),
|
|
58
|
+
artifactsDir: path.join(tempDir, 'artifacts')
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
const smallText = 'Hello Kiku memory engine';
|
|
63
|
+
const smallRes = await store.setNode('kiku/guidelines/small', smallText);
|
|
64
|
+
assert.equal(smallRes.node.content, smallText);
|
|
65
|
+
const largePayload = 'A'.repeat(2500);
|
|
66
|
+
const largeRes = await store.setNode('kiku/runs/run_pointer_test/state', {
|
|
67
|
+
rawLog: largePayload,
|
|
68
|
+
step: 'step1'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.equal(largeRes.node.content.$type, 'pointer');
|
|
72
|
+
assert.ok(largeRes.node.content.uri.startsWith('file://'));
|
|
73
|
+
assert.equal(largeRes.node.content.charCount > 2500, true);
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
const resolved = await store.resolvePointer(largeRes.node.content);
|
|
77
|
+
assert.equal(resolved.step, 'step1');
|
|
78
|
+
assert.equal(resolved.rawLog, largePayload);
|
|
79
|
+
} finally {
|
|
80
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('HierarchicalStore enforces Run-ID Scoping & Path Isolation', async () => {
|
|
85
|
+
const store = new HierarchicalStore({ persistence: false });
|
|
86
|
+
await store.setNode('kiku/runs/run_alpha/state', { status: 'IN_PROGRESS' }, { scopedRunId: 'run_alpha' });
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
await assert.rejects(
|
|
90
|
+
async () => store.setNode('kiku/runs/run_beta/state', { status: 'ATTACK' }, { scopedRunId: 'run_alpha' }),
|
|
91
|
+
/Run scoping violation/
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('HierarchicalStore enforces Idempotency with content hashing', async () => {
|
|
96
|
+
const store = new HierarchicalStore({ persistence: false });
|
|
97
|
+
const content = { status: 'IN_PROGRESS', step: 'search_works' };
|
|
98
|
+
|
|
99
|
+
const first = await store.setNode('kiku/runs/run_idem/state', content);
|
|
100
|
+
assert.equal(first.status, 'saved');
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
const second = await store.setNode('kiku/runs/run_idem/state', content);
|
|
104
|
+
assert.equal(second.status, 'unchanged');
|
|
105
|
+
assert.equal(first.contentHash, second.contentHash);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('Real-time Checkpointing: checkpointStep and recordFailure', async () => {
|
|
109
|
+
const store = new HierarchicalStore({ persistence: false });
|
|
110
|
+
|
|
111
|
+
const cp1 = await store.checkpointStep({
|
|
112
|
+
runId: 'run_cp_test',
|
|
113
|
+
stepName: 'analyze_query',
|
|
114
|
+
status: 'completed',
|
|
115
|
+
summary: 'Analyzed user query successfully'
|
|
116
|
+
});
|
|
117
|
+
assert.equal(cp1.status, 'completed');
|
|
118
|
+
assert.equal(cp1.currentStep, 'analyze_query');
|
|
119
|
+
|
|
120
|
+
const cp2 = await store.checkpointStep({
|
|
121
|
+
runId: 'run_cp_test',
|
|
122
|
+
stepName: 'query_catalog',
|
|
123
|
+
status: 'in_progress',
|
|
124
|
+
summary: 'Querying catalog works for Ju Jingyi'
|
|
125
|
+
});
|
|
126
|
+
assert.equal(cp2.currentStep, 'query_catalog');
|
|
127
|
+
assert.equal(cp2.totalSubtasks, 2);
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
const failRes = await store.recordFailure({
|
|
131
|
+
runId: 'run_cp_test',
|
|
132
|
+
errorTrace: 'HTTP 429 Rate limited on endpoint',
|
|
133
|
+
recoveryAction: 'Fallback to local catalog tools',
|
|
134
|
+
toolName: 'web_search'
|
|
135
|
+
});
|
|
136
|
+
assert.equal(failRes.status, 'recorded');
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
const hydrated = await store.loadActiveRun('run_cp_test');
|
|
140
|
+
assert.equal(hydrated.exists, true);
|
|
141
|
+
assert.equal(hydrated.state.currentStep, 'query_catalog');
|
|
142
|
+
assert.equal(hydrated.plan.subtasks.length, 2);
|
|
143
|
+
assert.equal(hydrated.state.failures.length, 1);
|
|
144
|
+
assert.equal(hydrated.state.failures[0].toolName, 'web_search');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('Post-Run Compaction Worker: Summarization, Reflection Extraction, Archival & TTL', async () => {
|
|
148
|
+
const store = new HierarchicalStore({ persistence: false });
|
|
149
|
+
const worker = new ReflectionWorker({ store, ttlDays: 3 });
|
|
150
|
+
|
|
151
|
+
const runId = 'run_worker_test';
|
|
152
|
+
await store.checkpointStep({
|
|
153
|
+
runId,
|
|
154
|
+
stepName: 'fetch_data',
|
|
155
|
+
status: 'completed',
|
|
156
|
+
summary: 'Fetched video information'
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await store.recordFailure({
|
|
160
|
+
runId,
|
|
161
|
+
errorTrace: 'Connection timeout after 30s',
|
|
162
|
+
recoveryAction: 'Switched to bilibili mirror search',
|
|
163
|
+
toolName: 'search_youtube'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
const result = await worker.processPostRunCompaction(runId, 'COMPLETED');
|
|
168
|
+
assert.equal(result.success, true);
|
|
169
|
+
assert.equal(result.finalStatus, 'COMPLETED');
|
|
170
|
+
assert.equal(result.executiveSummary.completedSteps, 1);
|
|
171
|
+
assert.equal(result.reflectionsExtracted, 1);
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
const reflection = await store.getNode('kiku/reflections/search_youtube');
|
|
175
|
+
assert.ok(reflection);
|
|
176
|
+
assert.equal(reflection.content.recoveryAction, 'Switched to bilibili mirror search');
|
|
177
|
+
assert.equal(reflection.content.occurrences, 1);
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
const activeCheck = await store.loadActiveRun(runId);
|
|
181
|
+
assert.equal(activeCheck.exists, false);
|
|
182
|
+
|
|
183
|
+
const archiveNode = await store.getNode(`kiku/archive/${runId}`);
|
|
184
|
+
assert.ok(archiveNode);
|
|
185
|
+
const resolvedArchive = await store.resolvePointer(archiveNode.content);
|
|
186
|
+
assert.equal(resolvedArchive.runId, runId);
|
|
187
|
+
assert.ok(resolvedArchive.expiresAt);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('ContextAllocator implements 4-Tier Quota and Context Hydration', async () => {
|
|
191
|
+
const store = new HierarchicalStore({ persistence: false });
|
|
192
|
+
const memory = new AgentMemory({ persistence: false });
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
await store.setNode('kiku/guidelines/drama', 'Always verify broadcast year with get_work_detail before stating.');
|
|
196
|
+
memory.addUserSkill('ตอบคำถามสั้น กระชับ ชัดเจน');
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
const runId = 'run_context_alloc_test';
|
|
200
|
+
await store.checkpointStep({
|
|
201
|
+
runId,
|
|
202
|
+
stepName: 'verify_year',
|
|
203
|
+
status: 'in_progress',
|
|
204
|
+
summary: 'Checking year for The Legend of White Snake'
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
await store.setNode('kiku/reflections/search_works', {
|
|
209
|
+
domain: 'search_works',
|
|
210
|
+
issuePattern: 'Generic title search matches too many items',
|
|
211
|
+
recoveryAction: 'Include artist name Ju Jingyi to narrow results',
|
|
212
|
+
confidence: 1.2
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
memory.addFact('鞠婧祎 played Bai Suzhen in The Legend of White Snake (2019)');
|
|
217
|
+
|
|
218
|
+
const allocator = new ContextAllocator({ store, maxTokens: 1500 });
|
|
219
|
+
const context = await allocator.allocateContext({
|
|
220
|
+
currentQuery: 'search works White Snake',
|
|
221
|
+
activeRunId: runId,
|
|
222
|
+
memory
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
assert.match(context, /=== STATEFUL & SELF-REFLECTIVE MEMORY CONTEXT ===/);
|
|
226
|
+
assert.match(context, /USER GUIDELINES & PREFERENCES/);
|
|
227
|
+
assert.match(context, /Always verify broadcast year/);
|
|
228
|
+
assert.match(context, /ACTIVE TASK RUN STATE/);
|
|
229
|
+
assert.match(context, /verify_year/);
|
|
230
|
+
assert.match(context, /EPISODIC REFLECTIONS & GOTCHAS/);
|
|
231
|
+
assert.match(context, /Generic title search/);
|
|
232
|
+
assert.match(context, /GENERAL KNOWLEDGE & CONTEXT/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('Agent Tools: checkpoint_step, record_failure, and load_active_run work via executeTool', async () => {
|
|
236
|
+
const memory = new AgentMemory({ persistence: false });
|
|
237
|
+
const runId = 'run_tool_exec_test';
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
const cpRes = await executeTool('checkpoint_step', {
|
|
241
|
+
run_id: runId,
|
|
242
|
+
step_name: 'parse_input',
|
|
243
|
+
status: 'completed',
|
|
244
|
+
summary: 'Input arguments verified'
|
|
245
|
+
}, { memory });
|
|
246
|
+
assert.equal(cpRes.status, 'success');
|
|
247
|
+
assert.equal(cpRes.checkpoint.currentStep, 'parse_input');
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
const failRes = await executeTool('record_failure', {
|
|
251
|
+
run_id: runId,
|
|
252
|
+
error_trace: 'ETIMEDOUT on remote host',
|
|
253
|
+
recovery_action: 'Switched to cache fallback',
|
|
254
|
+
tool_name: 'fetch_webpage'
|
|
255
|
+
}, { memory });
|
|
256
|
+
assert.equal(failRes.status, 'success');
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
const loadRes = await executeTool('load_active_run', {
|
|
260
|
+
run_id: runId
|
|
261
|
+
}, { memory });
|
|
262
|
+
assert.equal(loadRes.status, 'success');
|
|
263
|
+
assert.equal(loadRes.run.state.currentStep, 'parse_input');
|
|
264
|
+
assert.equal(loadRes.run.state.failures.length, 1);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('AgentMemory backward compatibility: legacy methods and facts remain unaffected', () => {
|
|
268
|
+
const memory = new AgentMemory({ persistence: false });
|
|
269
|
+
memory.addFact('Test fact about Ju Jingyi', { category: 'music' });
|
|
270
|
+
const facts = memory.getRelevantFacts('music');
|
|
271
|
+
assert.equal(facts.length, 1);
|
|
272
|
+
assert.equal(facts[0].content, 'Test fact about Ju Jingyi');
|
|
273
|
+
|
|
274
|
+
const legacyContext = memory.buildMemoryContext('music');
|
|
275
|
+
assert.match(legacyContext, /=== LONG-TERM MEMORY CONTEXT ===/);
|
|
276
|
+
assert.match(legacyContext, /Test fact about Ju Jingyi/);
|
|
277
|
+
});
|