kiku-agent 1.2.0 → 1.3.1
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 +32 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +637 -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 +532 -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/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -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/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- 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/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- 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/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +390 -190
- 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/routes/agent.js +55 -0
- 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
package/server/agent/research.js
CHANGED
|
@@ -2,19 +2,219 @@ import { ResearchSubagent } from './subagent.js';
|
|
|
2
2
|
import { executeTool } from './tools.js';
|
|
3
3
|
import { eventBus, Events } from './eventBus.js';
|
|
4
4
|
import { mapConcurrent } from './suggestions.js';
|
|
5
|
+
import { checkUrlLiveness } from './webSearch.js';
|
|
5
6
|
|
|
6
|
-
const CATALOG_TOOLS = Object.freeze([
|
|
7
|
+
export const CATALOG_TOOLS = Object.freeze([
|
|
7
8
|
'global_search', 'get_profile_and_fandom', 'search_works', 'get_work_detail',
|
|
8
9
|
'get_character_roles', 'search_discography', 'get_music_videos',
|
|
9
10
|
'get_media_links', 'get_brand_endorsements', 'get_career_timeline',
|
|
10
11
|
'get_awards', 'get_social_feed',
|
|
11
12
|
]);
|
|
12
|
-
const WEB_TOOLS = Object.freeze([
|
|
13
|
-
|
|
13
|
+
export const WEB_TOOLS = Object.freeze([
|
|
14
|
+
'web_search', 'web_extract', 'fetch_webpage',
|
|
15
|
+
'search_chinese_web', 'search_web_images', 'search_news', 'search_encyclopedia'
|
|
16
|
+
]);
|
|
17
|
+
export const VERIFY_TOOLS = Object.freeze([...CATALOG_TOOLS, ...WEB_TOOLS]);
|
|
18
|
+
|
|
19
|
+
export function cleanUrl(rawUrl = '') {
|
|
20
|
+
let url = String(rawUrl || '').trim();
|
|
21
|
+
const hasTrailingEllipsis = /\.{3,}$|…$/.test(url);
|
|
22
|
+
let prev;
|
|
23
|
+
do {
|
|
24
|
+
prev = url;
|
|
25
|
+
url = url.replace(/^[`"'\(\[<|\s]+/, '');
|
|
26
|
+
url = url.replace(/[`"'\)\]>|\s]+$/, '');
|
|
27
|
+
url = url.replace(/(?:(?<!\.)\.(?!\.)|[;:!?,])+$/, '');
|
|
28
|
+
} while (url !== prev && url.length > 0);
|
|
29
|
+
if (hasTrailingEllipsis && !url.includes('...') && !url.includes('…')) {
|
|
30
|
+
url = `${url.replace(/\/+$/, '')}/...`;
|
|
31
|
+
}
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function extractAllRealUrls(toolResults = []) {
|
|
36
|
+
const urls = new Set();
|
|
37
|
+
const add = (candidate) => {
|
|
38
|
+
if (!candidate || typeof candidate !== 'string') return;
|
|
39
|
+
const cleaned = cleanUrl(candidate);
|
|
40
|
+
if (/^https?:\/\/[a-zA-Z0-9]/i.test(cleaned) && !cleaned.includes('...') && !cleaned.includes('…')) {
|
|
41
|
+
urls.add(cleaned);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const traverse = (obj, depth = 0) => {
|
|
46
|
+
if (!obj || depth > 5) return;
|
|
47
|
+
if (typeof obj === 'string') {
|
|
48
|
+
const found = obj.match(/https?:\/\/[^\s<>()"'|\[\]`]+/g) || [];
|
|
49
|
+
for (const f of found) add(f);
|
|
50
|
+
} else if (Array.isArray(obj)) {
|
|
51
|
+
for (const item of obj) traverse(item, depth + 1);
|
|
52
|
+
} else if (typeof obj === 'object') {
|
|
53
|
+
if (typeof obj.url === 'string') add(obj.url);
|
|
54
|
+
if (typeof obj.link === 'string') add(obj.link);
|
|
55
|
+
for (const key of Object.keys(obj)) {
|
|
56
|
+
traverse(obj[key], depth + 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const item of toolResults) {
|
|
62
|
+
traverse(item);
|
|
63
|
+
}
|
|
64
|
+
return [...urls];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveTruncatedUrl(truncatedUrl, knownUrls = []) {
|
|
68
|
+
const cleaned = cleanUrl(truncatedUrl);
|
|
69
|
+
const normalized = cleaned.replace(/…/g, '...');
|
|
70
|
+
if (!normalized.includes('...')) return normalized;
|
|
71
|
+
|
|
72
|
+
if (!knownUrls || knownUrls.length === 0) {
|
|
73
|
+
return normalized
|
|
74
|
+
.replace(/\/+\.\.\.[^\/]*$/, '')
|
|
75
|
+
.replace(/\/\.\.\.\//g, '/')
|
|
76
|
+
.replace(/\.{3,}/g, '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
let domain = '';
|
|
81
|
+
try {
|
|
82
|
+
const parsed = new URL(normalized.replace(/\.\.\./g, 'placeholder'));
|
|
83
|
+
domain = parsed.hostname.replace(/^www\./, '').toLowerCase();
|
|
84
|
+
} catch {
|
|
85
|
+
const match = normalized.match(/https?:\/\/(?:www\.)?([^\/\s:]+)/i);
|
|
86
|
+
if (match) domain = match[1].toLowerCase();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const candidates = knownUrls.filter(known => {
|
|
90
|
+
if (!domain) return true;
|
|
91
|
+
try {
|
|
92
|
+
const kHost = new URL(known).hostname.replace(/^www\./, '').toLowerCase();
|
|
93
|
+
return kHost === domain || kHost.endsWith('.' + domain) || domain.endsWith('.' + kHost);
|
|
94
|
+
} catch {
|
|
95
|
+
return known.toLowerCase().includes(domain);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const pool = candidates.length > 0 ? candidates : knownUrls;
|
|
100
|
+
|
|
101
|
+
const rawTokens = decodeURIComponent(normalized)
|
|
102
|
+
.replace(/^https?:\/\/(?:www\.)?/i, '')
|
|
103
|
+
.split(/[\/\?#&]+/)
|
|
104
|
+
.flatMap(part => part.split(/\.\.\./))
|
|
105
|
+
.map(part => part.trim().replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, ''))
|
|
106
|
+
.filter(part => part.length >= 3 && !/^\.+$/.test(part));
|
|
107
|
+
|
|
108
|
+
const idTokens = rawTokens.filter(t => /^\d{4,}$/.test(t));
|
|
109
|
+
|
|
110
|
+
let bestMatch = null;
|
|
111
|
+
let highestScore = 0;
|
|
112
|
+
|
|
113
|
+
for (const known of pool) {
|
|
114
|
+
const decodedKnown = decodeURIComponent(known).toLowerCase();
|
|
115
|
+
let score = 0;
|
|
116
|
+
|
|
117
|
+
for (const id of idTokens) {
|
|
118
|
+
if (decodedKnown.includes(id.toLowerCase())) {
|
|
119
|
+
score += 50;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const token of rawTokens) {
|
|
124
|
+
const lowerToken = token.toLowerCase();
|
|
125
|
+
if (decodedKnown.includes(lowerToken)) {
|
|
126
|
+
score += 10;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (score > highestScore) {
|
|
131
|
+
highestScore = score;
|
|
132
|
+
bestMatch = known;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (bestMatch && (highestScore >= 20 || (idTokens.length > 0 && highestScore >= 50))) {
|
|
137
|
+
return bestMatch;
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
|
|
141
|
+
return normalized
|
|
142
|
+
.replace(/\/+\.\.\.[^\/]*$/, '')
|
|
143
|
+
.replace(/\/\.\.\.\//g, '/')
|
|
144
|
+
.replace(/\.{3,}/g, '');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function expandTruncatedUrlsInText(text, knownUrls = []) {
|
|
148
|
+
if (!text) return text;
|
|
149
|
+
return text.replace(/https?:\/\/[^\s<>()"'\n`|\]]+(?:\.\.\.|…)[^\s<>()"'\n`|\]]*/g, (match) => {
|
|
150
|
+
let urlCandidate = match;
|
|
151
|
+
let trailingPunct = '';
|
|
152
|
+
const punctMatch = urlCandidate.match(/([,;:!?]+)$/);
|
|
153
|
+
if (punctMatch) {
|
|
154
|
+
trailingPunct = punctMatch[1];
|
|
155
|
+
urlCandidate = urlCandidate.slice(0, -trailingPunct.length);
|
|
156
|
+
}
|
|
157
|
+
const resolved = resolveTruncatedUrl(urlCandidate, knownUrls);
|
|
158
|
+
return resolved + trailingPunct;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function uniqueUrls(text, knownUrls = []) {
|
|
163
|
+
const matches = String(text || '').match(/https?:\/\/[^\s<>()\]`"'|]+/g) || [];
|
|
164
|
+
const resolvedList = matches.map(url => resolveTruncatedUrl(cleanUrl(url), knownUrls));
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
const results = [];
|
|
167
|
+
|
|
168
|
+
for (const u of resolvedList) {
|
|
169
|
+
if (!u || !/^https?:\/\/[a-zA-Z0-9]/i.test(u)) continue;
|
|
170
|
+
if (u.includes('...') || u.includes('…')) continue;
|
|
171
|
+
const cleanU = cleanUrl(u);
|
|
172
|
+
if (!seen.has(cleanU)) {
|
|
173
|
+
seen.add(cleanU);
|
|
174
|
+
results.push(cleanU);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const known of knownUrls) {
|
|
179
|
+
try {
|
|
180
|
+
const cleanKnown = cleanUrl(known);
|
|
181
|
+
if (cleanKnown.includes('...') || cleanKnown.includes('…')) continue;
|
|
182
|
+
const kObj = new URL(cleanKnown);
|
|
183
|
+
const host = kObj.hostname.replace(/^www\./, '');
|
|
184
|
+
if (host.length > 4 && text.toLowerCase().includes(host.toLowerCase()) && !seen.has(cleanKnown)) {
|
|
185
|
+
seen.add(cleanKnown);
|
|
186
|
+
results.push(cleanKnown);
|
|
187
|
+
}
|
|
188
|
+
} catch {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function filterAliveSources(urls = [], realToolUrls = [], options = {}) {
|
|
195
|
+
if (!Array.isArray(urls) || urls.length === 0) return [];
|
|
196
|
+
if (options.checkLiveness === false || process.env.KIKU_SKIP_URL_LIVENESS === 'true') {
|
|
197
|
+
return urls.map(cleanUrl).filter(u => u && !u.includes('...') && !u.includes('…'));
|
|
198
|
+
}
|
|
199
|
+
const realSet = new Set(realToolUrls.map(u => cleanUrl(u).toLowerCase()));
|
|
200
|
+
const cleanedUrls = urls.map(cleanUrl);
|
|
201
|
+
|
|
202
|
+
const verified = [];
|
|
203
|
+
await mapConcurrent(cleanedUrls, 5, async (u) => {
|
|
204
|
+
if (!u || u.includes('...') || u.includes('…')) return;
|
|
205
|
+
|
|
206
|
+
const isDirectFromTool = realSet.has(u.toLowerCase());
|
|
14
207
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
208
|
+
const check = await checkUrlLiveness(u, { timeoutMs: 2500 });
|
|
209
|
+
if (check.alive) {
|
|
210
|
+
verified.push(u);
|
|
211
|
+
} else if (isDirectFromTool && check.reason !== 'not_found' && check.reason !== 'dns_failed') {
|
|
212
|
+
verified.push(u);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const verifiedSet = new Set(verified);
|
|
217
|
+
return cleanedUrls.filter(u => verifiedSet.has(u));
|
|
18
218
|
}
|
|
19
219
|
|
|
20
220
|
function stableSignature(name, args) {
|
|
@@ -63,6 +263,7 @@ export class ResearchOrchestrator {
|
|
|
63
263
|
this.maxIterations = Math.min(Math.max(options.maxIterations || 3, 1), 5);
|
|
64
264
|
this.active = null;
|
|
65
265
|
this.lastResult = null;
|
|
266
|
+
this._runLock = null;
|
|
66
267
|
}
|
|
67
268
|
|
|
68
269
|
getStatus() {
|
|
@@ -88,15 +289,31 @@ export class ResearchOrchestrator {
|
|
|
88
289
|
if (!query) throw new Error('A research question is required.');
|
|
89
290
|
|
|
90
291
|
|
|
292
|
+
while (this._runLock) {
|
|
293
|
+
try { await this._runLock; } catch { }
|
|
294
|
+
}
|
|
295
|
+
let releaseLock;
|
|
296
|
+
this._runLock = new Promise(resolve => { releaseLock = resolve; });
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
return await this._run(query, options);
|
|
300
|
+
} finally {
|
|
301
|
+
this._runLock = null;
|
|
302
|
+
releaseLock();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async _run(query, options = {}) {
|
|
91
307
|
if (this.active && this.active.question === query && this.active.promise) {
|
|
92
308
|
try {
|
|
93
309
|
return await this.active.promise;
|
|
94
310
|
} catch (err) {
|
|
95
|
-
|
|
311
|
+
|
|
312
|
+
console.warn('[Kiku research] previous identical run failed, starting a new one:', err?.message || err);
|
|
96
313
|
}
|
|
97
314
|
}
|
|
98
315
|
|
|
99
|
-
|
|
316
|
+
|
|
100
317
|
if (this.active) {
|
|
101
318
|
try {
|
|
102
319
|
this.active.controller.abort();
|
|
@@ -140,6 +357,11 @@ export class ResearchOrchestrator {
|
|
|
140
357
|
);
|
|
141
358
|
const concurrency = isLocalProvider ? 1 : Math.min(this.maxWorkers, plan.length);
|
|
142
359
|
|
|
360
|
+
const overallTimeoutMs = Number(options.timeout || process.env.KIKU_RESEARCH_TIMEOUT_MS) || (isLocalProvider ? 240000 : 120000);
|
|
361
|
+
const overallTimer = setTimeout(() => {
|
|
362
|
+
controller.abort(new Error(`Deep research timed out after ${overallTimeoutMs / 1000}s`));
|
|
363
|
+
}, overallTimeoutMs);
|
|
364
|
+
|
|
143
365
|
const executeJob = async () => {
|
|
144
366
|
try {
|
|
145
367
|
const settled = await mapConcurrent(plan, concurrency, async task => {
|
|
@@ -153,29 +375,50 @@ export class ResearchOrchestrator {
|
|
|
153
375
|
label: task.label,
|
|
154
376
|
});
|
|
155
377
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
378
|
+
const workerTimeoutMs = Number(options.workerTimeoutMs || process.env.KIKU_RESEARCH_WORKER_TIMEOUT_MS) || (isLocalProvider ? 90000 : 45000);
|
|
379
|
+
const workerAbort = new AbortController();
|
|
380
|
+
const timer = setTimeout(() => {
|
|
381
|
+
workerAbort.abort(new Error(`Worker ${task.label} timed out after ${workerTimeoutMs / 1000}s`));
|
|
382
|
+
}, workerTimeoutMs);
|
|
383
|
+
const onParentAbort = () => workerAbort.abort(controller.signal.reason);
|
|
384
|
+
controller.signal.addEventListener('abort', onParentAbort, { once: true });
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const worker = new ResearchSubagent({
|
|
388
|
+
client: this.client,
|
|
389
|
+
executeTool: cachedExecute,
|
|
390
|
+
allowedTools: task.tools,
|
|
391
|
+
maxIterations: this.maxIterations,
|
|
392
|
+
});
|
|
393
|
+
const result = await worker.run(task.objective, {
|
|
394
|
+
signal: workerAbort.signal,
|
|
395
|
+
skillContext: options.skillContext,
|
|
396
|
+
onProgress: event => {
|
|
397
|
+
if (event.type === 'tool') status.toolCalls += 1;
|
|
398
|
+
options.onProgress?.({ ...event, workerId: task.id, label: task.label });
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
status.state = 'complete';
|
|
403
|
+
options.onProgress?.({ type: 'worker-complete', id: task.id, label: task.label });
|
|
404
|
+
eventBus.emit(Events.AGENT_PLAN_STEP_UPDATE, {
|
|
405
|
+
planId: job.id,
|
|
406
|
+
stepId: task.id,
|
|
407
|
+
state: 'complete',
|
|
408
|
+
label: task.label,
|
|
409
|
+
});
|
|
410
|
+
return { id: task.id, label: task.label, ...result };
|
|
411
|
+
} catch (workerErr) {
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
if (controller.signal.aborted) throw workerErr;
|
|
414
|
+
console.warn(`[Kiku Research] Worker ${task.label} failed or timed out:`, workerErr?.message || workerErr);
|
|
415
|
+
status.state = 'failed';
|
|
416
|
+
status.error = workerErr?.message || 'Worker timed out';
|
|
417
|
+
return { id: task.id, label: task.label, answer: '', error: status.error, toolCalls: [] };
|
|
418
|
+
} finally {
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
controller.signal.removeEventListener('abort', onParentAbort);
|
|
421
|
+
}
|
|
179
422
|
});
|
|
180
423
|
|
|
181
424
|
const reports = settled.map((entry, index) => {
|
|
@@ -188,19 +431,49 @@ export class ResearchOrchestrator {
|
|
|
188
431
|
});
|
|
189
432
|
if (controller.signal.aborted) throw controller.signal.reason || new DOMException('Aborted', 'AbortError');
|
|
190
433
|
|
|
434
|
+
// Extract all real URLs discovered across all tool calls
|
|
435
|
+
const realToolUrls = extractAllRealUrls(reports.flatMap(r => (r.toolCalls || []).map(tc => tc.result)));
|
|
436
|
+
const realToolUrlSet = new Set(realToolUrls);
|
|
437
|
+
const recordRealUrls = (found) => {
|
|
438
|
+
for (const u of found) {
|
|
439
|
+
if (!realToolUrlSet.has(u)) {
|
|
440
|
+
realToolUrlSet.add(u);
|
|
441
|
+
realToolUrls.push(u);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
for (const promise of sharedToolCache.values()) {
|
|
446
|
+
try {
|
|
447
|
+
const res = await promise;
|
|
448
|
+
recordRealUrls(extractAllRealUrls([res]));
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
|
|
191
452
|
const usable = reports.filter(report => report.answer);
|
|
192
453
|
if (usable.length === 0) {
|
|
193
454
|
const { webExtract } = await import('./webSearch.js');
|
|
194
455
|
const fallbackRes = await webExtract(query);
|
|
195
456
|
if (fallbackRes?.text) {
|
|
196
457
|
usable.push({ label: 'Live Web Intelligence', answer: fallbackRes.text, sources: fallbackRes.sources || [] });
|
|
458
|
+
recordRealUrls(extractAllRealUrls([fallbackRes]));
|
|
197
459
|
} else {
|
|
198
460
|
throw new Error('All research workers failed and no web intelligence was returned.');
|
|
199
461
|
}
|
|
200
462
|
}
|
|
463
|
+
|
|
464
|
+
usable.forEach(report => {
|
|
465
|
+
if (report.answer) {
|
|
466
|
+
report.answer = expandTruncatedUrlsInText(report.answer, realToolUrls);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
201
470
|
const synthesisPrompt = options.mode === 'verify'
|
|
202
|
-
? 'Return a clear verdict: VERIFIED, UNVERIFIED, CONFLICTING, or FALSE. Explain briefly and cite only URLs
|
|
203
|
-
:
|
|
471
|
+
? 'Return a clear verdict: VERIFIED, UNVERIFIED, CONFLICTING, or FALSE. Explain briefly and cite only valid source URLs. Never truncate URLs with "..." or elide them.'
|
|
472
|
+
: `Synthesize a comprehensive, detailed, and polished answer with verified facts.
|
|
473
|
+
CRITICAL FORMATTING DIRECTIVE FOR SOURCES AND TABLES:
|
|
474
|
+
- In any markdown tables, lists, or citations, ALWAYS output the FULL, COMPLETE, UNTRUNCATED URL.
|
|
475
|
+
- NEVER shorten, elide, or truncate URLs with "..." (e.g. NEVER write "https://domain.com/play/..." or "https://...").
|
|
476
|
+
- Fans and users MUST be able to copy, click, or browse each source URL directly. Every URL in tables and lists must be 100% complete and fully formed.`;
|
|
204
477
|
const synthesis = await this.client.chat({
|
|
205
478
|
messages: [
|
|
206
479
|
{ role: 'system', content: `You are the Kiku research editor. ${synthesisPrompt} Never invent a source, date, title, or URL. Match the language of the question. Do not reveal hidden reasoning.` },
|
|
@@ -210,8 +483,13 @@ export class ResearchOrchestrator {
|
|
|
210
483
|
stream: false,
|
|
211
484
|
signal: controller.signal,
|
|
212
485
|
});
|
|
213
|
-
|
|
214
|
-
const
|
|
486
|
+
|
|
487
|
+
const rawAnswer = synthesis?.content || usable.map(report => `### ${report.label}\n${report.answer}`).join('\n\n');
|
|
488
|
+
const answer = expandTruncatedUrlsInText(rawAnswer, realToolUrls);
|
|
489
|
+
const combinedText = usable.map(report => report.answer).join('\n') + '\n' + answer;
|
|
490
|
+
const rawSources = uniqueUrls(combinedText, realToolUrls);
|
|
491
|
+
const sources = await filterAliveSources(rawSources, realToolUrls, options);
|
|
492
|
+
|
|
215
493
|
const result = {
|
|
216
494
|
id: job.id,
|
|
217
495
|
mode: options.mode || 'research',
|
|
@@ -231,6 +509,7 @@ export class ResearchOrchestrator {
|
|
|
231
509
|
});
|
|
232
510
|
return result;
|
|
233
511
|
} finally {
|
|
512
|
+
clearTimeout(overallTimer);
|
|
234
513
|
options.signal?.removeEventListener('abort', abortParent);
|
|
235
514
|
if (this.active?.id === job.id) {
|
|
236
515
|
this.active = null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export class RoutingInterceptor {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.historyCallSignatures = new Set();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interceptToolCall(toolCall, turnIndex = 0) {
|
|
7
|
+
if (!toolCall) return { valid: false, reason: 'Empty tool call' };
|
|
8
|
+
|
|
9
|
+
const name = toolCall.name || toolCall.function?.name || '';
|
|
10
|
+
const rawParams = toolCall.params || toolCall.function?.arguments || {};
|
|
11
|
+
let params = rawParams;
|
|
12
|
+
if (typeof params === 'string') {
|
|
13
|
+
try { params = JSON.parse(params); } catch { params = {}; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const signature = `${name}:${JSON.stringify(params)}`;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if (name === 'global_search' && turnIndex === 0) {
|
|
20
|
+
const query = (params.query || '').toLowerCase();
|
|
21
|
+
|
|
22
|
+
if (/仙剑|花间令|剧|drama|role|costume|character|เรื่อง|ซีรีส์|ละคร|ตัวละคร|บทบาท|in blossom|sword and fairy/i.test(query)) {
|
|
23
|
+
return {
|
|
24
|
+
rerouted: true,
|
|
25
|
+
replacement: { name: 'search_works', params: { query: params.query } },
|
|
26
|
+
reason: 'Auto-rerouted from illegal first-turn global_search to search_works'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if (/view from the wall|เพลง|song|single|ost|title|壁上观|如纱|不苦|find the way/i.test(query)) {
|
|
32
|
+
return {
|
|
33
|
+
rerouted: true,
|
|
34
|
+
replacement: { name: 'search_discography', params: { query: params.query } },
|
|
35
|
+
reason: 'Auto-rerouted from illegal first-turn global_search to search_discography'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if (/award|honor|รางวัล|เกียรติยศ|东方风云榜|风云榜|weibo night/i.test(query)) {
|
|
41
|
+
return {
|
|
42
|
+
rerouted: true,
|
|
43
|
+
replacement: { name: 'get_awards', params: { query: params.query } },
|
|
44
|
+
reason: 'Auto-rerouted from illegal first-turn global_search to get_awards'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (name === 'get_brand_endorsements') {
|
|
50
|
+
const brandParam = String(params.brand || params.query || '').toLowerCase();
|
|
51
|
+
if (/by my poi|be my poi|\bpoi\b|เพลง|mv|video|single|song|ost|stage/i.test(brandParam)) {
|
|
52
|
+
return {
|
|
53
|
+
rerouted: true,
|
|
54
|
+
replacement: { name: 'get_music_videos', params: { query: params.brand || params.query } },
|
|
55
|
+
reason: 'Auto-rerouted from misrouted get_brand_endorsements to get_music_videos for media/song query'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if (this.historyCallSignatures.has(signature)) {
|
|
62
|
+
return {
|
|
63
|
+
aborted: true,
|
|
64
|
+
reason: 'Duplicate call signature detected. Forcing synthesis.'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.historyCallSignatures.add(signature);
|
|
69
|
+
return { valid: true, toolCall };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
reset() {
|
|
73
|
+
this.historyCallSignatures.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const routingInterceptor = new RoutingInterceptor();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const RUMORED_OR_EXTERNAL_PROJECTS = [
|
|
2
|
+
{
|
|
3
|
+
id: 'qi-xing-cai',
|
|
4
|
+
keywords: ['七星彩', 'qi xiang cai', 'qi xing cai', 'ชีซิงไฉ่', 'seven star color'],
|
|
5
|
+
titleCn: '七星彩',
|
|
6
|
+
titleEn: 'Qi Xing Cai (Seven Star Color)',
|
|
7
|
+
status: 'rumored_casting',
|
|
8
|
+
producer: 'Youku (优酷) x Stellar Media (恒星引力传媒)',
|
|
9
|
+
originalNovel: '《七星彩》 by 明月珰 (Mingyue Dang)',
|
|
10
|
+
leads: '纪澄 (Ji Cheng) & 沈彻 (Shen Che)',
|
|
11
|
+
details: 'โปรเจกต์ซีรีส์จีนโบราณฟอร์มยักษ์ของ Youku ร่วมกับค่ายเหิงซิงอิ่นลี่ (ผู้สร้างของรักของข้า Love Between Fairy and Devil) ดัดแปลงจากนิยายของหมิงเยว่ตัง เล่าเรื่องราวของจี้เฉิง บุตรสาวตระกูลพ่อค้าที่ใช้ปฏิภาณไหวพริบเอาตัวรอดจนได้พบกับแม่ทัพเสิ่นเช่อ',
|
|
12
|
+
buzzReason: 'เหตุผลที่ชาวเน็ตให้ความสนใจอย่างมากคือ กระแสข่าวลือแคสติ้งนักแสดงนำ (Casting Rumors / 选角瓜) โดยมีการลือชื่อนักแสดงหญิงแถวหน้าหลายคนรวมถึง จวีจิ้งอี, หลูอวี้เสี่ยว ฯลฯ แต่ปัจจุบันยังไม่มีการประกาศยืนยันนักแสดงทางการจากทางค่ายหรือสตูดิโอ',
|
|
13
|
+
disambiguation: 'คนละผลงานกับซีรีส์ 《千香》 (The Eternal Fragrance / พันกลิ่นบุปผา) ที่จวีจิ้งอีนำแสดงร่วมกับ ซ่งเวยหลง'
|
|
14
|
+
}
|
|
15
|
+
];
|