kiku-agent 1.2.0 → 1.3.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.
- package/README.md +459 -407
- package/package.json +27 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +629 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +528 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +383 -179
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AgentStateMachine, AgentStates } from './agentState.js';
|
|
2
|
+
import { buildSystemPrompt } from './prompts.js';
|
|
2
3
|
import { config } from './config.js';
|
|
3
|
-
import { contextCompactor } from './contextCompactor.js';
|
|
4
|
+
import { contextCompactor, compactToolResultForContext, estimateTextTokens, estimateTokenCount } from './contextCompactor.js';
|
|
4
5
|
import { createAbortError, isAbortError } from './errors.js';
|
|
5
6
|
import { Events, eventBus } from './eventBus.js';
|
|
6
7
|
import { AgentJobManager } from './jobManager.js';
|
|
@@ -12,15 +13,22 @@ import { sanitizeQuery, sanitizeResponse } from './sanitizer.js';
|
|
|
12
13
|
import { AgentSession } from './session.js';
|
|
13
14
|
import { ResearchSubagent } from './subagent.js';
|
|
14
15
|
import { SuggestionEngine } from './suggestions.js';
|
|
16
|
+
import { buildTextToolProtocolDirective, extractTopLevelJsonObjects, MALFORMED_TOOL_CALL_FEEDBACK, parseTextToolCallsFromContent, stripToolCallBlocks } from './textToolProtocol.js';
|
|
15
17
|
import { DynamicToolRouter } from './toolRouter.js';
|
|
16
18
|
import { executeTool, prepareToolArguments, toolsSchema, isBuiltinTool } from './tools.js';
|
|
17
19
|
import { detectAmbiguities, isQueryTooVague, emitClarifyingQuestions, calculateConfidence, generateClarifyingQuestions, buildClarificationContext } from './clarifyingQuestions.js';
|
|
18
20
|
import { ClarificationAnalytics, ClarificationConfig } from './clarificationAnalytics.js';
|
|
19
21
|
import { calculateTokenCost } from './telemetry.js';
|
|
20
22
|
import { extractCitations, resolveCitationReference } from './citations.js';
|
|
23
|
+
import { resolveQueryEntities } from './catalogEntityResolver.js';
|
|
24
|
+
import { TokenTracker } from './tokenTracker.js';
|
|
25
|
+
import { RoutingInterceptor } from './routingInterceptor.js';
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
|
|
28
|
+
const OUTPUT_TOKEN_BUDGET = Math.max(512, Number(process.env.KIKU_MAX_OUTPUT_TOKENS) || 4096);
|
|
29
|
+
|
|
30
|
+
export function detectDominantLanguage(text, sessionLang = null) {
|
|
31
|
+
if (!text || typeof text !== 'string') return sessionLang || 'th';
|
|
24
32
|
|
|
25
33
|
const clean = text
|
|
26
34
|
.replace(/\[SYSTEM\s+[A-Z]+:[^\]]*\]/gi, '')
|
|
@@ -32,21 +40,19 @@ export function detectDominantLanguage(text) {
|
|
|
32
40
|
|
|
33
41
|
const targetText = clean || text;
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const englishCount = englishMatches.join('').length;
|
|
49
|
-
|
|
43
|
+
let thaiCount = 0;
|
|
44
|
+
let chineseCount = 0;
|
|
45
|
+
let kanaCount = 0;
|
|
46
|
+
let hangulCount = 0;
|
|
47
|
+
let englishCount = 0;
|
|
48
|
+
for (const ch of targetText) {
|
|
49
|
+
const code = ch.codePointAt(0);
|
|
50
|
+
if (code >= 0x0E00 && code <= 0x0E7F) thaiCount++;
|
|
51
|
+
else if (code >= 0x3040 && code <= 0x30FF) kanaCount++;
|
|
52
|
+
else if (code >= 0x4E00 && code <= 0x9FA5) chineseCount++;
|
|
53
|
+
else if (code >= 0xAC00 && code <= 0xD7AF) hangulCount++;
|
|
54
|
+
else if ((code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A)) englishCount++;
|
|
55
|
+
}
|
|
50
56
|
|
|
51
57
|
if (kanaCount > 0) {
|
|
52
58
|
return 'ja';
|
|
@@ -56,15 +62,31 @@ export function detectDominantLanguage(text) {
|
|
|
56
62
|
return 'ko';
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
|
|
66
|
+
if (thaiCount > 0) {
|
|
67
|
+
if (thaiCount >= 2 || thaiCount >= chineseCount) {
|
|
68
|
+
return 'th';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (chineseCount > 0 && chineseCount >= thaiCount && chineseCount >= englishCount) {
|
|
62
73
|
return 'zh';
|
|
63
|
-
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (thaiCount > 0) {
|
|
77
|
+
return 'th';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if (sessionLang && sessionLang !== 'en' && (targetText.match(/[a-zA-Z]+/g) || []).length <= 3 && !/\b(what|who|when|where|why|how|which|can|tell|give|list|show|is|are|was|were)\b/i.test(targetText)) {
|
|
82
|
+
return sessionLang;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (englishCount > 0) {
|
|
64
86
|
return 'en';
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
return 'th';
|
|
89
|
+
return sessionLang || 'th';
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
function stableToolSignature(toolName, args) {
|
|
@@ -78,11 +100,81 @@ function stableToolSignature(toolName, args) {
|
|
|
78
100
|
return `${toolName}:${JSON.stringify(sortValue(args || {}))}`;
|
|
79
101
|
}
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
|
|
104
|
+
export { extractTopLevelJsonObjects };
|
|
105
|
+
|
|
106
|
+
const REPEAT_LOOP_THRESHOLD = 3;
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
function summarizeToolDescription(description, maxChars = 160) {
|
|
110
|
+
const text = String(description || '').replace(/\s+/g, ' ').trim();
|
|
111
|
+
if (text.length <= maxChars) return text;
|
|
112
|
+
const cut = text.slice(0, maxChars);
|
|
113
|
+
const lastStop = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('; '));
|
|
114
|
+
return lastStop > 60 ? cut.slice(0, lastStop + 1) : `${cut.trim()}…`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
export function stripInjectedDirectives(text) {
|
|
119
|
+
let t = String(text || '');
|
|
120
|
+
t = t.replace(/\[SYSTEM DIRECTIVE[\s\S]*$/i, '');
|
|
121
|
+
t = t.replace(/\[SYSTEM CLARIFICATION\][\s\S]*$/i, '');
|
|
122
|
+
t = t.replace(/\[CONTEXT GROUNDING:[^\]]*\]/gi, '');
|
|
123
|
+
t = t.replace(/\[RESTORED SESSION TOOL CONTEXT\][\s\S]*$/i, '');
|
|
124
|
+
return t.trim();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const AUTO_RESEARCH_KEYWORDS = [
|
|
128
|
+
|
|
129
|
+
'วิเคราะห์', 'เปรียบเทียบ', 'เจาะลึก', 'ลงลึก', 'วิจัย', 'ค้นคว้า', 'รายงาน', 'ข่าวลือ', 'ตรวจสอบ', 'รวบรวม', 'ครบถ้วน',
|
|
130
|
+
|
|
131
|
+
'analyze', 'analysis', 'compare', 'research', 'investigate', 'deep dive', 'in depth', 'in-depth', 'comprehensive', 'everything about', 'report on', 'rumor',
|
|
132
|
+
|
|
133
|
+
'分析', '比较', '對比', '调查', '研究', '深入', '全面',
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
export function scoreResearchComplexity(prompt) {
|
|
137
|
+
const text = stripInjectedDirectives(prompt);
|
|
138
|
+
const signals = [];
|
|
139
|
+
let score = 0;
|
|
140
|
+
if (!text) return { score, signals, strippedLength: 0 };
|
|
141
|
+
|
|
142
|
+
if (text.length >= 240) { score += 1; signals.push('length'); }
|
|
143
|
+
if (text.length >= 600) { score += 1; signals.push('very-long'); }
|
|
144
|
+
|
|
145
|
+
const questionMarks = (text.match(/[??]/g) || []).length;
|
|
146
|
+
if (questionMarks >= 2) { score += 1; signals.push('multi-question'); }
|
|
147
|
+
|
|
148
|
+
const listMarkers = (text.match(/(^|\n)\s*(?:\d+[.)]|[-•*])\s+\S/g) || []).length;
|
|
149
|
+
if (listMarkers >= 2) { score += 1; signals.push('list'); }
|
|
150
|
+
|
|
151
|
+
const lower = text.toLowerCase();
|
|
152
|
+
const matchedKeywords = AUTO_RESEARCH_KEYWORDS.filter(k => lower.includes(k));
|
|
153
|
+
if (matchedKeywords.length >= 1) { score += 1; signals.push(`keywords:${matchedKeywords.slice(0, 3).join('|')}`); }
|
|
154
|
+
if (matchedKeywords.length >= 3) { score += 1; signals.push('keyword-rich'); }
|
|
155
|
+
|
|
156
|
+
return { score, signals, strippedLength: text.length };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
export function shouldAutoResearch(prompt, options = {}) {
|
|
161
|
+
const mode = String(options.mode ?? 'auto').toLowerCase();
|
|
162
|
+
if (mode === 'off') return false;
|
|
163
|
+
const { score } = scoreResearchComplexity(prompt);
|
|
164
|
+
if (mode === 'on') return score >= 1;
|
|
165
|
+
return score >= (Number(options.threshold) || 3);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildAutoResearchDirective(complexity) {
|
|
169
|
+
const signalText = (complexity.signals || []).slice(0, 4).join(', ');
|
|
170
|
+
return `[SYSTEM DIRECTIVE: AUTO DEEP RESEARCH] The user's query is complex and multi-part (complexity score ${complexity.score}${signalText ? ` — ${signalText}` : ''}). Do NOT answer it with a few quick tool calls. You MUST begin by calling run_deep_research ONCE with a focused, self-contained question derived from the user's request. After the research report returns, synthesize the final answer from its cited findings. If run_deep_research fails or returns insufficient data, fill the gaps with web_search/web_extract instead of leaving any section unanswered.`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class KikuAgent {
|
|
174
|
+
constructor(options = {}) {
|
|
175
|
+
this.allowedToolNames = options.allowedToolNames
|
|
176
|
+
? new Set(Array.from(options.allowedToolNames, String))
|
|
177
|
+
: null;
|
|
86
178
|
this.client = options.client || new LLMClient({
|
|
87
179
|
provider: options.provider || config.activeProvider,
|
|
88
180
|
baseUrl: options.baseUrl,
|
|
@@ -94,13 +186,18 @@ export class KikuAgent {
|
|
|
94
186
|
this.session = new AgentSession({
|
|
95
187
|
thinkingMode: options.thinkingMode || 'hide',
|
|
96
188
|
sessionId: options.sessionId,
|
|
97
|
-
storageKey: options.sessionStorageKey,
|
|
98
|
-
storageFile: options.sessionStorageFile,
|
|
99
|
-
persistence: options.sessionPersistence !== false,
|
|
189
|
+
storageKey: options.sessionStorageKey,
|
|
190
|
+
storageFile: options.sessionStorageFile,
|
|
191
|
+
persistence: options.sessionPersistence !== false,
|
|
100
192
|
});
|
|
101
193
|
this.memory = options.memory || new AgentMemory();
|
|
194
|
+
this.activeRunId = options.activeRunId || options.runId || null;
|
|
195
|
+
if (this.activeRunId && typeof this.memory.setActiveRunId === 'function') {
|
|
196
|
+
this.memory.setActiveRunId(this.activeRunId);
|
|
197
|
+
}
|
|
102
198
|
this.mcpManager = options.mcpManager || new McpClientManager();
|
|
103
199
|
this.router = new DynamicToolRouter(this.getAllToolsSchema());
|
|
200
|
+
this.routingInterceptor = new RoutingInterceptor();
|
|
104
201
|
this.research = new ResearchOrchestrator({ client: this.client });
|
|
105
202
|
this.suggestions = new SuggestionEngine({ client: this.client, memory: this.memory });
|
|
106
203
|
this.jobs = new AgentJobManager();
|
|
@@ -110,6 +207,8 @@ export class KikuAgent {
|
|
|
110
207
|
this.initHistory();
|
|
111
208
|
this.restoredMessageCount = 0;
|
|
112
209
|
this.lastCitations = [];
|
|
210
|
+
this.lastQueryMetrics = null;
|
|
211
|
+
this.queryMetricsHistory = [];
|
|
113
212
|
|
|
114
213
|
this.stateMachine = new AgentStateMachine();
|
|
115
214
|
|
|
@@ -133,7 +232,12 @@ export class KikuAgent {
|
|
|
133
232
|
await this.mcpManager.loadPersistedServers();
|
|
134
233
|
this.router.setTools(this.getAllToolsSchema());
|
|
135
234
|
this.systemPrompt = this.buildSystemPrompt();
|
|
136
|
-
} catch {
|
|
235
|
+
} catch (err) {
|
|
236
|
+
|
|
237
|
+
this.mcpInitError = err?.message || String(err);
|
|
238
|
+
console.warn('[Kiku] Failed to load persisted MCP servers:', this.mcpInitError);
|
|
239
|
+
eventBus.emit(Events.AGENT_QUERY_ERROR, { type: 'mcp_init', error: this.mcpInitError });
|
|
240
|
+
}
|
|
137
241
|
}
|
|
138
242
|
if (!this.conversationHistory || this.conversationHistory.length === 0) {
|
|
139
243
|
this.initHistory();
|
|
@@ -360,6 +464,52 @@ export class KikuAgent {
|
|
|
360
464
|
return this.endSession();
|
|
361
465
|
}
|
|
362
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Summarize tool evidence from persisted session messages. Raw tool payloads
|
|
469
|
+
* are not rehydrated into LLM history (they violate tool-call pairing after
|
|
470
|
+
* a restart and bloat the window), so the model gets one compact evidence
|
|
471
|
+
* line per earlier query instead.
|
|
472
|
+
*/
|
|
473
|
+
_rehydrateSessionMessages() {
|
|
474
|
+
const evidenceLines = [];
|
|
475
|
+
const messages = this.session.messages || [];
|
|
476
|
+
for (let i = 0; i < messages.length; i++) {
|
|
477
|
+
const msg = messages[i];
|
|
478
|
+
if (msg.role !== 'assistant') continue;
|
|
479
|
+
const toolParts = (msg.parts || []).filter(p => p.type === 'tool');
|
|
480
|
+
if (toolParts.length === 0) continue;
|
|
481
|
+
|
|
482
|
+
const textPart = (msg.parts || []).find(p => p.type === 'text');
|
|
483
|
+
let userQuestion = '';
|
|
484
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
485
|
+
if (messages[j].role === 'user') {
|
|
486
|
+
const part = (messages[j].parts || []).find(p => p.type === 'text');
|
|
487
|
+
userQuestion = String(part?.content || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const toolSummary = toolParts
|
|
492
|
+
.map(p => {
|
|
493
|
+
const status = p.status || p.result?.status || 'ok';
|
|
494
|
+
const count = p.result?.count ?? p.result?.returnedCount;
|
|
495
|
+
return `${p.name || 'tool'}(${status}${count !== undefined ? `, ${count}` : ''})`;
|
|
496
|
+
})
|
|
497
|
+
.join(', ');
|
|
498
|
+
const answerSnippet = String(textPart?.content || '').replace(/\s+/g, ' ').trim().slice(0, 100);
|
|
499
|
+
evidenceLines.push(`- Q: "${userQuestion || '(no text)'}" → ${toolSummary}${answerSnippet ? ` → answered: "${answerSnippet}"` : ''}`);
|
|
500
|
+
}
|
|
501
|
+
return evidenceLines;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
_pushRestoredToolContext() {
|
|
505
|
+
const evidenceLines = this._rehydrateSessionMessages();
|
|
506
|
+
if (evidenceLines.length === 0) return;
|
|
507
|
+
this.conversationHistory.push({
|
|
508
|
+
role: 'system',
|
|
509
|
+
content: `[RESTORED SESSION TOOL CONTEXT] The queries below were already answered with verified tool results earlier in this session (the raw tool outputs are no longer in history):\n${evidenceLines.slice(-20).join('\n')}\nTreat these as previously verified facts. Re-call the relevant tools only if the user needs fresh or deeper detail.`
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
363
513
|
async loadSession() {
|
|
364
514
|
const loaded = await this.session.loadFromDisk();
|
|
365
515
|
if (loaded && this.session.messages.length > 0) {
|
|
@@ -389,6 +539,7 @@ export class KikuAgent {
|
|
|
389
539
|
}
|
|
390
540
|
}
|
|
391
541
|
}
|
|
542
|
+
this._pushRestoredToolContext();
|
|
392
543
|
this.restoredMessageCount = this.session.messages.length;
|
|
393
544
|
}
|
|
394
545
|
}
|
|
@@ -441,6 +592,7 @@ export class KikuAgent {
|
|
|
441
592
|
}
|
|
442
593
|
}
|
|
443
594
|
}
|
|
595
|
+
this._pushRestoredToolContext();
|
|
444
596
|
this.restoredMessageCount = this.session.messages.length;
|
|
445
597
|
eventBus.emit(Events.SESSION_SAVED, { sessionId: this.session.id });
|
|
446
598
|
return archived;
|
|
@@ -509,21 +661,100 @@ export class KikuAgent {
|
|
|
509
661
|
return { hydrated: true, messageCount: normalized.length };
|
|
510
662
|
}
|
|
511
663
|
|
|
512
|
-
|
|
664
|
+
_isLocalProvider() {
|
|
665
|
+
const base = String(this.client?.baseUrl || '');
|
|
666
|
+
if (/localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/i.test(base)) return true;
|
|
667
|
+
return this.client?.provider === 'lmstudio' && !base;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async applySlidingWindow() {
|
|
513
671
|
if (this.conversationHistory.length > this.slidingWindowSize + 1) {
|
|
514
|
-
|
|
672
|
+
const options = {
|
|
515
673
|
maxTokens: 24000,
|
|
516
674
|
keepRecentMessages: this.slidingWindowSize,
|
|
517
|
-
}
|
|
675
|
+
};
|
|
676
|
+
if (!this._isLocalProvider()) {
|
|
677
|
+
options.summarizer = (older) => this._llmSummarizeOlderMessages(older);
|
|
678
|
+
}
|
|
679
|
+
this.conversationHistory = await contextCompactor.compactAsync(this.conversationHistory, options);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** LLM summary of the dropped window (cloud providers only) — falls back to
|
|
684
|
+
* the heuristic summary inside the compactor when this fails. */
|
|
685
|
+
async _llmSummarizeOlderMessages(olderMessages) {
|
|
686
|
+
const transcript = (olderMessages || [])
|
|
687
|
+
.map(m => {
|
|
688
|
+
const role = m.role === 'tool' ? `tool(${m.name || '?'})` : m.role;
|
|
689
|
+
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? m.tool_calls ?? '');
|
|
690
|
+
return `${role}: ${content.slice(0, 400)}`;
|
|
691
|
+
})
|
|
692
|
+
.join('\n')
|
|
693
|
+
.slice(0, 6000);
|
|
694
|
+
|
|
695
|
+
if (!transcript.trim()) return '';
|
|
696
|
+
const res = await Promise.race([
|
|
697
|
+
this.client.chat({
|
|
698
|
+
messages: [
|
|
699
|
+
{ role: 'system', content: 'Summarize the following conversation excerpt for an assistant context window. Capture what the user asked, which tools were used, and the key verified facts. Maximum 120 words. Output only the summary text.' },
|
|
700
|
+
{ role: 'user', content: transcript }
|
|
701
|
+
],
|
|
702
|
+
temperature: 0.1,
|
|
703
|
+
maxTokens: 300,
|
|
704
|
+
stream: false
|
|
705
|
+
}),
|
|
706
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('summary timeout')), 20_000))
|
|
707
|
+
]);
|
|
708
|
+
return String(res?.content || '').trim();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Hard context-budget guard: KIKU_CONTEXT_LIMIT becomes a real budget instead
|
|
713
|
+
* of telemetry. Before each LLM call, if the estimated prompt exceeds 85% of
|
|
714
|
+
* the limit, replace the oldest tool results with omission stubs — tool
|
|
715
|
+
* payloads are the bulkiest and least-likely-to-be-reused part of history.
|
|
716
|
+
*/
|
|
717
|
+
_enforceContextBudget() {
|
|
718
|
+
const rawLimit = Number(process.env.KIKU_CONTEXT_LIMIT);
|
|
719
|
+
if (!Number.isFinite(rawLimit) || rawLimit <= 0) return false;
|
|
720
|
+
const budget = Math.floor(rawLimit * 0.85);
|
|
721
|
+
|
|
722
|
+
let total = 0;
|
|
723
|
+
for (const msg of this.conversationHistory) {
|
|
724
|
+
if (typeof msg._tokenEstimate !== 'number') {
|
|
725
|
+
msg._tokenEstimate = estimateTokenCount([msg]);
|
|
726
|
+
}
|
|
727
|
+
total += msg._tokenEstimate;
|
|
518
728
|
}
|
|
729
|
+
if (total <= budget) return false;
|
|
730
|
+
|
|
731
|
+
const omitted = JSON.stringify({
|
|
732
|
+
authoritative: false,
|
|
733
|
+
status: 'omitted',
|
|
734
|
+
message: 'Older tool result was omitted to fit the context window; its facts are no longer visible. Re-call the tool only if essential.'
|
|
735
|
+
});
|
|
736
|
+
const omittedTokens = estimateTextTokens(omitted);
|
|
737
|
+
let trimmed = false;
|
|
738
|
+
|
|
739
|
+
for (const msg of this.conversationHistory) {
|
|
740
|
+
if (total <= budget) break;
|
|
741
|
+
if (msg.role !== 'tool' || msg._omittedForContext) continue;
|
|
742
|
+
const prev = msg._tokenEstimate || estimateTokenCount([msg]);
|
|
743
|
+
total -= prev - omittedTokens;
|
|
744
|
+
msg.content = omitted;
|
|
745
|
+
msg._omittedForContext = true;
|
|
746
|
+
msg._tokenEstimate = omittedTokens;
|
|
747
|
+
trimmed = true;
|
|
748
|
+
}
|
|
749
|
+
return trimmed;
|
|
519
750
|
}
|
|
520
751
|
|
|
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
|
-
}
|
|
752
|
+
getAllToolsSchema() {
|
|
753
|
+
const mcpTools = this.mcpManager ? this.mcpManager.getToolsSchema() : [];
|
|
754
|
+
const allTools = [...toolsSchema, ...mcpTools];
|
|
755
|
+
if (!this.allowedToolNames) return allTools;
|
|
756
|
+
return allTools.filter(tool => this.allowedToolNames.has(tool.function?.name));
|
|
757
|
+
}
|
|
527
758
|
|
|
528
759
|
async connectMcp(name, serverConfig) {
|
|
529
760
|
const result = await this.mcpManager.connect(name, serverConfig);
|
|
@@ -586,180 +817,18 @@ export class KikuAgent {
|
|
|
586
817
|
return results;
|
|
587
818
|
}
|
|
588
819
|
|
|
589
|
-
buildSystemPrompt() {
|
|
590
|
-
const allTools = this.getAllToolsSchema();
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
const f = t.function;
|
|
594
|
-
const paramKeys = Object.keys(f.parameters?.properties || {}).join(', ');
|
|
595
|
-
return `- ${f.name}(${paramKeys}): ${f.description}`;
|
|
596
|
-
}).join('\n');
|
|
597
|
-
|
|
598
|
-
return `${config.agent.systemPersona}
|
|
599
|
-
|
|
600
|
-
=== AVAILABLE KNOWLEDGE & LIVE TOOLS ===
|
|
601
|
-
${toolsSummary}
|
|
602
|
-
|
|
603
|
-
=== DYNAMIC MULTILINGUAL & RESPONSE RULES (CRITICAL) ===
|
|
604
|
-
|
|
605
|
-
0. NO SYSTEM LEAK (ABSOLUTE RULE):
|
|
606
|
-
- NEVER include "Thinking Process", "Tool Calling", "Action:", "Action Input:", function names, parameter schemas, JSON tool call blocks, or any internal reasoning in your response text.
|
|
607
|
-
- NEVER include advertisements, sponsor mentions, or promotional content.
|
|
608
|
-
- Your response must be CLEAN user-facing content ONLY.
|
|
609
|
-
- Think silently using tools, then present ONLY the final polished answer.
|
|
610
|
-
|
|
611
|
-
1. LANGUAGE MIRRORING:
|
|
612
|
-
- THAI ➔ polite Thai (ใช้ ค่ะ/นะคะ เสมอ)
|
|
613
|
-
- CHINESE ➔ fluent, polite Chinese (简体/繁体中文)
|
|
614
|
-
- ENGLISH ➔ polished, professional English
|
|
615
|
-
- MIXED ➔ Match dominant language, keep bilingual titles
|
|
616
|
-
|
|
617
|
-
2. REAL YOUTUBE & MEDIA LINKS:
|
|
618
|
-
- Direct Watch: https://www.youtube.com/watch?v=VIDEO_ID
|
|
619
|
-
- Search: https://www.youtube.com/results?search_query=QUERY
|
|
620
|
-
- Also Bilibili, QQ Music, Youku when applicable
|
|
621
|
-
- NEVER use fake placeholder IDs
|
|
622
|
-
|
|
623
|
-
3. FACT ACCURACY & NO DUPLICATION:
|
|
624
|
-
- Always use tools to verify facts before answering. Do NOT hallucinate.
|
|
625
|
-
- Current date is ${today}. Prioritize current and recent projects when relevant.
|
|
626
|
-
- Never repeat the same statement. Sort newest first.
|
|
627
|
-
|
|
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.
|
|
664
|
-
- News/external facts ➔ web_search or web_extract
|
|
665
|
-
- Deep multi-source research ➔ web_extract (search + auto-fetch)
|
|
666
|
-
- Read webpage content ➔ fetch_webpage
|
|
667
|
-
- Fetch API/JSON ➔ fetch_json
|
|
668
|
-
- Screenshot ➔ screenshot_webpage
|
|
669
|
-
|
|
670
|
-
6. AGGRESSIVE WEB SEARCH FALLBACK (CRITICAL):
|
|
671
|
-
- If a local catalog tool (e.g. search_works, search_discography, get_brand_endorsements, get_awards) returns not_found, empty, or 0 results, DO NOT give up or guess. You MUST IMMEDIATELY call web_search or web_extract in the next step to look up live external facts online.
|
|
672
|
-
- Combine local catalog data WITH live web search for complete, accurate, and up-to-date answers.
|
|
673
|
-
- Use web_extract for comprehensive research on any topic.
|
|
674
|
-
|
|
675
|
-
7. LEGAL & CAREER KNOWLEDGE:
|
|
676
|
-
- When discussing controversies, be specific and accurate:
|
|
677
|
-
• Contract dispute: Star48/Siba Media supplementary agreement. JJY claims main contract ended June 2024, alleges forged signatures. Star48 claims until 2033.
|
|
678
|
-
• Tax allegation: Tip-off report filed by Star48 side; JJY denies and confirms tax compliance.
|
|
679
|
-
• Portrait/copyright cases: JJY has sued beauty clinics and brands for unauthorized image use (肖像权纠纷).
|
|
680
|
-
- Use web_search or web_extract for latest verified information on legal matters.
|
|
681
|
-
|
|
682
|
-
8. DELIBERATE REASONING & AUTONOMOUS TOOL SELECTION (CRITICAL):
|
|
683
|
-
- Think carefully about the complexity, goal, and nature of the query before deciding which tool to use:
|
|
684
|
-
• Direct Specific Query ➔ Use direct catalog tools (search_works, search_discography, get_profile_and_fandom, get_awards, get_brand_endorsements, etc.).
|
|
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.
|
|
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.
|
|
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).
|
|
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.`;
|
|
820
|
+
buildSystemPrompt(scopedTools = null) {
|
|
821
|
+
const allTools = this.getAllToolsSchema ? this.getAllToolsSchema() : [];
|
|
822
|
+
const activeTools = scopedTools || allTools;
|
|
823
|
+
return buildSystemPrompt(activeTools, { isNativeToolCalling: true });
|
|
715
824
|
}
|
|
716
825
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const allTools = this.getAllToolsSchema();
|
|
721
|
-
|
|
722
|
-
const jsonBlockRegex = /```(?:json)?\s*(\{[\s\S]*?\})\s*```/g;
|
|
723
|
-
for (const match of content.matchAll(jsonBlockRegex)) {
|
|
724
|
-
try {
|
|
725
|
-
const parsed = JSON.parse(match[1]);
|
|
726
|
-
const toolName = parsed.action || parsed.tool || parsed.name || parsed.function;
|
|
727
|
-
const toolArgs = parsed.action_input || parsed.parameters || parsed.arguments || parsed.args || {};
|
|
728
|
-
if (toolName && allTools.some(t => t.function.name === toolName)) {
|
|
729
|
-
calls.push({
|
|
730
|
-
id: `call_txt_${Date.now()}_${calls.length}`,
|
|
731
|
-
function: {
|
|
732
|
-
name: toolName,
|
|
733
|
-
arguments: typeof toolArgs === 'string' ? toolArgs : JSON.stringify(toolArgs)
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
} catch {
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const actionRegex = /Action:\s*([a-zA-Z0-9_]+)\s*\nAction Input:\s*(\{[\s\S]*?\}|[^\n]+)/g;
|
|
742
|
-
for (const match of content.matchAll(actionRegex)) {
|
|
743
|
-
const toolName = match[1].trim();
|
|
744
|
-
const rawInput = match[2].trim();
|
|
745
|
-
if (allTools.some(t => t.function.name === toolName)) {
|
|
746
|
-
let args = {};
|
|
747
|
-
try {
|
|
748
|
-
args = JSON.parse(rawInput);
|
|
749
|
-
} catch {
|
|
750
|
-
args = { query: rawInput };
|
|
751
|
-
}
|
|
752
|
-
calls.push({
|
|
753
|
-
id: `call_act_${Date.now()}_${calls.length}`,
|
|
754
|
-
function: {
|
|
755
|
-
name: toolName,
|
|
756
|
-
arguments: JSON.stringify(args)
|
|
757
|
-
}
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
}
|
|
826
|
+
_extractTextToolCalls(content) {
|
|
827
|
+
return parseTextToolCallsFromContent(content, this._activeToolSchema || this.getAllToolsSchema());
|
|
828
|
+
}
|
|
761
829
|
|
|
762
|
-
|
|
830
|
+
parseTextToolCalls(content) {
|
|
831
|
+
return this._extractTextToolCalls(content).calls;
|
|
763
832
|
}
|
|
764
833
|
|
|
765
834
|
async ask(userPrompt, callbacks = {}) {
|
|
@@ -792,12 +861,24 @@ ${toolsSummary}
|
|
|
792
861
|
onTokenDelta = () => { },
|
|
793
862
|
onFinalAnswer = () => { },
|
|
794
863
|
onError = () => { },
|
|
864
|
+
onNotice = () => { },
|
|
865
|
+
systemDirectives = null,
|
|
795
866
|
signal = null
|
|
796
867
|
} = callbacks;
|
|
797
868
|
|
|
798
869
|
const queryStartTime = Date.now();
|
|
799
870
|
const queryId = `q_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
800
|
-
const dominantLang = detectDominantLanguage(promptForLLM);
|
|
871
|
+
const dominantLang = detectDominantLanguage(promptForLLM, this.session?.dominantLanguage);
|
|
872
|
+
if (this.session) {
|
|
873
|
+
this.session.dominantLanguage = dominantLang;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const tokenTracker = new TokenTracker({
|
|
877
|
+
sessionId: this.session?.id || null,
|
|
878
|
+
modelId: this.client?.model || 'unknown',
|
|
879
|
+
contextLimit: Number(process.env.KIKU_CONTEXT_LIMIT) || 0,
|
|
880
|
+
});
|
|
881
|
+
tokenTracker.start();
|
|
801
882
|
|
|
802
883
|
this.stateMachine.startQuery(queryId);
|
|
803
884
|
eventBus.emit(Events.AGENT_QUERY_START, { queryId, prompt: promptForLLM });
|
|
@@ -809,6 +890,27 @@ ${toolsSummary}
|
|
|
809
890
|
];
|
|
810
891
|
}
|
|
811
892
|
|
|
893
|
+
const isSecurityAttack = /(ผู้ใช้คนอื่น|หัวหน้าของเขา|แสดงประวัติส่วนตัวของเขา|ขอรหัส|password|secret|credential|token|api_?key|api\s*key|\.env|cookies\.txt|kiku_vip_keys|system\s+directive|system\s+prompt|prompt\s+leak|bypass|unauthorized|环境变量)/i.test(promptForLLM);
|
|
894
|
+
if (isSecurityAttack) {
|
|
895
|
+
const refusalMessage = dominantLang === 'zh'
|
|
896
|
+
? "抱歉,无法提供系统配置、私有数据或敏感指令。"
|
|
897
|
+
: dominantLang === 'en'
|
|
898
|
+
? "I cannot disclose system configurations, credentials, or personal private data."
|
|
899
|
+
: "ขออภัยค่ะ ไม่สามารถเปิดเผยข้อมูลการกำหนดค่าระบบ ข้อมูลประจำตัว หรือข้อมูลส่วนบุคคลได้";
|
|
900
|
+
this.session.createUserMessage(promptForLLM);
|
|
901
|
+
this.session.createAssistantMessage({ content: refusalMessage });
|
|
902
|
+
this.conversationHistory.push({ role: 'user', content: promptForLLM });
|
|
903
|
+
this.conversationHistory.push({ role: 'assistant', content: refusalMessage });
|
|
904
|
+
const totalDurationMs = Date.now() - queryStartTime;
|
|
905
|
+
onFinalAnswer(refusalMessage, { durationMs: totalDurationMs, language: dominantLang });
|
|
906
|
+
this.stateMachine.transition(AgentStates.COMPLETE);
|
|
907
|
+
eventBus.emit(Events.AGENT_FINAL_ANSWER, { queryId, answer: refusalMessage, durationMs: totalDurationMs });
|
|
908
|
+
eventBus.emit(Events.AGENT_QUERY_END, { queryId, durationMs: totalDurationMs });
|
|
909
|
+
this.stateMachine.reset();
|
|
910
|
+
await this.saveSession().catch(() => {});
|
|
911
|
+
return refusalMessage;
|
|
912
|
+
}
|
|
913
|
+
|
|
812
914
|
let promptForLLMWithGrounding = promptForLLM;
|
|
813
915
|
if (this.lastCitations && this.lastCitations.length > 0) {
|
|
814
916
|
const citedRef = resolveCitationReference(promptForLLM, this.lastCitations);
|
|
@@ -821,17 +923,21 @@ ${toolsSummary}
|
|
|
821
923
|
const userMessage = { role: 'user', content: promptForLLMWithGrounding };
|
|
822
924
|
this.conversationHistory.push(userMessage);
|
|
823
925
|
|
|
824
|
-
const memoryContext = this.memory.
|
|
926
|
+
const memoryContext = (this.activeRunId && typeof this.memory.addMemoryContext === 'function')
|
|
927
|
+
? await this.memory.addMemoryContext({ currentQuery: promptForLLM, activeRunId: this.activeRunId })
|
|
928
|
+
: this.memory.buildMemoryContext(promptForLLM);
|
|
825
929
|
this.conversationHistory[0] = {
|
|
826
930
|
role: 'system',
|
|
827
931
|
content: this.systemPrompt + memoryContext
|
|
828
932
|
};
|
|
829
933
|
|
|
934
|
+
const turnDirectives = [];
|
|
935
|
+
|
|
830
936
|
let langInstruction;
|
|
831
937
|
if (dominantLang === 'th') {
|
|
832
|
-
langInstruction = '[SYSTEM NOTE: User language is Thai. Reply in Thai only.]\n';
|
|
938
|
+
langInstruction = '[SYSTEM NOTE: User language is Thai. Reply in Thai only. Maintain Thai even when explaining errors, refusals, or reporting data not found.]\n';
|
|
833
939
|
} else if (dominantLang === 'zh') {
|
|
834
|
-
langInstruction = '[SYSTEM NOTE: User language is Chinese. Reply in Chinese only.]\n';
|
|
940
|
+
langInstruction = '[SYSTEM NOTE: User language is Chinese. Reply in Chinese only. Maintain Chinese even when reporting data not found.]\n';
|
|
835
941
|
} else if (dominantLang === 'ja') {
|
|
836
942
|
langInstruction = '[SYSTEM NOTE: User language is Japanese. Reply in Japanese only.]\n';
|
|
837
943
|
} else if (dominantLang === 'ko') {
|
|
@@ -844,7 +950,59 @@ ${toolsSummary}
|
|
|
844
950
|
if (directMentions.length > 0) {
|
|
845
951
|
langInstruction += `[SYSTEM DIRECTIVE: User explicitly invoked @${directMentions.join(', @')}. You MUST prioritize calling the referenced tool(s) to fetch accurate data.]\n`;
|
|
846
952
|
}
|
|
847
|
-
|
|
953
|
+
turnDirectives.push({ role: 'system', content: langInstruction });
|
|
954
|
+
|
|
955
|
+
if (/(เธอ|เขา|ศิลปิน|\bshe\b|\bher\b)/i.test(promptForLLM) && !/(บอต|bot|ai|คุณคือใคร|who are you)/i.test(promptForLLM)) {
|
|
956
|
+
turnDirectives.push({
|
|
957
|
+
role: 'system',
|
|
958
|
+
content: '[SUBJECT DISAMBIGUATION: The pronoun/subject ("เธอ", "เขา", "ศิลปิน", "She", "Her") in this query refers strictly to Ju Jingyi (鞠婧祎), NOT to the AI assistant. Query the appropriate catalog tool for Ju Jingyi\'s facts/quotes/works.]'
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
for (const directive of (Array.isArray(systemDirectives) ? systemDirectives : [])) {
|
|
963
|
+
if (directive && typeof directive === 'string') {
|
|
964
|
+
turnDirectives.push({ role: 'system', content: directive });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
let autoResearchTriggered = false;
|
|
969
|
+
const autoResearchMode = String(config.agent.autoResearch || 'auto').toLowerCase();
|
|
970
|
+
if (autoResearchMode !== 'off') {
|
|
971
|
+
const complexity = scoreResearchComplexity(promptForLLM);
|
|
972
|
+
if (shouldAutoResearch(promptForLLM, { mode: autoResearchMode })) {
|
|
973
|
+
const orchestratorAvailable = this.getAllToolsSchema().some(t => t.function?.name === 'run_deep_research');
|
|
974
|
+
if (orchestratorAvailable) {
|
|
975
|
+
autoResearchTriggered = true;
|
|
976
|
+
turnDirectives.push({ role: 'system', content: buildAutoResearchDirective(complexity) });
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const matchedQueryIntents = this.router.detectIntents(promptForLLM);
|
|
982
|
+
const queryResolution = resolveQueryEntities(promptForLLM);
|
|
983
|
+
|
|
984
|
+
if (this.conversationHistory.length > 2) {
|
|
985
|
+
const priorAssistantMsgs = this.conversationHistory
|
|
986
|
+
.filter(m => m.role === 'assistant' && typeof m.content === 'string')
|
|
987
|
+
.slice(-2);
|
|
988
|
+
const priorText = priorAssistantMsgs.map(m => m.content).join(' ');
|
|
989
|
+
const priorResolution = resolveQueryEntities(priorText);
|
|
990
|
+
|
|
991
|
+
if (priorResolution.hasBrand && (queryResolution.hasSong || queryResolution.hasWork || queryResolution.hasMedia)) {
|
|
992
|
+
turnDirectives.push({
|
|
993
|
+
role: 'system',
|
|
994
|
+
content: '[CONTEXT DISAMBIGUATION: The user has shifted focus away from earlier brand/endorsement discussions. The current query targets a music/video/drama entity. Query tools appropriate for works/songs/videos, NOT brand endorsements.]'
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const mentionsArtist = /(ju\s?jing\s?yi|jujingyi|จวีจิ้งอี|จิ้งอี|จวี|鞠婧祎|小鞠|kikuuu?|miju|蜜橘)/i.test(promptForLLM);
|
|
1000
|
+
if (matchedQueryIntents.length === 0 && queryResolution.matchedEntities.length === 0 && !mentionsArtist && !autoResearchTriggered) {
|
|
1001
|
+
turnDirectives.push({
|
|
1002
|
+
role: 'system',
|
|
1003
|
+
content: "[SYSTEM NOTE: This query does not appear to be about Ju Jingyi or her catalog. Answer directly from your general knowledge in the user's language. Do NOT call catalog tools (profile, works, discography, brands, awards, gallery, quotes, timeline) for this query; use web_search or web_extract only if current external information is genuinely required.]"
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
848
1006
|
|
|
849
1007
|
const clarifyingResult = await this._handleClarifyingQuestions(
|
|
850
1008
|
promptForLLM,
|
|
@@ -853,13 +1011,13 @@ ${toolsSummary}
|
|
|
853
1011
|
callbacks
|
|
854
1012
|
);
|
|
855
1013
|
if (clarifyingResult) {
|
|
856
|
-
|
|
1014
|
+
turnDirectives.push({
|
|
857
1015
|
role: 'user',
|
|
858
1016
|
content: `[SYSTEM CLARIFICATION] ${clarifyingResult.summary}\nOriginal query: ${promptForLLM}`
|
|
859
1017
|
});
|
|
860
1018
|
}
|
|
861
1019
|
|
|
862
|
-
this.applySlidingWindow();
|
|
1020
|
+
await this.applySlidingWindow();
|
|
863
1021
|
|
|
864
1022
|
const currentAssistantSessionMsg = this.session.createAssistantMessage({
|
|
865
1023
|
model: this.client?.model || null,
|
|
@@ -872,12 +1030,25 @@ ${toolsSummary}
|
|
|
872
1030
|
|
|
873
1031
|
let currentIteration = 0;
|
|
874
1032
|
const toolResultCache = new Map();
|
|
1033
|
+
const repeatSignatureCounts = new Map();
|
|
1034
|
+
this.routingInterceptor.reset();
|
|
1035
|
+
let textProtocolTaught = false;
|
|
875
1036
|
const toolResultTracker = { notFound: 0, error: 0, success: 0, total: 0, toolNames: [] };
|
|
1037
|
+
const recordedTools = [];
|
|
876
1038
|
const activeTools = this.router.routeToolsForQuery(promptForLLM, {
|
|
877
1039
|
showAll: callbacks.showAllTools || false,
|
|
878
1040
|
includeMcp: true,
|
|
879
1041
|
});
|
|
880
1042
|
|
|
1043
|
+
if (autoResearchTriggered) {
|
|
1044
|
+
const orchestrator = this.getAllToolsSchema().find(t => t.function?.name === 'run_deep_research');
|
|
1045
|
+
if (orchestrator && !activeTools.some(t => t.function?.name === 'run_deep_research')) {
|
|
1046
|
+
activeTools.push(orchestrator);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
this._activeToolSchema = activeTools;
|
|
1050
|
+
const activeToolNames = new Set(activeTools.map(t => t.function?.name).filter(Boolean));
|
|
1051
|
+
|
|
881
1052
|
while (currentIteration < this.maxIterations) {
|
|
882
1053
|
if (signal?.aborted) {
|
|
883
1054
|
this.stateMachine.reset();
|
|
@@ -887,19 +1058,31 @@ ${toolsSummary}
|
|
|
887
1058
|
currentIteration++;
|
|
888
1059
|
onStepStart(currentIteration, this.maxIterations);
|
|
889
1060
|
|
|
1061
|
+
if (currentIteration > 1) {
|
|
1062
|
+
await this.applySlidingWindow();
|
|
1063
|
+
}
|
|
1064
|
+
this._enforceContextBudget();
|
|
1065
|
+
|
|
890
1066
|
let assistantMessage = null;
|
|
891
1067
|
let isReasoning = false;
|
|
892
1068
|
let isTokenEmitting = false;
|
|
893
1069
|
|
|
894
1070
|
try {
|
|
1071
|
+
const trackerSegment = tokenTracker.beginSegment(`iteration-${currentIteration}`);
|
|
1072
|
+
const chatMessages = [
|
|
1073
|
+
this.conversationHistory[0],
|
|
1074
|
+
...turnDirectives,
|
|
1075
|
+
...this.conversationHistory.slice(1)
|
|
1076
|
+
];
|
|
895
1077
|
assistantMessage = await this.client.chat({
|
|
896
|
-
messages:
|
|
1078
|
+
messages: chatMessages,
|
|
897
1079
|
tools: activeTools,
|
|
898
1080
|
temperature: 0.3,
|
|
899
|
-
maxTokens:
|
|
1081
|
+
maxTokens: OUTPUT_TOKEN_BUDGET,
|
|
900
1082
|
stream: true,
|
|
901
1083
|
signal,
|
|
902
1084
|
onThought: (thoughtChunk) => {
|
|
1085
|
+
tokenTracker.markToken(trackerSegment);
|
|
903
1086
|
if (!isReasoning) {
|
|
904
1087
|
isReasoning = true;
|
|
905
1088
|
this.stateMachine.transition(AgentStates.STREAMING_THOUGHTS);
|
|
@@ -911,6 +1094,7 @@ ${toolsSummary}
|
|
|
911
1094
|
eventBus.emit(Events.AGENT_REASONING_DELTA, { queryId, chunk: thoughtChunk });
|
|
912
1095
|
},
|
|
913
1096
|
onToken: (tokenChunk) => {
|
|
1097
|
+
tokenTracker.markToken(trackerSegment);
|
|
914
1098
|
if (isReasoning) {
|
|
915
1099
|
isReasoning = false;
|
|
916
1100
|
this.session.finalizeReasoningPart(currentAssistantSessionMsg);
|
|
@@ -939,6 +1123,11 @@ ${toolsSummary}
|
|
|
939
1123
|
}
|
|
940
1124
|
onReasoningEnd();
|
|
941
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
tokenTracker.endSegment({
|
|
1128
|
+
promptTokens: assistantMessage.usage?.prompt_tokens,
|
|
1129
|
+
completionTokens: assistantMessage.usage?.completion_tokens,
|
|
1130
|
+
}, trackerSegment);
|
|
942
1131
|
} catch (err) {
|
|
943
1132
|
this.stateMachine.reset();
|
|
944
1133
|
if (!isAbortError(err)) {
|
|
@@ -960,22 +1149,83 @@ ${toolsSummary}
|
|
|
960
1149
|
}
|
|
961
1150
|
|
|
962
1151
|
let toolCalls = assistantMessage.tool_calls || [];
|
|
1152
|
+
let malformedTextCalls = 0;
|
|
963
1153
|
if (toolCalls.length === 0 && assistantMessage.content) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
let cleaned = assistantMessage.content;
|
|
1154
|
+
const parsedTextCalls = this._extractTextToolCalls(assistantMessage.content);
|
|
1155
|
+
toolCalls = parsedTextCalls.calls;
|
|
1156
|
+
malformedTextCalls = parsedTextCalls.malformed;
|
|
968
1157
|
|
|
969
|
-
|
|
1158
|
+
if (toolCalls.length > 0) {
|
|
1159
|
+
assistantMessage.content = stripToolCallBlocks(assistantMessage.content);
|
|
970
1160
|
|
|
971
|
-
|
|
972
|
-
|
|
1161
|
+
assistantMessage.tool_calls = toolCalls.map(tc => ({
|
|
1162
|
+
id: tc.id,
|
|
1163
|
+
type: 'function',
|
|
1164
|
+
function: tc.function
|
|
1165
|
+
}));
|
|
973
1166
|
}
|
|
974
1167
|
}
|
|
975
1168
|
|
|
976
|
-
|
|
1169
|
+
const historyMessage = {
|
|
1170
|
+
role: 'assistant',
|
|
1171
|
+
content: assistantMessage.content || '',
|
|
1172
|
+
...(assistantMessage.tool_calls ? { tool_calls: assistantMessage.tool_calls } : {})
|
|
1173
|
+
};
|
|
1174
|
+
this.conversationHistory.push(historyMessage);
|
|
977
1175
|
|
|
978
1176
|
if (!toolCalls || toolCalls.length === 0) {
|
|
1177
|
+
|
|
1178
|
+
if (currentIteration === 1 && recordedTools.length === 0) {
|
|
1179
|
+
const content = String(assistantMessage.content || '');
|
|
1180
|
+
const refusalRegex = /(ไม่มี(การ)?บันทึก|ไม่มี(อยู่ใน)?ระบบ|ไม่มีข้อมูล|ไม่มีรายละเอียด|ไม่พบ|ข้อมูลทางธุรกิจ|ข้อมูลธุรกิจ|ข้อมูลเชิงพาณิชย์|ความลับทางการค้า|ความลับ|ความลับเชิงพาณิชย์|ปรัชญาการทำงาน|ตัวตนของฉัน|นอกเหนือขอบเขต|ไม่สามารถระบุ|ไม่ได้ระบุ|not found|no verified record|no record|could not find|unable to find|outside the scope|confidential|internal business|trade secret|business secret|commercial secret|proprietary|internal statistics?|operational philosophy|my own mindset|without querying|没有找到|未找到|暂无|没有记录|无法查询|商业机密|内部数据|机密)/i;
|
|
1181
|
+
const isRefusal = refusalRegex.test(content);
|
|
1182
|
+
const isSec = /(ผู้ใช้คนอื่น|หัวหน้าของเขา|รหัสผ่าน|ความลับของระบบ|password|secret|credential|token|\.env|cookies\.txt|kiku_vip_keys|system\s+directive|mcp\s+directive|bypass|unauthorized)/i.test(promptForLLM);
|
|
1183
|
+
if (isRefusal && !isSec) {
|
|
1184
|
+
this.conversationHistory.pop();
|
|
1185
|
+
if (currentAssistantSessionMsg) currentAssistantSessionMsg.parts = [];
|
|
1186
|
+
this.conversationHistory.push({
|
|
1187
|
+
role: 'user',
|
|
1188
|
+
content: '[MANDATORY RETRIEVAL RULE & VERIFICATION CONSTRAINT] You are strictly FORBIDDEN from asserting that catalog information does not exist, is confidential, or is missing without querying a tool first in this turn. Call the appropriate catalog tool (e.g. get_brand_endorsements, search_works, search_discography, get_awards, get_quotes, get_music_videos, get_gallery_photos) NOW to fetch and verify the facts before answering.'
|
|
1189
|
+
});
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const explicitCatalogRegex = /(ตาม|ใน|จาก)?(แค็ตตาล็อก|catalog|ฐานข้อมูล|database|ข้อมูลทางการ|บันทึกทางการ)/i;
|
|
1194
|
+
const isExplicitCatalogMention = explicitCatalogRegex.test(promptForLLM);
|
|
1195
|
+
const hasCatalogTarget = (queryResolution?.recommendedTools?.length > 0 || queryResolution?.matchedEntities?.length > 0 || isExplicitCatalogMention) && (mentionsArtist || isExplicitCatalogMention);
|
|
1196
|
+
|
|
1197
|
+
if (!isSec && hasCatalogTarget) {
|
|
1198
|
+
const suggestedTools = queryResolution?.recommendedTools?.length > 0
|
|
1199
|
+
? queryResolution.recommendedTools.join(', ')
|
|
1200
|
+
: 'search_works, search_discography, get_brand_endorsements, get_career_timeline, get_profile_and_fandom';
|
|
1201
|
+
this.conversationHistory.pop();
|
|
1202
|
+
if (currentAssistantSessionMsg) currentAssistantSessionMsg.parts = [];
|
|
1203
|
+
this.conversationHistory.push({
|
|
1204
|
+
role: 'user',
|
|
1205
|
+
content: `[MANDATORY CATALOG-FIRST RETRIEVAL] This request asks for verified facts from the official catalog. You must NOT provide an unverified direct answer without calling a catalog tool first. Call the appropriate tool (recommended: ${suggestedTools}) NOW to retrieve and verify the exact facts before formulating your response.`
|
|
1206
|
+
});
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const needProtocol = Boolean(this.client?.toolsDisabledReason) && !textProtocolTaught;
|
|
1212
|
+
if (currentIteration < this.maxIterations && (needProtocol || malformedTextCalls > 0)) {
|
|
1213
|
+
if (needProtocol) {
|
|
1214
|
+
textProtocolTaught = true;
|
|
1215
|
+
this.conversationHistory.push({
|
|
1216
|
+
role: 'system',
|
|
1217
|
+
content: buildTextToolProtocolDirective(this.getAllToolsSchema())
|
|
1218
|
+
});
|
|
1219
|
+
try {
|
|
1220
|
+
onNotice('Native tool calling is unavailable for this model/provider — switched to the text tool protocol.');
|
|
1221
|
+
} catch { }
|
|
1222
|
+
}
|
|
1223
|
+
if (malformedTextCalls > 0) {
|
|
1224
|
+
this.conversationHistory.push({ role: 'user', content: MALFORMED_TOOL_CALL_FEEDBACK });
|
|
1225
|
+
}
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
979
1229
|
const totalDurationMs = Date.now() - queryStartTime;
|
|
980
1230
|
this.session.stats.totalDurationMs += totalDurationMs;
|
|
981
1231
|
|
|
@@ -986,6 +1236,10 @@ ${toolsSummary}
|
|
|
986
1236
|
dominantLang === 'en' ? "Sorry, unable to process a response." :
|
|
987
1237
|
"ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้"
|
|
988
1238
|
));
|
|
1239
|
+
historyMessage.content = finalAnswer;
|
|
1240
|
+
this.conversationHistory = this.conversationHistory.filter(m =>
|
|
1241
|
+
!(typeof m?.content === 'string' && m.content.includes('[MANDATORY'))
|
|
1242
|
+
);
|
|
989
1243
|
onFinalAnswer(finalAnswer, { durationMs: totalDurationMs, language: dominantLang });
|
|
990
1244
|
this._extractFactsFromFinalAnswer(finalAnswer);
|
|
991
1245
|
|
|
@@ -993,29 +1247,68 @@ ${toolsSummary}
|
|
|
993
1247
|
eventBus.emit(Events.AGENT_FINAL_ANSWER, { queryId, answer: finalAnswer, durationMs: totalDurationMs });
|
|
994
1248
|
this.stateMachine.reset();
|
|
995
1249
|
|
|
1250
|
+
this._recordQueryMetrics(tokenTracker, {
|
|
1251
|
+
promptTokens: queryPromptTokens,
|
|
1252
|
+
completionTokens: queryCompletionTokens,
|
|
1253
|
+
});
|
|
1254
|
+
|
|
996
1255
|
await this.saveSession().catch(err => console.error('Session save failed:', err));
|
|
997
1256
|
return finalAnswer;
|
|
998
1257
|
}
|
|
999
1258
|
|
|
1259
|
+
let shouldAbortLoop = false;
|
|
1000
1260
|
const executionPromises = toolCalls.map(async (tc, toolCallIndex) => {
|
|
1001
1261
|
if (signal?.aborted) throw createAbortError();
|
|
1002
1262
|
|
|
1003
|
-
|
|
1004
|
-
const
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
name:
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1263
|
+
|
|
1264
|
+
const intercepted = this.routingInterceptor.interceptToolCall(tc, currentIteration - 1);
|
|
1265
|
+
if (intercepted.aborted) {
|
|
1266
|
+
shouldAbortLoop = true;
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
if (intercepted.rerouted && intercepted.replacement) {
|
|
1270
|
+
tc = {
|
|
1271
|
+
...tc,
|
|
1272
|
+
function: {
|
|
1273
|
+
name: intercepted.replacement.name,
|
|
1274
|
+
arguments: JSON.stringify(intercepted.replacement.params)
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const toolName = tc?.function?.name || '';
|
|
1280
|
+
const toolCallId = tc?.id || `call_${queryId}_${currentIteration}_${toolCallIndex}`;
|
|
1281
|
+
if (this.allowedToolNames && !this.allowedToolNames.has(toolName)) {
|
|
1282
|
+
const rejectedResult = { status: 'error', message: `Tool "${toolName}" is not permitted in this execution context.` };
|
|
1283
|
+
onToolStart(toolName, {});
|
|
1284
|
+
onToolResult(toolName, rejectedResult, 0);
|
|
1285
|
+
return {
|
|
1286
|
+
toolMessage: {
|
|
1287
|
+
role: 'tool',
|
|
1288
|
+
tool_call_id: toolCallId,
|
|
1289
|
+
name: toolName,
|
|
1290
|
+
content: JSON.stringify(rejectedResult),
|
|
1291
|
+
},
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
if (!activeToolNames.has(toolName)) {
|
|
1295
|
+
const available = [...activeToolNames].slice(0, 20).join(', ');
|
|
1296
|
+
const rejectedResult = {
|
|
1297
|
+
status: 'error',
|
|
1298
|
+
message: `Tool "${toolName}" was not offered for this query, so calling it is not allowed. Available tools for this query: ${available}. Re-answer using the available tools or the evidence you already have.`
|
|
1299
|
+
};
|
|
1300
|
+
onToolStart(toolName, {});
|
|
1301
|
+
onToolResult(toolName, rejectedResult, 0);
|
|
1302
|
+
return {
|
|
1303
|
+
toolMessage: {
|
|
1304
|
+
role: 'tool',
|
|
1305
|
+
tool_call_id: toolCallId,
|
|
1306
|
+
name: toolName,
|
|
1307
|
+
content: JSON.stringify(rejectedResult),
|
|
1308
|
+
},
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
const isBuiltin = isBuiltinTool(toolName);
|
|
1019
1312
|
const isMcp = !isBuiltin && (toolName.startsWith('mcp_') || Boolean(this.mcpManager?.isMcpTool(toolName)));
|
|
1020
1313
|
|
|
1021
1314
|
let prepared;
|
|
@@ -1067,6 +1360,8 @@ ${toolsSummary}
|
|
|
1067
1360
|
const toolStartTime = Date.now();
|
|
1068
1361
|
const signature = stableToolSignature(toolName, finalArgs);
|
|
1069
1362
|
const cachedResult = toolResultCache.get(signature);
|
|
1363
|
+
const repeatCount = (repeatSignatureCounts.get(signature) || 0) + 1;
|
|
1364
|
+
repeatSignatureCounts.set(signature, repeatCount);
|
|
1070
1365
|
|
|
1071
1366
|
let result;
|
|
1072
1367
|
if (cachedResult) {
|
|
@@ -1096,6 +1391,15 @@ ${toolsSummary}
|
|
|
1096
1391
|
|
|
1097
1392
|
const cappedResult = this._capToolResultForHistory(result);
|
|
1098
1393
|
|
|
1394
|
+
let instruction = result.status === 'not_found'
|
|
1395
|
+
? 'CRITICAL: No matching data was found for this query in the catalog. State explicitly in the user\'s language that no verified records exist in the database. Do NOT invent, fabricate, or guess any records. Do NOT claim you searched external platforms like Bilibili, Weibo, or Google unless those specific tools were called. Under no circumstances should you generate plausible-sounding but unverified details.'
|
|
1396
|
+
: result.status === 'error'
|
|
1397
|
+
? '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.'
|
|
1398
|
+
: '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.';
|
|
1399
|
+
if (cachedResult) {
|
|
1400
|
+
instruction += ' NOTE: You already called this exact tool with identical arguments earlier in this query — the duplicated result above adds nothing new. Do NOT repeat this call; answer with the retrieved data now.';
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1099
1403
|
return {
|
|
1100
1404
|
toolMessage: {
|
|
1101
1405
|
role: 'tool',
|
|
@@ -1104,36 +1408,60 @@ ${toolsSummary}
|
|
|
1104
1408
|
content: JSON.stringify({
|
|
1105
1409
|
authoritative: true,
|
|
1106
1410
|
result: cappedResult,
|
|
1107
|
-
instruction
|
|
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.'
|
|
1109
|
-
: result.status === 'error'
|
|
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.'
|
|
1411
|
+
instruction
|
|
1112
1412
|
})
|
|
1113
|
-
}
|
|
1413
|
+
},
|
|
1414
|
+
toolName,
|
|
1415
|
+
result
|
|
1114
1416
|
};
|
|
1115
1417
|
});
|
|
1116
1418
|
|
|
1117
1419
|
const toolResults = await Promise.all(executionPromises);
|
|
1420
|
+
|
|
1118
1421
|
for (const item of toolResults) {
|
|
1119
1422
|
if (item?.toolMessage) {
|
|
1120
1423
|
this.conversationHistory.push(item.toolMessage);
|
|
1121
1424
|
}
|
|
1425
|
+
if (item?.toolName) {
|
|
1426
|
+
recordedTools.push({ name: item.toolName, result: item.result });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
if (shouldAbortLoop) {
|
|
1432
|
+
try {
|
|
1433
|
+
onNotice('Duplicate tool call signature detected by RoutingInterceptor — moving directly to final answer synthesis.');
|
|
1434
|
+
} catch { }
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (currentIteration < this.maxIterations) {
|
|
1439
|
+
let stuck = false;
|
|
1440
|
+
for (const count of repeatSignatureCounts.values()) {
|
|
1441
|
+
if (count >= REPEAT_LOOP_THRESHOLD) { stuck = true; break; }
|
|
1442
|
+
}
|
|
1443
|
+
if (stuck) {
|
|
1444
|
+
try {
|
|
1445
|
+
onNotice('Repeated identical tool calls detected — moving to the final answer.');
|
|
1446
|
+
} catch { }
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1122
1449
|
}
|
|
1123
1450
|
}
|
|
1124
1451
|
|
|
1125
|
-
const selfCritiqueDirective = await this._performSelfCritique(
|
|
1452
|
+
const selfCritiqueDirective = await this._performSelfCritique(
|
|
1126
1453
|
toolResultTracker,
|
|
1127
1454
|
dominantLang,
|
|
1128
1455
|
promptForLLM
|
|
1129
1456
|
);
|
|
1130
1457
|
|
|
1131
1458
|
const langPrompt = dominantLang === 'zh'
|
|
1132
|
-
? "
|
|
1459
|
+
? "请根据上述工具查询结果回答,回答深度须与问题相称:单一事实类问题(日期、名字、数字、是/否)用 1-4 句话直接简洁作答,不加标题或表格;仅当问题是比较、排序、列表或深度分析时,才使用完整的 Markdown 结构(标题、要点、表格)并附上具体的 YouTube 真实链接(如 Direct Watch: https://www.youtube.com/watch?v=... 或 Search: https://www.youtube.com/results?search_query=...)。不要用无关段落填充简短的事实性回答,但也绝不能遗漏用户明确要求的任何条目。\n\n【反幻觉守则 — 必须遵守】只陈述工具结果中明确包含的事实。禁止编造、推测或补充工具结果中不存在的剧名、歌曲名、角色名、品牌名、日期或任何细节。如果工具结果不足以完整回答,请明确说明哪些部分未经验证,而不是凭空补全。"
|
|
1133
1460
|
: dominantLang === 'en'
|
|
1134
|
-
? "Based on the tool results above, provide a
|
|
1135
|
-
: "สรุปคำตอบจากผลลัพธ์เครื่องมือข้างต้น
|
|
1461
|
+
? "Based on the tool results above, provide a response whose depth matches the question: for simple single-fact questions (a date, a name, a number, yes/no) answer concisely and directly in 1-4 sentences without headers or tables; only for comparison, ranking, list, or deep-dive questions use rich detail with well-organized Markdown headers, bullet points, and verified YouTube links (https://www.youtube.com/results?search_query=...). Never pad short factual answers with filler sections, and never omit any item the user explicitly requested.\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."
|
|
1462
|
+
: "สรุปคำตอบจากผลลัพธ์เครื่องมือข้างต้น โดยให้ความลึกของคำตอบสมดุลกับคำถาม: คำถามข้อเท็จจริงเดี่ยว (วันที่ ชื่อ ตัวเลข ใช่/ไม่ใช่) ตอบสั้น ตรงประเด็น 1-4 ประโยค โดยไม่ต้องมีหัวข้อหรือตาราง ส่วนคำถามเปรียบเทียบ จัดอันดับ จัดรายการ หรือวิเคราะห์เชิงลึก จึงจัดโครงสร้างด้วยหัวข้อ Markdown, ตาราง และ bullet points พร้อมแสดงลิงก์ YouTube จริง (Direct Watch Link และ https://www.youtube.com/results?search_query=...) อย่างครบถ้วน ห้ามเติมเนื้อหาฟุ่มเฟือยในคำตอบสั้น แต่ห้ามลดทอนรายการที่ผู้ใช้ระบุขอมาทุกข้อ\n\n【กฎป้องกันการแต่งข้อมูล — บังคับใช้】ระบุเฉพาะข้อเท็จจริงที่มีอยู่จริงในผลลัพธ์เครื่องมือเท่านั้น ห้ามสร้างชื่อละคร เพลง ตัวละคร แบรนด์ วันที่ หรือรายละเอียดใดๆ ที่ไม่มีอยู่ในผลลัพธ์เครื่องมือ หากข้อมูลไม่เพียงพอสำหรับคำตอบที่สมบูรณ์ ให้ระบุชัดเจนว่าส่วนใดยังไม่ได้รับการยืนยัน แทนการเติมข้อมูลที่ฟังดูน่าเชื่อแต่ไม่มีหลักฐาน";
|
|
1136
1463
|
|
|
1464
|
+
const synthHistoryBase = this.conversationHistory.length;
|
|
1137
1465
|
this.conversationHistory.push({ role: 'user', content: langPrompt });
|
|
1138
1466
|
this.conversationHistory.push({ role: 'system', content: selfCritiqueDirective });
|
|
1139
1467
|
|
|
@@ -1143,13 +1471,19 @@ ${toolsSummary}
|
|
|
1143
1471
|
|
|
1144
1472
|
let isReasoning = false;
|
|
1145
1473
|
let synthTokenStarted = false;
|
|
1474
|
+
const synthSegment = tokenTracker.beginSegment('synthesis');
|
|
1146
1475
|
const finalRes = await this.client.chat({
|
|
1147
|
-
messages:
|
|
1476
|
+
messages: [
|
|
1477
|
+
this.conversationHistory[0],
|
|
1478
|
+
...turnDirectives,
|
|
1479
|
+
...this.conversationHistory.slice(1)
|
|
1480
|
+
],
|
|
1148
1481
|
temperature: 0.3,
|
|
1149
|
-
maxTokens:
|
|
1482
|
+
maxTokens: OUTPUT_TOKEN_BUDGET,
|
|
1150
1483
|
stream: true,
|
|
1151
1484
|
signal,
|
|
1152
1485
|
onThought: (thoughtChunk) => {
|
|
1486
|
+
tokenTracker.markToken(synthSegment);
|
|
1153
1487
|
if (!isReasoning) {
|
|
1154
1488
|
isReasoning = true;
|
|
1155
1489
|
onReasoningStart();
|
|
@@ -1158,6 +1492,7 @@ ${toolsSummary}
|
|
|
1158
1492
|
onReasoningDelta(thoughtChunk);
|
|
1159
1493
|
},
|
|
1160
1494
|
onToken: (tokenChunk) => {
|
|
1495
|
+
tokenTracker.markToken(synthSegment);
|
|
1161
1496
|
if (isReasoning) {
|
|
1162
1497
|
isReasoning = false;
|
|
1163
1498
|
this.session.finalizeReasoningPart(currentAssistantSessionMsg);
|
|
@@ -1172,6 +1507,14 @@ ${toolsSummary}
|
|
|
1172
1507
|
}
|
|
1173
1508
|
});
|
|
1174
1509
|
|
|
1510
|
+
|
|
1511
|
+
this.conversationHistory.length = Math.min(this.conversationHistory.length, synthHistoryBase);
|
|
1512
|
+
|
|
1513
|
+
tokenTracker.endSegment({
|
|
1514
|
+
promptTokens: finalRes?.usage?.prompt_tokens,
|
|
1515
|
+
completionTokens: finalRes?.usage?.completion_tokens,
|
|
1516
|
+
}, synthSegment);
|
|
1517
|
+
|
|
1175
1518
|
if (finalRes?.usage) {
|
|
1176
1519
|
queryPromptTokens += Number(finalRes.usage.prompt_tokens || 0);
|
|
1177
1520
|
queryCompletionTokens += Number(finalRes.usage.completion_tokens || 0);
|
|
@@ -1195,6 +1538,11 @@ ${toolsSummary}
|
|
|
1195
1538
|
this.session.recordQueryTokens(queryTokens, queryCostUsd);
|
|
1196
1539
|
this.lastTelemetry = { tokens: queryTokens, costUsd: queryCostUsd };
|
|
1197
1540
|
|
|
1541
|
+
this._recordQueryMetrics(tokenTracker, {
|
|
1542
|
+
promptTokens: queryPromptTokens,
|
|
1543
|
+
completionTokens: queryCompletionTokens,
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1198
1546
|
const totalDurationMs = Date.now() - queryStartTime;
|
|
1199
1547
|
this.session.stats.totalDurationMs += totalDurationMs;
|
|
1200
1548
|
|
|
@@ -1204,6 +1552,10 @@ ${toolsSummary}
|
|
|
1204
1552
|
dominantLang === 'ko' ? "검색이 완료되었습니다" :
|
|
1205
1553
|
dominantLang === 'en' ? "Search complete." : "เสร็จสิ้นการค้นหาข้อมูล";
|
|
1206
1554
|
const output = sanitizeResponse(finalRes?.content || defaultDone);
|
|
1555
|
+
this.conversationHistory.push({ role: 'assistant', content: output });
|
|
1556
|
+
this.conversationHistory = this.conversationHistory.filter(m =>
|
|
1557
|
+
!(typeof m?.content === 'string' && m.content.includes('[MANDATORY'))
|
|
1558
|
+
);
|
|
1207
1559
|
const citations = extractCitations(output, recordedTools);
|
|
1208
1560
|
this.lastCitations = citations;
|
|
1209
1561
|
if (currentAssistantSessionMsg) {
|
|
@@ -1245,6 +1597,22 @@ ${toolsSummary}
|
|
|
1245
1597
|
return this.ask(userPrompt, callbacks);
|
|
1246
1598
|
}
|
|
1247
1599
|
|
|
1600
|
+
/** Fold the query's TokenTracker timings into lastQueryMetrics / history. */
|
|
1601
|
+
_recordQueryMetrics(tokenTracker, { promptTokens, completionTokens } = {}) {
|
|
1602
|
+
try {
|
|
1603
|
+
if (!tokenTracker) return null;
|
|
1604
|
+
const payload = tokenTracker.finish({ promptTokens, completionTokens });
|
|
1605
|
+
if (payload) {
|
|
1606
|
+
this.lastQueryMetrics = payload;
|
|
1607
|
+
this.queryMetricsHistory.push(payload);
|
|
1608
|
+
if (this.queryMetricsHistory.length > 50) this.queryMetricsHistory.shift();
|
|
1609
|
+
}
|
|
1610
|
+
return payload;
|
|
1611
|
+
} catch {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1248
1616
|
_computeConfidenceScore(toolResultTracker) {
|
|
1249
1617
|
if (toolResultTracker.total === 0) return 0;
|
|
1250
1618
|
|
|
@@ -1312,16 +1680,17 @@ ${toolsSummary}
|
|
|
1312
1680
|
_capToolResultForHistory(result) {
|
|
1313
1681
|
if (!result || typeof result !== 'object') return result;
|
|
1314
1682
|
|
|
1315
|
-
const
|
|
1316
|
-
const serialized = JSON.stringify(result);
|
|
1317
|
-
if (serialized.length <= MAX_RESULT_BYTES) return result;
|
|
1683
|
+
const compacted = compactToolResultForContext(result, { maxTokens: 800 });
|
|
1318
1684
|
|
|
1685
|
+
const MAX_RESULT_BYTES = 16384;
|
|
1686
|
+
const serialized = JSON.stringify(compacted);
|
|
1687
|
+
if (serialized.length <= MAX_RESULT_BYTES) return compacted;
|
|
1319
1688
|
|
|
1320
1689
|
const truncate = (obj) => {
|
|
1321
1690
|
if (Array.isArray(obj)) {
|
|
1322
|
-
if (obj.length >
|
|
1323
|
-
const kept = obj.slice(0,
|
|
1324
|
-
kept.push({ _truncated: true, totalItems: obj.length, showing:
|
|
1691
|
+
if (obj.length > 5) {
|
|
1692
|
+
const kept = obj.slice(0, 5).map(truncate);
|
|
1693
|
+
kept.push({ _truncated: true, totalItems: obj.length, showing: 5 });
|
|
1325
1694
|
return kept;
|
|
1326
1695
|
}
|
|
1327
1696
|
return obj.map(truncate);
|
|
@@ -1333,14 +1702,13 @@ ${toolsSummary}
|
|
|
1333
1702
|
}
|
|
1334
1703
|
return out;
|
|
1335
1704
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
return `${obj.slice(0, 500)}…[truncated]`;
|
|
1705
|
+
if (typeof obj === 'string' && obj.length > 300) {
|
|
1706
|
+
return `${obj.slice(0, 300)}…[truncated]`;
|
|
1339
1707
|
}
|
|
1340
1708
|
return obj;
|
|
1341
1709
|
};
|
|
1342
1710
|
|
|
1343
|
-
const capped = truncate(
|
|
1711
|
+
const capped = truncate(compacted);
|
|
1344
1712
|
capped._cappedForHistory = true;
|
|
1345
1713
|
return capped;
|
|
1346
1714
|
}
|