kiku-agent 1.0.11 → 1.2.0

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 (221) hide show
  1. package/README.md +300 -495
  2. package/package.json +12 -11
  3. package/public/fonts/GoogleSans-Bold.ttf +0 -0
  4. package/public/fonts/GoogleSans-Bold.woff2 +0 -0
  5. package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
  6. package/public/fonts/GoogleSans-Italic.ttf +0 -0
  7. package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
  8. package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
  9. package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
  10. package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
  11. package/public/fonts/GoogleSans-Medium.ttf +0 -0
  12. package/public/fonts/GoogleSans-Medium.woff2 +0 -0
  13. package/public/fonts/GoogleSans-Regular.ttf +0 -0
  14. package/public/fonts/GoogleSans-Regular.woff2 +0 -0
  15. package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
  16. package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
  17. package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
  18. package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
  19. package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
  20. package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
  21. package/public/fonts/google-sans-official.css +900 -0
  22. package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  23. package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  24. package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  25. package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  26. package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  27. package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  28. package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  29. package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  30. package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  31. package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  32. package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  33. package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  34. package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  35. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
  36. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
  37. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
  38. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
  39. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
  40. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
  41. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
  42. package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
  43. package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
  44. package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
  45. package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
  46. package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
  47. package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
  48. package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
  49. package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
  50. package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
  51. package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  52. package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  53. package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  54. package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  55. package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  56. package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  57. package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  58. package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  59. package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  60. package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  61. package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  62. package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  63. package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  64. package/public/images/general/jujingyi_portrait.jpg +0 -0
  65. package/public/images/jujingyi_portrait.jpg +0 -0
  66. package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
  67. package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
  68. package/public/images/series/detective-samoyeds/poster.jpg +0 -0
  69. package/public/images/series/everyday/poster.jpg +0 -0
  70. package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
  71. package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
  72. package/public/images/series/in-blossom/poster.jpg +0 -0
  73. package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
  74. package/public/images/series/lets-fight/poster-2.jpg +0 -0
  75. package/public/images/series/lets-fight/poster.jpg +0 -0
  76. package/public/images/series/love-emergency/poster.jpg +0 -0
  77. package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
  78. package/public/images/series/money-bastion/poster.jpg +0 -0
  79. package/public/images/series/mr-swimmer/poster.jpg +0 -0
  80. package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
  81. package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
  82. package/public/images/series/rebirth-for-you/poster.jpg +0 -0
  83. package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
  84. package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
  85. package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
  86. package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
  87. package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
  88. package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
  89. package/public/images/series/veil-of-shadows/poster.jpg +0 -0
  90. package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
  91. package/public/images/series/wanhua-sin/poster.jpg +0 -0
  92. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
  93. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
  94. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
  95. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
  96. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
  97. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
  98. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
  99. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
  100. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
  101. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
  102. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
  103. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
  104. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
  105. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
  106. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
  107. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
  108. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
  109. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
  110. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
  111. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
  112. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
  113. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
  114. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
  115. package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
  116. package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
  117. package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
  118. package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
  119. package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
  120. package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
  121. package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
  122. package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
  123. package/public/manifest.json +25 -0
  124. package/public/mcp-logo-128.png +0 -0
  125. package/public/mcp-logo-64.png +0 -0
  126. package/public/mcp-logo.svg +16 -0
  127. package/server/agent/agentCore.js +542 -74
  128. package/server/agent/appPaths.js +1 -0
  129. package/server/agent/audioPlayer.js +4 -2
  130. package/server/agent/citations.js +294 -0
  131. package/server/agent/clarificationAnalytics.js +477 -0
  132. package/server/agent/clarifyingQuestions.js +1089 -0
  133. package/server/agent/cli.js +549 -125
  134. package/server/agent/commandFeatures.js +1 -0
  135. package/server/agent/commandRegistry.js +946 -879
  136. package/server/agent/commands.js +546 -272
  137. package/server/agent/config.js +581 -412
  138. package/server/agent/contextCompactor.js +2 -2
  139. package/server/agent/debate.js +235 -0
  140. package/server/agent/eventBus.js +3 -0
  141. package/server/agent/feedbackLearning.js +217 -0
  142. package/server/agent/llmClient.js +512 -394
  143. package/server/agent/mcpClient.js +182 -32
  144. package/server/agent/memory.js +283 -224
  145. package/server/agent/modelManager.js +79 -39
  146. package/server/agent/profiles.js +178 -0
  147. package/server/agent/publicTools.js +22 -0
  148. package/server/agent/queryDetails.js +68 -8
  149. package/server/agent/quizData.js +398 -384
  150. package/server/agent/research.js +121 -79
  151. package/server/agent/sanitizer.js +104 -13
  152. package/server/agent/session.js +319 -11
  153. package/server/agent/subagent.js +8 -3
  154. package/server/agent/suggestionTui.js +294 -0
  155. package/server/agent/suggestions.js +69 -3
  156. package/server/agent/telemetry.js +119 -0
  157. package/server/agent/terminalImage.js +146 -27
  158. package/server/agent/theme.js +17 -0
  159. package/server/agent/toolRouter.js +16 -1
  160. package/server/agent/tools.js +424 -225
  161. package/server/agent/ui.js +486 -64
  162. package/server/agent/version.js +2 -4
  163. package/server/agent/webSearch.js +111 -11
  164. package/server/agent/youtubeAuth.js +24 -6
  165. package/server/apiRouter.js +4222 -0
  166. package/server/mcp/data/discography.js +25 -16
  167. package/server/mcp/data/gallery.js +308 -20
  168. package/server/mcp/data/profile.js +74 -44
  169. package/server/mcp/data/timeline.js +1 -1
  170. package/server/mcp/data/works.js +51 -17
  171. package/server/mcp/dataAdapter.js +5 -3
  172. package/server/mcp/handler.js +5 -11
  173. package/server/mcp/prompts.js +86 -0
  174. package/server/mcp/security.js +18 -15
  175. package/server/mcp/server.js +7 -2
  176. package/server/mcp/tools.js +66 -2
  177. package/server/web-chat/core/browserLauncher.js +18 -0
  178. package/server/web-chat/core/connection.js +194 -0
  179. package/server/web-chat/core/conversationContext.js +82 -0
  180. package/server/web-chat/core/directCatalogEngine.js +196 -0
  181. package/server/web-chat/core/dramaKnowledge.js +142 -0
  182. package/server/web-chat/core/errorHandler.js +51 -0
  183. package/server/web-chat/core/followUpEngine.js +182 -0
  184. package/server/web-chat/core/futureEventGuard.js +80 -0
  185. package/server/web-chat/core/memorySnapshot.js +12 -0
  186. package/server/web-chat/core/security.js +146 -0
  187. package/server/web-chat/core/toolInspector.js +52 -0
  188. package/server/web-chat/router.js +120 -0
  189. package/server/web-chat/routes/chatRoute.js +397 -0
  190. package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
  191. package/server/web-chat/routes/clarifyRoute.js +83 -0
  192. package/server/web-chat/routes/compareRoute.js +79 -0
  193. package/server/web-chat/routes/exportRoute.js +59 -0
  194. package/server/web-chat/routes/feedbackRoute.js +57 -0
  195. package/server/web-chat/routes/newsRoute.js +33 -0
  196. package/server/web-chat/routes/quizRoute.js +11 -0
  197. package/server/web-chat/routes/staticRoute.js +103 -0
  198. package/server/web-chat/routes/statusRoute.js +52 -0
  199. package/server/web-chat/routes/suggestRoute.js +77 -0
  200. package/server/web-chat/routes/timelineRoute.js +34 -0
  201. package/server/web-chat/routes/verifyRoute.js +57 -0
  202. package/server/web-chat/server.js +116 -0
  203. package/server/web-chat/ui/htmlBuilder.js +91 -0
  204. package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
  205. package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
  206. package/server/web-chat/ui/scripts/client.js +8397 -0
  207. package/server/web-chat/ui/scripts/kikuDb.js +177 -0
  208. package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
  209. package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
  210. package/server/web-chat/ui/styles/main.css +8568 -0
  211. package/server/web-chat/ui/templates/body.html +796 -0
  212. package/server/webChatHtml.js +1 -0
  213. package/server/webChatServer.js +6 -0
  214. package/src/data/discographyData.js +892 -866
  215. package/src/data/galleryData.js +1805 -561
  216. package/src/data/profileData.js +79 -2
  217. package/src/data/socialUpdatesData.js +106 -47
  218. package/src/data/timelineData.js +1 -1
  219. package/src/data/worksData.js +25 -4
  220. package/tokens.css +97 -0
  221. package/server/agent/updateManager.js +0 -456
@@ -1,32 +1,61 @@
1
- import { config } from './config.js';
2
- import { toolsSchema, executeTool, prepareToolArguments } from './tools.js';
3
- import { LLMClient, LMStudioClient } from './llmClient.js';
4
- import { AgentSession } from './session.js';
5
- import { AgentMemory } from './memory.js';
6
1
  import { AgentStateMachine, AgentStates } from './agentState.js';
7
- import { eventBus, Events } from './eventBus.js';
8
- import { sanitizeQuery, isLikelySafe } from './sanitizer.js';
2
+ import { config } from './config.js';
3
+ import { contextCompactor } from './contextCompactor.js';
9
4
  import { createAbortError, isAbortError } from './errors.js';
10
- import { ResearchSubagent } from './subagent.js';
5
+ import { Events, eventBus } from './eventBus.js';
6
+ import { AgentJobManager } from './jobManager.js';
7
+ import { LLMClient } from './llmClient.js';
8
+ import { McpClientManager } from './mcpClient.js';
9
+ import { AgentMemory } from './memory.js';
11
10
  import { ResearchOrchestrator } from './research.js';
11
+ import { sanitizeQuery, sanitizeResponse } from './sanitizer.js';
12
+ import { AgentSession } from './session.js';
13
+ import { ResearchSubagent } from './subagent.js';
12
14
  import { SuggestionEngine } from './suggestions.js';
13
- import { McpClientManager } from './mcpClient.js';
14
15
  import { DynamicToolRouter } from './toolRouter.js';
15
- import { contextCompactor } from './contextCompactor.js';
16
- import { AgentJobManager } from './jobManager.js';
16
+ import { executeTool, prepareToolArguments, toolsSchema, isBuiltinTool } from './tools.js';
17
+ import { detectAmbiguities, isQueryTooVague, emitClarifyingQuestions, calculateConfidence, generateClarifyingQuestions, buildClarificationContext } from './clarifyingQuestions.js';
18
+ import { ClarificationAnalytics, ClarificationConfig } from './clarificationAnalytics.js';
19
+ import { calculateTokenCost } from './telemetry.js';
20
+ import { extractCitations, resolveCitationReference } from './citations.js';
17
21
 
18
22
  export function detectDominantLanguage(text) {
19
23
  if (!text || typeof text !== 'string') return 'th';
20
24
 
21
- const thaiMatches = text.match(/[\u0E00-\u0E7F]/g) || [];
25
+ const clean = text
26
+ .replace(/\[SYSTEM\s+[A-Z]+:[^\]]*\]/gi, '')
27
+ .replace(/\[SYSTEM:[^\]]*\]/gi, '')
28
+ .replace(/\[SYSTEM\s+INSTRUCTION:[^\]]*\]/gi, '')
29
+ .replace(/\[SYSTEM\s+DIRECTIVE:[^\]]*\]/gi, '')
30
+ .replace(/^User Question:\s*/gim, '')
31
+ .trim();
32
+
33
+ const targetText = clean || text;
34
+
35
+ const thaiMatches = targetText.match(/[\u0E00-\u0E7F]/g) || [];
22
36
  const thaiCount = thaiMatches.length;
23
37
 
24
- const chineseMatches = text.match(/[\u4E00-\u9FA5]/g) || [];
38
+ const chineseMatches = targetText.match(/[\u4E00-\u9FA5]/g) || [];
25
39
  const chineseCount = chineseMatches.length;
26
40
 
27
- const englishMatches = text.match(/[a-zA-Z]+/g) || [];
41
+ const kanaMatches = targetText.match(/[\u3040-\u30FF]/g) || [];
42
+ const kanaCount = kanaMatches.length;
43
+
44
+ const hangulMatches = targetText.match(/[\uAC00-\uD7AF]/g) || [];
45
+ const hangulCount = hangulMatches.length;
46
+
47
+ const englishMatches = targetText.match(/[a-zA-Z]+/g) || [];
28
48
  const englishCount = englishMatches.join('').length;
29
49
 
50
+
51
+ if (kanaCount > 0) {
52
+ return 'ja';
53
+ }
54
+
55
+ if (hangulCount > 0 && hangulCount >= thaiCount && hangulCount >= chineseCount && hangulCount >= englishCount) {
56
+ return 'ko';
57
+ }
58
+
30
59
  if (thaiCount >= chineseCount && thaiCount >= englishCount && thaiCount > 0) {
31
60
  return 'th';
32
61
  } else if (chineseCount > thaiCount && chineseCount >= englishCount && chineseCount > 0) {
@@ -49,8 +78,11 @@ function stableToolSignature(toolName, args) {
49
78
  return `${toolName}:${JSON.stringify(sortValue(args || {}))}`;
50
79
  }
51
80
 
52
- export class KikuAgent {
53
- constructor(options = {}) {
81
+ export class KikuAgent {
82
+ constructor(options = {}) {
83
+ this.allowedToolNames = options.allowedToolNames
84
+ ? new Set(Array.from(options.allowedToolNames, String))
85
+ : null;
54
86
  this.client = options.client || new LLMClient({
55
87
  provider: options.provider || config.activeProvider,
56
88
  baseUrl: options.baseUrl,
@@ -59,20 +91,29 @@ export class KikuAgent {
59
91
  });
60
92
  this.maxIterations = options.maxIterations || config.agent.maxIterations || 5;
61
93
  this.slidingWindowSize = options.slidingWindowSize || config.agent.slidingWindowSize || 8;
62
- this.session = new AgentSession({ thinkingMode: options.thinkingMode || 'hide' });
63
- this.memory = new AgentMemory();
94
+ this.session = new AgentSession({
95
+ thinkingMode: options.thinkingMode || 'hide',
96
+ sessionId: options.sessionId,
97
+ storageKey: options.sessionStorageKey,
98
+ storageFile: options.sessionStorageFile,
99
+ persistence: options.sessionPersistence !== false,
100
+ });
101
+ this.memory = options.memory || new AgentMemory();
64
102
  this.mcpManager = options.mcpManager || new McpClientManager();
65
103
  this.router = new DynamicToolRouter(this.getAllToolsSchema());
66
104
  this.research = new ResearchOrchestrator({ client: this.client });
67
105
  this.suggestions = new SuggestionEngine({ client: this.client, memory: this.memory });
68
106
  this.jobs = new AgentJobManager();
107
+ this.clarifyConfig = options.clarifyConfig || new ClarificationConfig();
108
+ this.clarifyAnalytics = options.clarifyAnalytics || new ClarificationAnalytics();
69
109
  this.systemPrompt = this.buildSystemPrompt();
70
110
  this.initHistory();
71
111
  this.restoredMessageCount = 0;
112
+ this.lastCitations = [];
72
113
 
73
114
  this.stateMachine = new AgentStateMachine();
74
115
 
75
- this.stateMachine.on('transition', ({ from, to }) => {
116
+ this.stateMachine.on('transition', ({ to }) => {
76
117
  eventBus.emit(Events.AGENT_STATE_CHANGE, {
77
118
  state: to,
78
119
  label: this.stateMachine.label,
@@ -85,7 +126,7 @@ export class KikuAgent {
85
126
  }
86
127
 
87
128
  async _init() {
88
- await this.memory.load();
129
+ if (!this.memory._loaded) await this.memory.load();
89
130
  await this.loadSession();
90
131
  if (this.mcpManager && typeof this.mcpManager.loadPersistedServers === 'function') {
91
132
  try {
@@ -171,6 +212,141 @@ export class KikuAgent {
171
212
  return this.jobs.clear();
172
213
  }
173
214
 
215
+ async _handleClarifyingQuestions(query, lang, queryId, callbacks) {
216
+ if (!callbacks.onClarifyingQuestions || !callbacks.onClarifyingAnswer) {
217
+ return null;
218
+ }
219
+
220
+ const clarifyConfig = this.clarifyConfig.getConfig();
221
+ if (!clarifyConfig.enabled) {
222
+ return null;
223
+ }
224
+
225
+
226
+ const hasPriorClarification = (this._clarificationAskedCount && this._clarificationAskedCount >= 1) ||
227
+ (this.conversationHistory && this.conversationHistory.some(m => m.content && (m.content.includes('[SYSTEM CLARIFICATION]') || m.content.includes('CLARIFICATION:'))));
228
+ if (hasPriorClarification) {
229
+ return null;
230
+ }
231
+
232
+ if (isQueryTooVague(query)) {
233
+ return null;
234
+ }
235
+
236
+ if (query.includes('[SYSTEM CLARIFICATION]') || query.includes('CLARIFICATION:')) {
237
+ return null;
238
+ }
239
+
240
+ const ambiguities = detectAmbiguities(query, lang);
241
+ const confidence = calculateConfidence(query, ambiguities);
242
+ const threshold = clarifyConfig.confidenceThreshold;
243
+
244
+
245
+ const CROSS_CATEGORY_PRIORITIES = ['media_type', 'career_phase', 'specific_work', 'timeframe', 'comparison'];
246
+ const highImpactAmbiguities = ambiguities.filter(a => CROSS_CATEGORY_PRIORITIES.includes(a.category));
247
+
248
+ const shouldAsk = (confidence.score < threshold || confidence.shouldAsk) && highImpactAmbiguities.length > 0;
249
+
250
+ eventBus.emit(Events.AGENT_CLARIFYING_QUESTIONS, {
251
+ queryId,
252
+ confidence,
253
+ threshold,
254
+ shouldAsk,
255
+ timestamp: Date.now(),
256
+ });
257
+
258
+ if (!shouldAsk || highImpactAmbiguities.length === 0) {
259
+ return null;
260
+ }
261
+
262
+
263
+
264
+ let questionsToAsk = null;
265
+ try {
266
+ questionsToAsk = await generateClarifyingQuestions(query, lang, {
267
+ conversationHistory: this.conversationHistory,
268
+ client: this.client,
269
+ maxQuestions: 4,
270
+ detectedAmbiguities: highImpactAmbiguities,
271
+ });
272
+ } catch {
273
+
274
+ }
275
+
276
+ if (!questionsToAsk || questionsToAsk.length === 0) {
277
+ questionsToAsk = highImpactAmbiguities.slice(0, 4).map(q => ({
278
+ ...q,
279
+ options: (q.options || []).slice(0, 4)
280
+ }));
281
+ } else {
282
+ questionsToAsk = questionsToAsk.slice(0, 5).map(q => ({
283
+ ...q,
284
+ options: (q.options || []).slice(0, 4)
285
+ }));
286
+ }
287
+
288
+ this._clarificationAskedCount = (this._clarificationAskedCount || 0) + 1;
289
+
290
+ const startTime = Date.now();
291
+ this.clarifyAnalytics.recordClarificationStart(queryId, questionsToAsk, confidence.score);
292
+
293
+ try {
294
+ eventBus.emit(Events.AGENT_CLARIFYING_QUESTIONS, {
295
+ queryId,
296
+ questions: questionsToAsk,
297
+ confidence,
298
+ threshold,
299
+ timestamp: Date.now(),
300
+ });
301
+
302
+ callbacks.onClarifyingQuestions(questionsToAsk, queryId);
303
+
304
+ const answers = await Promise.race([
305
+ callbacks.onClarifyingAnswer(queryId),
306
+ new Promise((_, reject) =>
307
+ setTimeout(() => reject(new Error('CLARIFY_TIMEOUT')), clarifyConfig.timeoutMs)
308
+ ),
309
+ ]);
310
+
311
+ const responseTimeMs = Date.now() - startTime;
312
+ const { parseClarifyingAnswers } = await import('./clarifyingQuestions.js');
313
+ const result = parseClarifyingAnswers(answers, questionsToAsk);
314
+
315
+ if (result.summary.includes('skip')) {
316
+ this.clarifyAnalytics.recordClarificationSkipped(queryId);
317
+ } else {
318
+ this.clarifyAnalytics.recordClarificationAnswer(queryId, result.answers, 1, responseTimeMs);
319
+ }
320
+
321
+ eventBus.emit(Events.AGENT_CLARIFYING_ANSWER, {
322
+ queryId,
323
+ answers: result.answers,
324
+ summary: result.summary,
325
+ confidence,
326
+ threshold,
327
+ responseTimeMs,
328
+ timestamp: Date.now(),
329
+ });
330
+
331
+ return result;
332
+ } catch {
333
+ this.clarifyAnalytics.recordClarificationTimeout(queryId);
334
+ return null;
335
+ }
336
+ }
337
+
338
+ getClarificationAnalytics(options = {}) {
339
+ return this.clarifyAnalytics.getAnalytics(options);
340
+ }
341
+
342
+ getClarificationConfig() {
343
+ return this.clarifyConfig.getConfig();
344
+ }
345
+
346
+ updateClarificationConfig(updates) {
347
+ return this.clarifyConfig.updateConfig(updates);
348
+ }
349
+
174
350
  initHistory() {
175
351
  this.conversationHistory = [
176
352
  {
@@ -280,19 +456,74 @@ export class KikuAgent {
280
456
  eventBus.emit(Events.SESSION_SAVED, { sessionId: this.session.id });
281
457
  }
282
458
 
459
+ async hydrateConversationHistory(messages = [], options = {}) {
460
+ await this._ready;
461
+ const alreadyHasConversation = this.conversationHistory?.some(message =>
462
+ message.role === 'user' || message.role === 'assistant'
463
+ );
464
+ if (alreadyHasConversation && options.force !== true) {
465
+ return { hydrated: false, reason: 'session_already_loaded', messageCount: this.session.messages.length };
466
+ }
467
+
468
+ const maxMessages = Math.max(1, Math.min(Number(options.maxMessages) || 24, 50));
469
+ const normalized = (Array.isArray(messages) ? messages : [])
470
+ .slice(-maxMessages)
471
+ .map(message => {
472
+ const role = message?.role === 'user' ? 'user' :
473
+ (message?.role === 'assistant' || message?.role === 'bot' ? 'assistant' : null);
474
+ const content = String(message?.content ?? message?.text ?? '').trim().slice(0, 6000);
475
+ return role && content ? { role, content } : null;
476
+ })
477
+ .filter(Boolean);
478
+
479
+ if (normalized.length === 0) {
480
+ return { hydrated: false, reason: 'empty_history', messageCount: 0 };
481
+ }
482
+
483
+ const createdAt = new Date();
484
+ const sessionMessages = normalized.map((message, index) => ({
485
+ id: `msg_hydrated_${createdAt.getTime()}_${index}`,
486
+ role: message.role,
487
+ queryNumber: Math.floor(index / 2) + 1,
488
+ createdAt,
489
+ parts: [{ type: 'text', content: message.content }]
490
+ }));
491
+ const userMessageCount = normalized.filter(message => message.role === 'user').length;
492
+
493
+ this.session.restoreFromSnapshot({
494
+ id: options.sessionId || this.session.id,
495
+ createdAt,
496
+ thinkingMode: this.session.thinkingMode,
497
+ messages: sessionMessages,
498
+ stats: {
499
+ totalQueries: userMessageCount,
500
+ totalToolCalls: 0,
501
+ totalDurationMs: 0,
502
+ }
503
+ });
504
+ this.initHistory();
505
+ normalized.forEach(message => this.conversationHistory.push(message));
506
+ this.restoredMessageCount = normalized.length;
507
+ await this.saveSession();
508
+
509
+ return { hydrated: true, messageCount: normalized.length };
510
+ }
511
+
283
512
  applySlidingWindow() {
284
513
  if (this.conversationHistory.length > this.slidingWindowSize + 1) {
285
514
  this.conversationHistory = contextCompactor.compact(this.conversationHistory, {
286
- maxTokens: 6000,
515
+ maxTokens: 24000,
287
516
  keepRecentMessages: this.slidingWindowSize,
288
517
  });
289
518
  }
290
519
  }
291
520
 
292
- getAllToolsSchema() {
293
- const mcpTools = this.mcpManager ? this.mcpManager.getToolsSchema() : [];
294
- return [...toolsSchema, ...mcpTools];
295
- }
521
+ getAllToolsSchema() {
522
+ const mcpTools = this.mcpManager ? this.mcpManager.getToolsSchema() : [];
523
+ const allTools = [...toolsSchema, ...mcpTools];
524
+ if (!this.allowedToolNames) return allTools;
525
+ return allTools.filter(tool => this.allowedToolNames.has(tool.function?.name));
526
+ }
296
527
 
297
528
  async connectMcp(name, serverConfig) {
298
529
  const result = await this.mcpManager.connect(name, serverConfig);
@@ -357,6 +588,7 @@ export class KikuAgent {
357
588
 
358
589
  buildSystemPrompt() {
359
590
  const allTools = this.getAllToolsSchema();
591
+ const today = new Date().toISOString().slice(0, 10);
360
592
  const toolsSummary = allTools.map(t => {
361
593
  const f = t.function;
362
594
  const paramKeys = Object.keys(f.parameters?.properties || {}).join(', ');
@@ -390,25 +622,49 @@ ${toolsSummary}
390
622
 
391
623
  3. FACT ACCURACY & NO DUPLICATION:
392
624
  - Always use tools to verify facts before answering. Do NOT hallucinate.
393
- - Current year is 2026. Prioritize 2026/2025 projects.
625
+ - Current date is ${today}. Prioritize current and recent projects when relevant.
394
626
  - Never repeat the same statement. Sort newest first.
395
627
 
396
- 4. RESPONSE STYLE:
397
- - NEVER start with template greetings — dive DIRECTLY into the answer.
398
- - Format with Markdown headers (###), bold (**text**), bullet points (•).
399
- - Emoji are optional: use at most one contextually relevant emoji per response and never repeat them across headings or bullets.
400
- - Match answer depth to what the user asked.
401
-
402
- 5. SMART TOOL SELECTION:
403
- - Videos/stages ➔ get_music_videos or get_media_links
404
- - Songs/OSTs ➔ search_discography or get_media_links
405
- - Dramas ➔ search_works or get_work_detail
628
+ 3A. CONVERSATION TOPIC ANCHOR:
629
+ - Resolve short or ambiguous follow-ups (for example "ทำไมน่าจับตามอง?", "งานนี้", "why is it notable?") against the most recent user topic and assistant answer.
630
+ - The current follow-up inherits that topic. Do not default the subject to Ju Jingyi merely because the persona or knowledge base focuses on her.
631
+ - A persona changes voice and tone only; it never replaces the user's subject.
632
+ - If the reference remains genuinely ambiguous after reading recent history, ask one focused clarification question instead of guessing.
633
+
634
+ 3B. FUTURE-EVENT CLAIM SAFETY:
635
+ - Treat any event after today's date as scheduled or reported, not completed fact.
636
+ - Never assert an exact future date, invitation, guest list, or attendance without checking a current official source with a live web tool.
637
+ - Never carry a date, guest list, or attendance status from one annual edition into another year.
638
+ - When official confirmation is unavailable, say clearly that the detail is unconfirmed and subject to change.
639
+
640
+ 4. RESPONSE STYLE:
641
+ - NEVER start with template greetings — dive DIRECTLY into the answer.
642
+ - Format with Markdown headers (###), bold (**text**), bullet points (•).
643
+ - Emoji are optional: use at most one contextually relevant emoji per response and never repeat them across headings or bullets.
644
+ - Match answer depth to what the user asked.
645
+ - Separate catalog facts from interpretation. Clearly frame acting observations as analysis unless a cited review supports them.
646
+ - Do not infer that newer means deeper, more realistic, or better acted. Compare concrete role demands, narrative agency, genre, and verified scenes instead.
647
+ - Omit catalog ratings from analytical conclusions unless the tool output names the rating source and scale.
648
+ - Avoid unsupported superlatives or career verdicts (for example "unrivalled", "top-tier", or "completely transcended idol status").
649
+ - In Blossom (花间令): Ju Jingyi portrays Yang Caiwei's identity in Shangguan Zhi's appearance after the face-swap premise; do not describe this as two unrelated roles throughout the series.
650
+ - The Legend of White Snake (新白娘子传奇): Bai Suzhen is the white-snake spirit in human form and Xu Xian is the human man; never reverse this relationship.
651
+ - On an explicit continuation, answer the new angle without restating the previous response.
652
+
653
+ 5. SMART TOOL SELECTION (MANDATORY LOCAL DATABASE FIRST):
654
+ - ALWAYS call LOCAL built-in database tools FIRST. They query the rich, local catalog database directly in-process and are the authoritative source of truth.
655
+ - Videos/stages/audio ➔ search_discography, get_music_videos, get_media_links, play_music
656
+ - Songs/OSTs ➔ search_discography
657
+ - Dramas/roles ➔ search_works, get_work_detail, get_character_roles
658
+ - Profile/fandom ➔ get_profile_and_fandom
659
+ - Brands ➔ get_brand_endorsements
660
+ - Awards ➔ get_awards
661
+ - Career timeline ➔ get_career_timeline
662
+ - Local files ➔ read_local_file, list_directory, get_system_stats
663
+ - MCP tools (mcp_*) ➔ Secondary only; only invoke when user explicitly mentions MCP (@mcp) or when specialized external services are required.
406
664
  - News/external facts ➔ web_search or web_extract
407
665
  - Deep multi-source research ➔ web_extract (search + auto-fetch)
408
666
  - Read webpage content ➔ fetch_webpage
409
667
  - Fetch API/JSON ➔ fetch_json
410
- - Local files ➔ read_local_file
411
- - Directory listing ➔ list_directory
412
668
  - Screenshot ➔ screenshot_webpage
413
669
 
414
670
  6. AGGRESSIVE WEB SEARCH FALLBACK (CRITICAL):
@@ -429,7 +685,33 @@ ${toolsSummary}
429
685
  • Isolated In-Depth Subtask ➔ Call run_subagent with a specific task parameter to delegate an isolated iterative investigation with evidence and tool call tracing.
430
686
  • Complex Multi-Faceted Research ➔ Call run_deep_research with question when the user asks an open-ended, complex query that requires parallel multi-worker investigation across catalog, live web sources, and independent verifier with cited sources.
431
687
  • Rumors, Cast Leaks & Fact Verification ➔ Call verify_claim with claim when the user asks whether a rumor, statement, or unconfirmed news is true (returns VERIFIED, UNVERIFIED, CONFLICTING, or FALSE).
432
- • Teaching / Updating Response Preferences ➔ Call manage_skill with action 'learn' (with instruction), 'list', 'remove' (with skillId), 'clear', 'enable', or 'disable' when the user instructs how they want Kiku to answer in future conversations.`;
688
+ • Teaching / Updating Response Preferences ➔ Call manage_skill with action 'learn' (with instruction), 'list', 'remove' (with skillId), 'clear', 'enable', or 'disable' when the user instructs how they want Kiku to answer in future conversations.
689
+
690
+ 9. ANTI-HALLUCINATION GUARDRAILS (CRITICAL — MANDATORY COMPLIANCE):
691
+
692
+ 0. INDEPENDENT VERIFICATION FIRST:
693
+ - When asked about specific named entities (e.g., upcoming dramas, casts, titles, products, brand deals, real-world releases), do NOT generate an explanation based solely on keyword inference or assumed context.
694
+ - You MUST cross-reference and verify the exact entity existence using your local catalog tools (search_works, search_discography, get_brand_endorsements, etc.) BEFORE writing the response.
695
+ - If the local catalog has no match, you MUST call web_search or web_extract to verify externally before responding.
696
+
697
+ 1. ZERO-ASSUMPTION ON UNVERIFIED ENTITIES:
698
+ - If the subject/entity cannot be verified with official records (not found in catalog tools AND not found via web_search), explicitly state that no verified record exists.
699
+ - Strictly avoid fabricating plausible generic narratives (e.g., generic tropes, placeholder plotlines, speculative casting, invented brand partnerships).
700
+ - NEVER invent drama titles, song names, character roles, brand endorsements, or career milestones that are not backed by tool results.
701
+
702
+ 2. FALLBACK & UNCERTAINTY PROTOCOL:
703
+ - If the entity appears to be misspelled, fan fiction, or an unverified rumor, directly flag the discrepancy (including linguistic errors like incorrect Pinyin or romanization) rather than adopting the user's premise as true.
704
+ - Always prioritize stating "No official information found in the database" over generating a complete but ungrounded description.
705
+ - If tool results are ambiguous or partial, present only what is confirmed and clearly label uncertain parts as "unverified" or "not confirmed."
706
+
707
+ 3. SOURCE ATTRIBUTION:
708
+ - When stating facts, prefer answers that trace back to tool results. If a claim comes from local catalog, you may present it as confirmed data. If from web search, note it as external/recent information.
709
+ - NEVER present fabricated information as if it were verified catalog data.
710
+
711
+ 4. SELF-CHECK BEFORE FINAL ANSWER:
712
+ - Before synthesizing your final response, mentally verify: "Does every factual claim in my answer have a corresponding tool result backing it up?"
713
+ - If any claim lacks tool evidence, either (a) call the appropriate tool to verify it, or (b) remove it from your answer.
714
+ - This applies to ALL entity types: dramas, songs, MVs, brands, awards, dates, character names, and career milestones.`;
433
715
  }
434
716
 
435
717
  parseTextToolCalls(content) {
@@ -438,8 +720,7 @@ ${toolsSummary}
438
720
  const allTools = this.getAllToolsSchema();
439
721
 
440
722
  const jsonBlockRegex = /```(?:json)?\s*(\{[\s\S]*?\})\s*```/g;
441
- let match;
442
- while ((match = jsonBlockRegex.exec(content)) !== null) {
723
+ for (const match of content.matchAll(jsonBlockRegex)) {
443
724
  try {
444
725
  const parsed = JSON.parse(match[1]);
445
726
  const toolName = parsed.action || parsed.tool || parsed.name || parsed.function;
@@ -458,9 +739,9 @@ ${toolsSummary}
458
739
  }
459
740
 
460
741
  const actionRegex = /Action:\s*([a-zA-Z0-9_]+)\s*\nAction Input:\s*(\{[\s\S]*?\}|[^\n]+)/g;
461
- while ((match = actionRegex.exec(content)) !== null) {
742
+ for (const match of content.matchAll(actionRegex)) {
462
743
  const toolName = match[1].trim();
463
- let rawInput = match[2].trim();
744
+ const rawInput = match[2].trim();
464
745
  if (allTools.some(t => t.function.name === toolName)) {
465
746
  let args = {};
466
747
  try {
@@ -506,6 +787,7 @@ ${toolsSummary}
506
787
  onReasoningEnd = () => { },
507
788
  onToolStart = () => { },
508
789
  onToolResult = () => { },
790
+ onSubagentProgress = () => { },
509
791
  onTokenStart = () => { },
510
792
  onTokenDelta = () => { },
511
793
  onFinalAnswer = () => { },
@@ -527,8 +809,16 @@ ${toolsSummary}
527
809
  ];
528
810
  }
529
811
 
812
+ let promptForLLMWithGrounding = promptForLLM;
813
+ if (this.lastCitations && this.lastCitations.length > 0) {
814
+ const citedRef = resolveCitationReference(promptForLLM, this.lastCitations);
815
+ if (citedRef) {
816
+ promptForLLMWithGrounding = `${promptForLLM}\n\n[CONTEXT GROUNDING: User references Citation [${citedRef.id}] "${citedRef.title}" (${citedRef.category || citedRef.type || ''}${citedRef.year ? `, ${citedRef.year}` : ''})]`;
817
+ }
818
+ }
819
+
530
820
  this.session.createUserMessage(promptForLLM);
531
- const userMessage = { role: 'user', content: promptForLLM };
821
+ const userMessage = { role: 'user', content: promptForLLMWithGrounding };
532
822
  this.conversationHistory.push(userMessage);
533
823
 
534
824
  const memoryContext = this.memory.buildMemoryContext(promptForLLM);
@@ -542,6 +832,10 @@ ${toolsSummary}
542
832
  langInstruction = '[SYSTEM NOTE: User language is Thai. Reply in Thai only.]\n';
543
833
  } else if (dominantLang === 'zh') {
544
834
  langInstruction = '[SYSTEM NOTE: User language is Chinese. Reply in Chinese only.]\n';
835
+ } else if (dominantLang === 'ja') {
836
+ langInstruction = '[SYSTEM NOTE: User language is Japanese. Reply in Japanese only.]\n';
837
+ } else if (dominantLang === 'ko') {
838
+ langInstruction = '[SYSTEM NOTE: User language is Korean. Reply in Korean only.]\n';
545
839
  } else {
546
840
  langInstruction = '[SYSTEM NOTE: User language is English. Reply in English only.]\n';
547
841
  }
@@ -552,6 +846,19 @@ ${toolsSummary}
552
846
  }
553
847
  this.conversationHistory.push({ role: 'system', content: langInstruction });
554
848
 
849
+ const clarifyingResult = await this._handleClarifyingQuestions(
850
+ promptForLLM,
851
+ dominantLang,
852
+ queryId,
853
+ callbacks
854
+ );
855
+ if (clarifyingResult) {
856
+ this.conversationHistory.push({
857
+ role: 'user',
858
+ content: `[SYSTEM CLARIFICATION] ${clarifyingResult.summary}\nOriginal query: ${promptForLLM}`
859
+ });
860
+ }
861
+
555
862
  this.applySlidingWindow();
556
863
 
557
864
  const currentAssistantSessionMsg = this.session.createAssistantMessage({
@@ -559,8 +866,13 @@ ${toolsSummary}
559
866
  provider: this.client?.providerName || this.client?.provider || null,
560
867
  });
561
868
 
869
+ let queryPromptTokens = 0;
870
+ let queryCompletionTokens = 0;
871
+ let queryReasoningTokens = 0;
872
+
562
873
  let currentIteration = 0;
563
874
  const toolResultCache = new Map();
875
+ const toolResultTracker = { notFound: 0, error: 0, success: 0, total: 0, toolNames: [] };
564
876
  const activeTools = this.router.routeToolsForQuery(promptForLLM, {
565
877
  showAll: callbacks.showAllTools || false,
566
878
  includeMcp: true,
@@ -584,6 +896,7 @@ ${toolsSummary}
584
896
  messages: this.conversationHistory,
585
897
  tools: activeTools,
586
898
  temperature: 0.3,
899
+ maxTokens: 8192,
587
900
  stream: true,
588
901
  signal,
589
902
  onThought: (thoughtChunk) => {
@@ -640,6 +953,12 @@ ${toolsSummary}
640
953
  throw new Error(`No response received from ${this.client?.providerName || 'AI provider'}.`);
641
954
  }
642
955
 
956
+ if (assistantMessage.usage) {
957
+ queryPromptTokens += Number(assistantMessage.usage.prompt_tokens || 0);
958
+ queryCompletionTokens += Number(assistantMessage.usage.completion_tokens || 0);
959
+ queryReasoningTokens += Number(assistantMessage.usage.reasoning_tokens || 0);
960
+ }
961
+
643
962
  let toolCalls = assistantMessage.tool_calls || [];
644
963
  if (toolCalls.length === 0 && assistantMessage.content) {
645
964
  toolCalls = this.parseTextToolCalls(assistantMessage.content);
@@ -660,11 +979,13 @@ ${toolsSummary}
660
979
  const totalDurationMs = Date.now() - queryStartTime;
661
980
  this.session.stats.totalDurationMs += totalDurationMs;
662
981
 
663
- const finalAnswer = assistantMessage.content || (
982
+ const finalAnswer = sanitizeResponse(assistantMessage.content || (
664
983
  dominantLang === 'zh' ? "抱歉,无法处理您的回答。" :
665
- dominantLang === 'en' ? "Sorry, unable to process a response." :
666
- "ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้"
667
- );
984
+ dominantLang === 'ja' ? "申し訳ありません、回答を処理できませんでした。" :
985
+ dominantLang === 'ko' ? "죄송합니다. 답변을 처리할 수 없습니다." :
986
+ dominantLang === 'en' ? "Sorry, unable to process a response." :
987
+ "ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้"
988
+ ));
668
989
  onFinalAnswer(finalAnswer, { durationMs: totalDurationMs, language: dominantLang });
669
990
  this._extractFactsFromFinalAnswer(finalAnswer);
670
991
 
@@ -672,15 +993,30 @@ ${toolsSummary}
672
993
  eventBus.emit(Events.AGENT_FINAL_ANSWER, { queryId, answer: finalAnswer, durationMs: totalDurationMs });
673
994
  this.stateMachine.reset();
674
995
 
996
+ await this.saveSession().catch(err => console.error('Session save failed:', err));
675
997
  return finalAnswer;
676
998
  }
677
999
 
678
1000
  const executionPromises = toolCalls.map(async (tc, toolCallIndex) => {
679
1001
  if (signal?.aborted) throw createAbortError();
680
1002
 
681
- const toolName = tc?.function?.name || '';
682
- const toolCallId = tc?.id || `call_${queryId}_${currentIteration}_${toolCallIndex}`;
683
- const isMcp = Boolean(this.mcpManager?.isMcpTool(toolName));
1003
+ const toolName = tc?.function?.name || '';
1004
+ const toolCallId = tc?.id || `call_${queryId}_${currentIteration}_${toolCallIndex}`;
1005
+ if (this.allowedToolNames && !this.allowedToolNames.has(toolName)) {
1006
+ const rejectedResult = { status: 'error', message: `Tool "${toolName}" is not permitted in this execution context.` };
1007
+ onToolStart(toolName, {});
1008
+ onToolResult(toolName, rejectedResult, 0);
1009
+ return {
1010
+ toolMessage: {
1011
+ role: 'tool',
1012
+ tool_call_id: toolCallId,
1013
+ name: toolName,
1014
+ content: JSON.stringify(rejectedResult),
1015
+ },
1016
+ };
1017
+ }
1018
+ const isBuiltin = isBuiltinTool(toolName);
1019
+ const isMcp = !isBuiltin && (toolName.startsWith('mcp_') || Boolean(this.mcpManager?.isMcpTool(toolName)));
684
1020
 
685
1021
  let prepared;
686
1022
  if (isMcp) {
@@ -735,10 +1071,12 @@ ${toolsSummary}
735
1071
  let result;
736
1072
  if (cachedResult) {
737
1073
  result = { ...cachedResult, cache: { hit: true, reason: 'duplicate_call_in_query' } };
1074
+ } else if (isBuiltin) {
1075
+ result = await executeTool(toolName, finalArgs, { agent: this, memory: this.memory, client: this.client, signal, onSubagentProgress });
738
1076
  } else if (isMcp) {
739
1077
  result = await this.mcpManager.callTool(toolName, finalArgs);
740
1078
  } else {
741
- result = await executeTool(toolName, finalArgs, { agent: this, memory: this.memory, client: this.client, signal });
1079
+ result = await executeTool(toolName, finalArgs, { agent: this, memory: this.memory, client: this.client, signal, onSubagentProgress });
742
1080
  }
743
1081
 
744
1082
  if (!cachedResult) toolResultCache.set(signature, result);
@@ -750,7 +1088,12 @@ ${toolsSummary}
750
1088
 
751
1089
  this._extractFactsFromToolResult(toolName, finalArgs, result);
752
1090
 
753
-
1091
+ toolResultTracker.total++;
1092
+ toolResultTracker.toolNames.push(toolName);
1093
+ if (result.status === 'not_found') toolResultTracker.notFound++;
1094
+ else if (result.status === 'error') toolResultTracker.error++;
1095
+ else toolResultTracker.success++;
1096
+
754
1097
  const cappedResult = this._capToolResultForHistory(result);
755
1098
 
756
1099
  return {
@@ -758,14 +1101,14 @@ ${toolsSummary}
758
1101
  role: 'tool',
759
1102
  tool_call_id: toolCallId,
760
1103
  name: toolName,
761
- content: JSON.stringify({
1104
+ content: JSON.stringify({
762
1105
  authoritative: true,
763
1106
  result: cappedResult,
764
1107
  instruction: result.status === 'not_found'
765
- ? 'No matching data was found. Do not invent records; say that the catalog has no match or use an appropriate fallback tool.'
1108
+ ? 'CRITICAL: No matching data was found for this query. Do NOT invent, fabricate, or guess any records. You MUST either (a) state explicitly that no verified information exists, or (b) immediately call web_search to look up live external information. Under no circumstances should you generate plausible-sounding but unverified details.'
766
1109
  : result.status === 'error'
767
- ? 'The tool failed. Do not claim that the requested facts were verified.'
768
- : 'Use only facts present in this result when answering. Do not echo raw JSON data — summarize in natural language.'
1110
+ ? 'CRITICAL: The tool failed to retrieve data. Do NOT claim that the requested facts were verified. You MUST acknowledge the error and either retry or inform the user that the information could not be retrieved.'
1111
+ : 'Use ONLY the specific facts present in this result when answering. Do NOT add, infer, or extrapolate information beyond what is explicitly provided. Do NOT fabricate additional details, dates, names, or relationships not present in this result. Do not echo raw JSON data — summarize in natural language while preserving factual accuracy.'
769
1112
  })
770
1113
  }
771
1114
  };
@@ -779,13 +1122,20 @@ ${toolsSummary}
779
1122
  }
780
1123
  }
781
1124
 
1125
+ const selfCritiqueDirective = await this._performSelfCritique(
1126
+ toolResultTracker,
1127
+ dominantLang,
1128
+ promptForLLM
1129
+ );
1130
+
782
1131
  const langPrompt = dominantLang === 'zh'
783
- ? "请根据上述工具查询结果,直接回答(不要开头问候语),用流畅优雅的中文,先1-2句概述,再列要点。包含具体的 YouTube 真实链接(如 Direct Watch: https://www.youtube.com/watch?v=... หรือ Search: https://www.youtube.com/results?search_query=...)及事实数据。"
1132
+ ? "请根据上述工具查询结果,为粉丝提供详细、全面且深入的解答。条理清晰地阐述作品背景、角色特质、音乐成就与演艺里程碑,包含具体的 YouTube 真实链接(如 Direct Watch: https://www.youtube.com/watch?v=... 或 Search: https://www.youtube.com/results?search_query=...)及完整事实数据。\n\n【反幻觉守则 — 必须遵守】只陈述工具结果中明确包含的事实。禁止编造、推测或补充工具结果中不存在的剧名、歌曲名、角色名、品牌名、日期或任何细节。如果工具结果不足以完整回答,请明确说明哪些部分未经验证,而不是凭空补全。"
784
1133
  : dominantLang === 'en'
785
- ? "Based on the tool results above, answer directly (no opening greeting or template phrase). Give a 1-2 sentence summary, then concise bullet points in polished English. Include verified YouTube direct and search links (https://www.youtube.com/results?search_query=...)."
786
- : "สรุปคำตอบจากผลลัพธ์เครื่องมือข้างต้น โดยไม่ต้องทักทาย ตอบตรงประเด็น 1-2 ประโยค แล้วตามด้วย bullet points สั้นๆ เป็นภาษาไทย พร้อมแสดงลิงก์ YouTube จริง (Direct Watch Link และ https://www.youtube.com/results?search_query=...) และข้อมูลผลงานที่ถูกต้องแม่นยำ";
1134
+ ? "Based on the tool results above, provide a comprehensive, richly detailed response for fans. Explain background storylines, character traits, musical highlights, and career milestones with well-organized Markdown headers, bullet points, and verified YouTube links (https://www.youtube.com/results?search_query=...).\n\n【Anti-Hallucination Mandate — MANDATORY】Only state facts explicitly present in the tool results above. Do NOT fabricate, infer, or supplement drama titles, song names, character roles, brand names, dates, or any details not found in the tool results. If the tool results are insufficient to fully answer, explicitly note which parts are unverified rather than inventing plausible-sounding details."
1135
+ : "สรุปคำตอบจากผลลัพธ์เครื่องมือข้างต้น โดยให้ข้อมูลแฟนคลับอย่างละเอียด ลึกซึ้ง ครบถ้วนและชัดเจน จัดโครงสร้างด้วยหัวข้อ Markdown, ตาราง และ bullet points ให้สวยงาม พร้อมแสดงลิงก์ YouTube จริง (Direct Watch Link และ https://www.youtube.com/results?search_query=...) พร้อมเกร็ดประวัติและผลงานอย่างเต็มที่\n\n【กฎป้องกันการแต่งข้อมูล — บังคับใช้】ระบุเฉพาะข้อเท็จจริงที่มีอยู่จริงในผลลัพธ์เครื่องมือเท่านั้น ห้ามสร้างชื่อละคร เพลง ตัวละคร แบรนด์ วันที่ หรือรายละเอียดใดๆ ที่ไม่มีอยู่ในผลลัพธ์เครื่องมือ หากข้อมูลไม่เพียงพอสำหรับคำตอบที่สมบูรณ์ ให้ระบุชัดเจนว่าส่วนใดยังไม่ได้รับการยืนยัน แทนการเติมข้อมูลที่ฟังดูน่าเชื่อแต่ไม่มีหลักฐาน";
787
1136
 
788
1137
  this.conversationHistory.push({ role: 'user', content: langPrompt });
1138
+ this.conversationHistory.push({ role: 'system', content: selfCritiqueDirective });
789
1139
 
790
1140
  if (this.stateMachine.state !== AgentStates.STREAMING_ANSWER) {
791
1141
  this.stateMachine.transition(AgentStates.STREAMING_ANSWER);
@@ -796,6 +1146,7 @@ ${toolsSummary}
796
1146
  const finalRes = await this.client.chat({
797
1147
  messages: this.conversationHistory,
798
1148
  temperature: 0.3,
1149
+ maxTokens: 8192,
799
1150
  stream: true,
800
1151
  signal,
801
1152
  onThought: (thoughtChunk) => {
@@ -821,29 +1172,147 @@ ${toolsSummary}
821
1172
  }
822
1173
  });
823
1174
 
1175
+ if (finalRes?.usage) {
1176
+ queryPromptTokens += Number(finalRes.usage.prompt_tokens || 0);
1177
+ queryCompletionTokens += Number(finalRes.usage.completion_tokens || 0);
1178
+ queryReasoningTokens += Number(finalRes.usage.reasoning_tokens || 0);
1179
+ }
1180
+
1181
+ const queryTokens = {
1182
+ prompt: queryPromptTokens,
1183
+ completion: queryCompletionTokens,
1184
+ reasoning: queryReasoningTokens,
1185
+ total: queryPromptTokens + queryCompletionTokens
1186
+ };
1187
+
1188
+ const queryCostUsd = calculateTokenCost(
1189
+ this.client?.provider || 'lmstudio',
1190
+ this.client?.model || 'google/gemma-4-e4b',
1191
+ queryPromptTokens,
1192
+ queryCompletionTokens
1193
+ );
1194
+
1195
+ this.session.recordQueryTokens(queryTokens, queryCostUsd);
1196
+ this.lastTelemetry = { tokens: queryTokens, costUsd: queryCostUsd };
1197
+
824
1198
  const totalDurationMs = Date.now() - queryStartTime;
825
1199
  this.session.stats.totalDurationMs += totalDurationMs;
826
1200
 
827
- const defaultDone = dominantLang === 'zh' ? "查询完毕" : dominantLang === 'en' ? "Search complete." : "เสร็จสิ้นการค้นหาข้อมูล";
828
- const output = finalRes?.content || defaultDone;
829
- onFinalAnswer(output, { durationMs: totalDurationMs, language: dominantLang });
1201
+ const defaultDone =
1202
+ dominantLang === 'zh' ? "查询完毕" :
1203
+ dominantLang === 'ja' ? "検索が完了しました" :
1204
+ dominantLang === 'ko' ? "검색이 완료되었습니다" :
1205
+ dominantLang === 'en' ? "Search complete." : "เสร็จสิ้นการค้นหาข้อมูล";
1206
+ const output = sanitizeResponse(finalRes?.content || defaultDone);
1207
+ const citations = extractCitations(output, recordedTools);
1208
+ this.lastCitations = citations;
1209
+ if (currentAssistantSessionMsg) {
1210
+ currentAssistantSessionMsg.citations = citations;
1211
+ }
1212
+
1213
+ onFinalAnswer(output, {
1214
+ durationMs: totalDurationMs,
1215
+ language: dominantLang,
1216
+ tokens: queryTokens,
1217
+ costUsd: queryCostUsd,
1218
+ citations
1219
+ });
830
1220
 
831
1221
  this._extractFactsFromFinalAnswer(output);
832
1222
 
833
1223
  this.stateMachine.transition(AgentStates.COMPLETE);
834
- eventBus.emit(Events.AGENT_FINAL_ANSWER, { queryId, answer: output, durationMs: totalDurationMs });
835
- eventBus.emit(Events.AGENT_QUERY_END, { queryId, durationMs: totalDurationMs });
836
- this.stateMachine.reset();
837
-
838
- this.saveSession().catch(err => console.error('Session save failed:', err));
1224
+ eventBus.emit(Events.AGENT_FINAL_ANSWER, {
1225
+ queryId,
1226
+ answer: output,
1227
+ durationMs: totalDurationMs,
1228
+ tokens: queryTokens,
1229
+ costUsd: queryCostUsd,
1230
+ citations
1231
+ });
1232
+ eventBus.emit(Events.AGENT_QUERY_END, {
1233
+ queryId,
1234
+ durationMs: totalDurationMs,
1235
+ tokens: queryTokens,
1236
+ costUsd: queryCostUsd,
1237
+ citations
1238
+ });
1239
+ await this.saveSession().catch(err => console.error('Session save failed:', err));
839
1240
 
840
1241
  return output;
841
1242
  }
842
1243
 
1244
+ async run(userPrompt, callbacks = {}) {
1245
+ return this.ask(userPrompt, callbacks);
1246
+ }
1247
+
1248
+ _computeConfidenceScore(toolResultTracker) {
1249
+ if (toolResultTracker.total === 0) return 0;
1250
+
1251
+ const successRatio = toolResultTracker.success / toolResultTracker.total;
1252
+ const errorPenalty = toolResultTracker.error * 0.5;
1253
+ const notFoundPenalty = toolResultTracker.notFound * 0.35;
1254
+ const base = Math.max(0, successRatio - errorPenalty - notFoundPenalty);
1255
+
1256
+ const hasVerifiedSource = toolResultTracker.success > 0;
1257
+ const allFailed = toolResultTracker.success === 0 && (toolResultTracker.notFound > 0 || toolResultTracker.error > 0);
1258
+
1259
+ if (allFailed) return Math.min(base, 0.2);
1260
+ if (!hasVerifiedSource) return Math.min(base, 0.4);
1261
+
1262
+ return Math.max(0.1, Math.min(1, base));
1263
+ }
1264
+
1265
+ async _performSelfCritique(toolResultTracker, dominantLang, originalQuery) {
1266
+ const confidence = this._computeConfidenceScore(toolResultTracker);
1267
+
1268
+ const th = dominantLang === 'th';
1269
+ const zh = dominantLang === 'zh';
1270
+
1271
+ if (confidence <= 0.35) {
1272
+ const prefix = th
1273
+ ? '【คำสั่งระบบเชิงวิพากษ์ — ลดระดับความมั่นใจต่ำ】'
1274
+ : zh
1275
+ ? '【系统自我审查 — 证据不足】'
1276
+ : '【SYSTEM SELF-CRITIQUE — EVIDENCE INSUFFICIENT】';
1277
+ const body = th
1278
+ ? 'เครื่องมือที่เรียกใช้ส่วนใหญ่ (หรือทั้งหมด) ไม่พบข้อมูลที่ยืนยันได้ หรือเกิดข้อผิดพลาด ความมั่นใจในความถูกต้องของข้อมูลต่ำในระดับที่จะตอบได้อย่างสมบูรณ์ ดังนั้นคุณต้องปฏิบัติตามนี้: (1) ห้ามแต่งเติมหรือคาดเดารายละเอียดใดๆ (ชื่อผลงาน, นักแสดง, วันที่, แบรนด์) (2) ระบุอย่างตรงไปตรงมาว่าไม่พบข้อมูลที่ยืนยันได้สำหรับการสอบถามนี้ และ (3) แนะนำวิธีอื่นให้ผู้ใช้ค้นหาข้อมูล เช่น ถามใหม่ด้วยคำค้นหาอื่น หรือรอการประกาศอย่างเป็นทางการ การตอบแบบ "ไม่มีข้อมูลอย่างเป็นทางการ" ดีกว่าการสร้างคำตอบที่สมบูรณ์แต่ไม่มีหลักฐานเสมอ'
1279
+ : zh
1280
+ ? '您请求的信息在已验证的数据库中未能找到充分的证据支持。您必须遵循以下规则:(1) 严禁编造、推测或虚构任何清晰信息(作品名称、演员、日期、品牌);(2) 如实说明当前没有找到可核实的记录;(3) 建议用户换一种问法或等待官方公告。始终优先说"未找到官方信息",而不是生成完整但缺乏依据的描述。'
1281
+ : 'The verification tools returned insufficient or no confirmed evidence for this query. You MUST follow these rules: (1) Do NOT fabricate, infer, or invent any concrete details (titles, casts, dates, brands); (2) Honestly state that no verified record could be found; (3) Suggest alternative ways for the user to find the information. Always prefer stating "No official information found" over generating a complete but ungrounded description.';
1282
+ return `${prefix}\n${body}\nOriginal query: ${originalQuery}`;
1283
+ }
1284
+
1285
+ if (confidence >= 0.7) {
1286
+ const prefix = th
1287
+ ? '【คำสั่งระบบเชิงวิพากษ์ — แหล่งข้อมูลแข็งแกร่ง】'
1288
+ : zh
1289
+ ? '【系统自我审查 — 证据充分】'
1290
+ : '【SYSTEM SELF-CRITIQUE — EVIDENCE STRONG】';
1291
+ const body = th
1292
+ ? 'เครื่องมือยืนยันข้อมูลได้สำเร็จเพียงพอสำหรับการตั้งคำถามนี้ คุณสามารถตอบได้อย่างมั่นใจโดยอ้างอิงเฉพาะข้อเท็จจริงที่ปรากฏในผลลัพธ์เครื่องมือเท่านั้น ยังคงห้ามเติมรายละเอียดที่ไม่มีในผลลัพธ์เครื่องมือ'
1293
+ : zh
1294
+ ? '工具已成功验证足够的数据。您可以自信地回答,但仅基于工具结果中呈现的事实。仍然禁止添加工具结果中不存在的细节。'
1295
+ : 'Tool results successfully verified sufficient data for this query. You may answer with confidence, but ONLY based on what is present in the tool results. Do NOT add details not present in the tool output.';
1296
+ return `${prefix}\n${body}`;
1297
+ }
1298
+
1299
+ const prefix = th
1300
+ ? '【คำสั่งระบบเชิงวิพากษ์ — ความมั่นใจปานกลาง】'
1301
+ : zh
1302
+ ? '【系统自我审查 — 需谨慎】'
1303
+ : '【SYSTEM SELF-CRITIQUE — PROCEED WITH CAUTION】';
1304
+ const body = th
1305
+ ? 'เครื่องมือให้ข้อมูลบางส่วนแต่ยังไม่ครบถ้วนสมบูรณ์ ตรวจสอบคำตอบของคุณก่อนส่งให้ผู้ใช้: (1) ระบุเฉพาะข้อเท็จจริงที่ยืนยันโดยผลลัพธ์เครื่องมือเท่านั้น (2) ส่วนที่ยังไม่ยืนยัน ให้ระบุชัดเจนว่า "ยังไม่ได้รับการยืนยัน" หรือ "รายงานไม่ชัดเจน" (3) ห้ามเติมโครงเรื่อง การคัดเลือกนักแสดง หรือวันที่ที่ไม่มีแหล่งที่มา'
1306
+ : zh
1307
+ ? '工具提供部分但非完整的信息。在回答前自查:(1) 只陈述工具结果已确认的事实;(2) 明确标记仍未确认的部分为"未验证"或"未官方确认";(3) 不要补充没有来源支持的剧情、选角或日期。'
1308
+ : 'Tool results provide partial but incomplete information. Before answering, self-check: (1) Only state facts confirmed by tool results. (2) Explicitly label unconfirmed aspects as "unverified" or "not officially confirmed." (3) Do not supplement plotlines, casting, or dates without supporting source.';
1309
+ return `${prefix}\n${body}`;
1310
+ }
1311
+
843
1312
  _capToolResultForHistory(result) {
844
1313
  if (!result || typeof result !== 'object') return result;
845
1314
 
846
- const MAX_RESULT_BYTES = 4096;
1315
+ const MAX_RESULT_BYTES = 16384;
847
1316
  const serialized = JSON.stringify(result);
848
1317
  if (serialized.length <= MAX_RESULT_BYTES) return result;
849
1318
 
@@ -866,7 +1335,7 @@ ${toolsSummary}
866
1335
  }
867
1336
 
868
1337
  if (typeof obj === 'string' && obj.length > 500) {
869
- return obj.slice(0, 500) + '…[truncated]';
1338
+ return `${obj.slice(0, 500)}…[truncated]`;
870
1339
  }
871
1340
  return obj;
872
1341
  };
@@ -876,7 +1345,7 @@ ${toolsSummary}
876
1345
  return capped;
877
1346
  }
878
1347
 
879
- _extractFactsFromToolResult(toolName, toolArgs, result) {
1348
+ _extractFactsFromToolResult(toolName, _toolArgs, result) {
880
1349
  if (!result || result.status === 'error' || result.status === 'not_found') return;
881
1350
 
882
1351
  const facts = [];
@@ -898,7 +1367,7 @@ ${toolsSummary}
898
1367
 
899
1368
  case 'get_work_detail': {
900
1369
  const w = result.work;
901
- if (w && w.titleCn) {
1370
+ if (w?.titleCn) {
902
1371
  facts.push({
903
1372
  content: `${w.titleCn} (${w.titleEn || w.titleTh || ''}) — ${w.role || 'Actor'} | ${w.platform || ''} | ${w.year || ''}`,
904
1373
  category: 'drama',
@@ -1029,9 +1498,8 @@ ${toolsSummary}
1029
1498
  if (!answer || answer.length < 20) return;
1030
1499
 
1031
1500
  const ytRegex = /https?:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/g;
1032
- let match;
1033
1501
  const ytIds = new Set();
1034
- while ((match = ytRegex.exec(answer)) !== null) {
1502
+ for (const match of answer.matchAll(ytRegex)) {
1035
1503
  ytIds.add(match[1]);
1036
1504
  }
1037
1505
  if (ytIds.size > 0) {