kiku-agent 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -12
- package/package.json +2 -1
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +14 -8
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +18 -18
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -1,4 +1,179 @@
|
|
|
1
|
-
import { dynamicToolRouter } from './toolRouter.js';
|
|
1
|
+
import { dynamicToolRouter } from './toolRouter.js';
|
|
2
|
+
|
|
3
|
+
const graphemeSegmenter = typeof Intl?.Segmenter === 'function'
|
|
4
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
5
|
+
: null;
|
|
6
|
+
|
|
7
|
+
export function sanitizePromptString(input = '') {
|
|
8
|
+
const value = String(input);
|
|
9
|
+
let clean = '';
|
|
10
|
+
for (let i = 0; i < value.length; i++) {
|
|
11
|
+
const code = value.charCodeAt(i);
|
|
12
|
+
if (code >= 0xD800 && code <= 0xDBFF) {
|
|
13
|
+
const next = value.charCodeAt(i + 1);
|
|
14
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
15
|
+
clean += value[i] + value[i + 1];
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (code >= 0xDC00 && code <= 0xDFFF) continue;
|
|
21
|
+
clean += value[i];
|
|
22
|
+
}
|
|
23
|
+
return clean.normalize('NFC');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function truncatePromptString(input, maxLength, suffix = '') {
|
|
27
|
+
const clean = sanitizePromptString(input);
|
|
28
|
+
const limit = Math.max(0, Math.floor(Number(maxLength) || 0));
|
|
29
|
+
if (clean.length <= limit) return clean;
|
|
30
|
+
if (limit === 0) return '';
|
|
31
|
+
|
|
32
|
+
let output = '';
|
|
33
|
+
const segments = graphemeSegmenter
|
|
34
|
+
? Array.from(graphemeSegmenter.segment(clean), part => part.segment)
|
|
35
|
+
: Array.from(clean);
|
|
36
|
+
for (const segment of segments) {
|
|
37
|
+
if (output.length + segment.length > limit) break;
|
|
38
|
+
output += segment;
|
|
39
|
+
}
|
|
40
|
+
return output + suffix;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function sanitizePromptValue(value) {
|
|
44
|
+
if (typeof value === 'string') return sanitizePromptString(value);
|
|
45
|
+
if (Array.isArray(value)) return value.map(sanitizePromptValue);
|
|
46
|
+
if (!value || typeof value !== 'object') return value;
|
|
47
|
+
const clean = {};
|
|
48
|
+
for (const [key, item] of Object.entries(value)) {
|
|
49
|
+
clean[sanitizePromptString(key)] = sanitizePromptValue(item);
|
|
50
|
+
}
|
|
51
|
+
return clean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function prependUserContext(content, preamble) {
|
|
55
|
+
const prefix = `[Previous Session Context Summary: ${sanitizePromptString(preamble)}]\n\n`;
|
|
56
|
+
if (typeof content === 'string') return prefix + sanitizePromptString(content);
|
|
57
|
+
if (Array.isArray(content)) {
|
|
58
|
+
return [{ type: 'text', text: prefix.trim() }, ...sanitizePromptValue(content)];
|
|
59
|
+
}
|
|
60
|
+
return prefix + sanitizePromptString(content == null ? '' : JSON.stringify(content));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function alignChatMessageRoles(messages = []) {
|
|
64
|
+
if (!Array.isArray(messages)) return [];
|
|
65
|
+
const cleanMessages = sanitizePromptValue(messages).filter(message => message && typeof message === 'object');
|
|
66
|
+
const firstSystemIndex = cleanMessages[0]?.role === 'system' ? 0 : -1;
|
|
67
|
+
const leadingSystem = firstSystemIndex === 0 ? cleanMessages[0] : null;
|
|
68
|
+
const contextualSystemMessages = cleanMessages
|
|
69
|
+
.filter((message, index) => message.role === 'system' && index !== firstSystemIndex)
|
|
70
|
+
.map(message => message.content)
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
const aligned = cleanMessages.filter(message => message.role !== 'system');
|
|
73
|
+
|
|
74
|
+
if (contextualSystemMessages.length > 0) {
|
|
75
|
+
let targetIndex = -1;
|
|
76
|
+
for (let i = aligned.length - 1; i >= 0; i--) {
|
|
77
|
+
if (aligned[i].role === 'user') {
|
|
78
|
+
targetIndex = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const preamble = contextualSystemMessages.join('\n\n');
|
|
83
|
+
if (targetIndex >= 0) {
|
|
84
|
+
aligned[targetIndex] = {
|
|
85
|
+
...aligned[targetIndex],
|
|
86
|
+
content: prependUserContext(aligned[targetIndex].content, preamble),
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
aligned.unshift({ role: 'user', content: prependUserContext('', preamble).trim() });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return leadingSystem ? [leadingSystem, ...aligned] : aligned;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function messageTokens(messages) {
|
|
97
|
+
return estimateTokenCount(messages);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function omissionMessage(message) {
|
|
101
|
+
return {
|
|
102
|
+
...message,
|
|
103
|
+
content: JSON.stringify({
|
|
104
|
+
status: 'omitted',
|
|
105
|
+
authoritative: false,
|
|
106
|
+
message: 'Older tool output omitted by the context budget guard.',
|
|
107
|
+
}),
|
|
108
|
+
_omittedForContext: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function prepareChatMessagesForDispatch(messages = [], options = {}) {
|
|
113
|
+
const aligned = alignChatMessageRoles(messages);
|
|
114
|
+
const sanitizedTools = sanitizePromptValue(Array.isArray(options.tools) ? options.tools : []);
|
|
115
|
+
const maxContextTokens = Math.max(1024, Math.floor(Number(options.maxContextTokens) || 32768));
|
|
116
|
+
const generationReserve = Math.max(0, Math.floor(Number(options.generationReserve) || 0));
|
|
117
|
+
const systemTokens = aligned[0]?.role === 'system' ? messageTokens([aligned[0]]) : 0;
|
|
118
|
+
const toolDefinitionTokens = sanitizedTools.length > 0
|
|
119
|
+
? estimateTextTokens(JSON.stringify(sanitizedTools))
|
|
120
|
+
: 0;
|
|
121
|
+
const allocation = Math.max(256, maxContextTokens - systemTokens - generationReserve - toolDefinitionTokens);
|
|
122
|
+
const threshold = Math.max(128, Math.floor(allocation * Math.min(1, Math.max(0.5, Number(options.pruneAt) || 0.85))));
|
|
123
|
+
const prepared = aligned.map(message => ({ ...message }));
|
|
124
|
+
const prefixCount = prepared[0]?.role === 'system' ? 1 : 0;
|
|
125
|
+
const historyUsage = () => messageTokens(prepared.slice(prefixCount));
|
|
126
|
+
|
|
127
|
+
let pruned = false;
|
|
128
|
+
while (historyUsage() > threshold) {
|
|
129
|
+
const userIndexes = [];
|
|
130
|
+
for (let i = prefixCount; i < prepared.length; i++) {
|
|
131
|
+
if (prepared[i].role === 'user') userIndexes.push(i);
|
|
132
|
+
}
|
|
133
|
+
if (userIndexes.length <= 1) break;
|
|
134
|
+
const nextTurn = userIndexes[1];
|
|
135
|
+
prepared.splice(prefixCount, nextTurn - prefixCount);
|
|
136
|
+
pruned = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (let i = prefixCount; i < prepared.length && historyUsage() > threshold; i++) {
|
|
140
|
+
if (prepared[i].role !== 'tool' || prepared[i]._omittedForContext) continue;
|
|
141
|
+
prepared[i] = omissionMessage(prepared[i]);
|
|
142
|
+
pruned = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (historyUsage() > threshold) {
|
|
146
|
+
for (let i = prefixCount; i < prepared.length && historyUsage() > threshold; i++) {
|
|
147
|
+
const message = prepared[i];
|
|
148
|
+
if (typeof message.content !== 'string' || message.content.length <= 256) continue;
|
|
149
|
+
const currentTokens = estimateTextTokens(message.content);
|
|
150
|
+
const overflow = historyUsage() - threshold;
|
|
151
|
+
const targetTokens = Math.max(64, currentTokens - overflow);
|
|
152
|
+
const targetChars = Math.max(256, Math.floor(message.content.length * (targetTokens / Math.max(1, currentTokens))));
|
|
153
|
+
prepared[i] = {
|
|
154
|
+
...message,
|
|
155
|
+
content: truncatePromptString(message.content, targetChars, '…'),
|
|
156
|
+
_truncatedForContext: true,
|
|
157
|
+
};
|
|
158
|
+
pruned = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
messages: sanitizePromptValue(prepared),
|
|
164
|
+
tools: sanitizedTools,
|
|
165
|
+
budget: {
|
|
166
|
+
maxContextTokens,
|
|
167
|
+
systemTokens,
|
|
168
|
+
generationReserve,
|
|
169
|
+
toolDefinitionTokens,
|
|
170
|
+
allocation,
|
|
171
|
+
threshold,
|
|
172
|
+
promptTokens: messageTokens(prepared),
|
|
173
|
+
pruned,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
2
177
|
|
|
3
178
|
export function estimateTextTokens(text) {
|
|
4
179
|
if (!text) return 0;
|
|
@@ -56,10 +231,10 @@ export class ContextCompactor {
|
|
|
56
231
|
const points = [];
|
|
57
232
|
for (const msg of messages) {
|
|
58
233
|
if (msg.role === 'user' && typeof msg.content === 'string' && msg.content.trim()) {
|
|
59
|
-
const text = msg.content
|
|
234
|
+
const text = truncatePromptString(msg.content, 100).replace(/\n+/g, ' ').trim();
|
|
60
235
|
points.push(`User asked: "${text}"`);
|
|
61
236
|
} else if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.trim()) {
|
|
62
|
-
const text = msg.content
|
|
237
|
+
const text = truncatePromptString(msg.content, 120).replace(/\n+/g, ' ').trim();
|
|
63
238
|
points.push(`Assistant covered: "${text}"`);
|
|
64
239
|
} else if (msg.role === 'tool') {
|
|
65
240
|
const toolName = msg.name || 'tool';
|
|
@@ -86,13 +261,15 @@ export class ContextCompactor {
|
|
|
86
261
|
};
|
|
87
262
|
}
|
|
88
263
|
|
|
89
|
-
_assemble(systemMessage, olderMessages, recentMessages, summaryText) {
|
|
90
|
-
const text = summaryText ?? this.summarizeHistorySync(olderMessages);
|
|
91
|
-
const compacted = [];
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
264
|
+
_assemble(systemMessage, olderMessages, recentMessages, summaryText) {
|
|
265
|
+
const text = summaryText ?? this.summarizeHistorySync(olderMessages);
|
|
266
|
+
const compacted = systemMessage ? [systemMessage, ...recentMessages] : [...recentMessages];
|
|
267
|
+
if (!text) return alignChatMessageRoles(compacted);
|
|
268
|
+
const summaryDirective = {
|
|
269
|
+
role: 'system',
|
|
270
|
+
content: text,
|
|
271
|
+
};
|
|
272
|
+
return alignChatMessageRoles([...compacted, summaryDirective]);
|
|
96
273
|
}
|
|
97
274
|
|
|
98
275
|
_shouldCompact(conversationHistory, options) {
|
|
@@ -147,11 +324,6 @@ export function compactToolsForPrompt(query = '', options = {}) {
|
|
|
147
324
|
|
|
148
325
|
|
|
149
326
|
if (/photoshoot|magazine|editorial|port|大片|九月刊|写真|นิตยสาร|แฟชั่นเซ็ต/i.test(rawQuery)) {
|
|
150
|
-
|
|
151
|
-
if (!/contract|ambassador|commercial|代言|พรีเซนเตอร์|ทูต/i.test(rawQuery)) {
|
|
152
|
-
routed = routed.filter(t => (t.function?.name || t.schema?.name || t.name) !== 'get_brand_endorsements');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
327
|
const hasGallery = routed.some(t => (t.function?.name || t.schema?.name || t.name) === 'get_gallery_photos');
|
|
156
328
|
if (!hasGallery) {
|
|
157
329
|
const galleryTool = router.allTools?.find(t => (t.function?.name || t.schema?.name) === 'get_gallery_photos');
|
|
@@ -159,6 +331,9 @@ export function compactToolsForPrompt(query = '', options = {}) {
|
|
|
159
331
|
routed.unshift(galleryTool);
|
|
160
332
|
}
|
|
161
333
|
}
|
|
334
|
+
if (options.schemaStrategy === 'domain_grouped' || !/แบรนด์|พรีเซนเตอร์|ambassador|endorsement/i.test(rawQuery)) {
|
|
335
|
+
routed = routed.filter(t => (t.function?.name || t.schema?.name || t.name) !== 'get_brand_endorsements');
|
|
336
|
+
}
|
|
162
337
|
}
|
|
163
338
|
|
|
164
339
|
return routed.map(t => {
|
|
@@ -193,6 +368,17 @@ const ESSENTIAL_SONG_KEYS = new Set([
|
|
|
193
368
|
const ESSENTIAL_ROLE_KEYS = new Set([
|
|
194
369
|
'workTitle', 'year', 'role', 'roleCn'
|
|
195
370
|
]);
|
|
371
|
+
const ESSENTIAL_WEB_KEYS = new Set([
|
|
372
|
+
'title', 'name', 'alt', 'url', 'link', 'snippet', 'description', 'content', 'source', 'date', 'author',
|
|
373
|
+
'imageUrl', 'thumbUrl', 'sourcePageUrl', 'width', 'height', 'relevanceScore',
|
|
374
|
+
]);
|
|
375
|
+
const LINK_VALUE_KEYS = /(url|link|href|image|thumb|pic|cover)$/i;
|
|
376
|
+
|
|
377
|
+
function isLinkValue(key, value) {
|
|
378
|
+
return typeof value === 'string'
|
|
379
|
+
&& LINK_VALUE_KEYS.test(String(key || ''))
|
|
380
|
+
&& /^https?:\/\//i.test(value);
|
|
381
|
+
}
|
|
196
382
|
|
|
197
383
|
function stripSearchUrls(obj) {
|
|
198
384
|
if (Array.isArray(obj)) {
|
|
@@ -239,6 +425,12 @@ export function compactToolResultForContext(result, options = {}) {
|
|
|
239
425
|
if (Array.isArray(stripped.videos)) stripped.videos = projectList(stripped.videos, ESSENTIAL_VIDEO_KEYS);
|
|
240
426
|
if (Array.isArray(stripped.musicVideos)) stripped.musicVideos = projectList(stripped.musicVideos, ESSENTIAL_VIDEO_KEYS);
|
|
241
427
|
if (Array.isArray(stripped.roles)) stripped.roles = projectList(stripped.roles, ESSENTIAL_ROLE_KEYS);
|
|
428
|
+
if (Array.isArray(stripped.results)) stripped.results = projectList(stripped.results, ESSENTIAL_WEB_KEYS);
|
|
429
|
+
|
|
430
|
+
if (stripped.scrapedPages) delete stripped.scrapedPages;
|
|
431
|
+
if (typeof stripped.combinedContent === 'string' && stripped.combinedContent.length > 2000) {
|
|
432
|
+
stripped.combinedContent = truncatePromptString(stripped.combinedContent, 2000, '...');
|
|
433
|
+
}
|
|
242
434
|
|
|
243
435
|
|
|
244
436
|
let serialized = JSON.stringify(stripped);
|
|
@@ -255,15 +447,15 @@ export function compactToolResultForContext(result, options = {}) {
|
|
|
255
447
|
if (node && typeof node === 'object') {
|
|
256
448
|
const out = {};
|
|
257
449
|
for (const [k, v] of Object.entries(node)) {
|
|
258
|
-
out[k] = truncateNode(v);
|
|
450
|
+
out[k] = isLinkValue(k, v) ? v : truncateNode(v);
|
|
259
451
|
}
|
|
260
452
|
return out;
|
|
261
453
|
}
|
|
262
454
|
if (typeof node === 'string' && node.length > 240) {
|
|
263
|
-
return
|
|
455
|
+
return truncatePromptString(node, 240, '…');
|
|
264
456
|
}
|
|
265
457
|
return node;
|
|
266
|
-
};
|
|
458
|
+
};
|
|
267
459
|
|
|
268
460
|
stripped = truncateNode(stripped);
|
|
269
461
|
serialized = JSON.stringify(stripped);
|
|
@@ -275,12 +467,12 @@ export function compactToolResultForContext(result, options = {}) {
|
|
|
275
467
|
if (node && typeof node === 'object') {
|
|
276
468
|
const out = {};
|
|
277
469
|
for (const [k, v] of Object.entries(node)) {
|
|
278
|
-
out[k] = aggressive(v);
|
|
470
|
+
out[k] = isLinkValue(k, v) ? v : aggressive(v);
|
|
279
471
|
}
|
|
280
472
|
return out;
|
|
281
473
|
}
|
|
282
|
-
if (typeof node === 'string' && node.length > 120) {
|
|
283
|
-
return
|
|
474
|
+
if (typeof node === 'string' && node.length > 120) {
|
|
475
|
+
return truncatePromptString(node, 120, '…');
|
|
284
476
|
}
|
|
285
477
|
return node;
|
|
286
478
|
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
function runProcessCapture(cmd, args, timeoutMs = 2500) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
let stdout = '';
|
|
7
|
+
let stderr = '';
|
|
8
|
+
let settled = false;
|
|
9
|
+
|
|
10
|
+
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
11
|
+
|
|
12
|
+
const finish = (result) => {
|
|
13
|
+
if (settled) return;
|
|
14
|
+
settled = true;
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
resolve(result);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
21
|
+
finish({ ok: false, stdout: '', stderr: 'timeout' });
|
|
22
|
+
}, timeoutMs);
|
|
23
|
+
|
|
24
|
+
child.stdout?.on('data', d => { stdout += d.toString(); });
|
|
25
|
+
child.stderr?.on('data', d => { stderr += d.toString(); });
|
|
26
|
+
|
|
27
|
+
child.on('error', (err) => finish({ ok: false, stdout: '', stderr: err.message }));
|
|
28
|
+
child.on('close', (code) => finish({ ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() }));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectCpuVendor(model = '') {
|
|
33
|
+
const m = String(model).toLowerCase();
|
|
34
|
+
if (m.includes('intel')) return 'Intel';
|
|
35
|
+
if (m.includes('amd') || m.includes('ryzen') || m.includes('epyc')) return 'AMD';
|
|
36
|
+
if (m.includes('apple') || m.includes('m1') || m.includes('m2') || m.includes('m3') || m.includes('m4')) return 'Apple';
|
|
37
|
+
if (m.includes('arm') || m.includes('cortex') || m.includes('snapdragon')) return 'ARM';
|
|
38
|
+
return 'Generic';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let cachedCpuMetrics = null;
|
|
42
|
+
const CPU_METRICS_TTL_MS = 60_000;
|
|
43
|
+
|
|
44
|
+
export function clearCpuMetricsCache() {
|
|
45
|
+
cachedCpuMetrics = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getCpuMetrics(force = false) {
|
|
49
|
+
if (!force && cachedCpuMetrics && (Date.now() - cachedCpuMetrics.timestamp < CPU_METRICS_TTL_MS)) {
|
|
50
|
+
return cachedCpuMetrics.data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cpus = os.cpus() || [];
|
|
54
|
+
const rawModel = cpus[0]?.model?.trim() || 'Generic CPU';
|
|
55
|
+
const logicalCores = Math.max(1, cpus.length || 1);
|
|
56
|
+
let physicalCores = logicalCores;
|
|
57
|
+
let maxClockMhz = cpus[0]?.speed ? Math.round(cpus[0].speed) : null;
|
|
58
|
+
let socket = null;
|
|
59
|
+
let source = 'os.cpus';
|
|
60
|
+
|
|
61
|
+
if (process.platform === 'win32') {
|
|
62
|
+
try {
|
|
63
|
+
const psScript = 'Get-CimInstance Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed, SocketDesignation | ConvertTo-Json -Compress';
|
|
64
|
+
const res = await runProcessCapture('powershell', ['-NoProfile', '-NonInteractive', '-Command', psScript], 2500);
|
|
65
|
+
if (res.ok && res.stdout) {
|
|
66
|
+
const parsed = JSON.parse(res.stdout);
|
|
67
|
+
const item = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
68
|
+
if (item) {
|
|
69
|
+
if (item.NumberOfCores && Number.isFinite(Number(item.NumberOfCores))) {
|
|
70
|
+
physicalCores = Math.max(1, Number(item.NumberOfCores));
|
|
71
|
+
}
|
|
72
|
+
if (item.MaxClockSpeed && Number.isFinite(Number(item.MaxClockSpeed))) {
|
|
73
|
+
maxClockMhz = Math.round(Number(item.MaxClockSpeed));
|
|
74
|
+
}
|
|
75
|
+
if (item.SocketDesignation) {
|
|
76
|
+
socket = String(item.SocketDesignation).trim();
|
|
77
|
+
}
|
|
78
|
+
source = 'Win32_Processor';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
} else if (process.platform === 'darwin') {
|
|
83
|
+
try {
|
|
84
|
+
const [physRes, logRes] = await Promise.all([
|
|
85
|
+
runProcessCapture('sysctl', ['-n', 'hw.physicalcpu'], 1500),
|
|
86
|
+
runProcessCapture('sysctl', ['-n', 'hw.logicalcpu'], 1500),
|
|
87
|
+
]);
|
|
88
|
+
if (physRes.ok && physRes.stdout) {
|
|
89
|
+
const n = parseInt(physRes.stdout, 10);
|
|
90
|
+
if (Number.isFinite(n) && n > 0) physicalCores = n;
|
|
91
|
+
source = 'sysctl';
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
} else if (process.platform === 'linux') {
|
|
95
|
+
try {
|
|
96
|
+
const res = await runProcessCapture('lscpu', ['-J'], 1500);
|
|
97
|
+
if (res.ok && res.stdout) {
|
|
98
|
+
const json = JSON.parse(res.stdout);
|
|
99
|
+
const list = json?.lscpu || [];
|
|
100
|
+
const findField = (name) => list.find(x => x.field?.toLowerCase()?.includes(name))?.data;
|
|
101
|
+
const coresPerSocket = parseInt(findField('core(s) per socket'), 10);
|
|
102
|
+
const sockets = parseInt(findField('socket(s)'), 10) || 1;
|
|
103
|
+
if (Number.isFinite(coresPerSocket) && coresPerSocket > 0) {
|
|
104
|
+
physicalCores = coresPerSocket * sockets;
|
|
105
|
+
source = 'lscpu';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if (physicalCores === logicalCores && logicalCores >= 4) {
|
|
113
|
+
const vendor = detectCpuVendor(rawModel);
|
|
114
|
+
if (vendor === 'Intel' || vendor === 'AMD') {
|
|
115
|
+
|
|
116
|
+
physicalCores = Math.max(1, Math.floor(logicalCores / 2));
|
|
117
|
+
source = 'heuristic-x86-smt';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const hyperThreading = logicalCores > physicalCores;
|
|
122
|
+
const smtMultiplier = physicalCores > 0 ? Number((logicalCores / physicalCores).toFixed(2)) : 1;
|
|
123
|
+
const vendor = detectCpuVendor(rawModel);
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
const cleanModel = rawModel
|
|
127
|
+
.replace(/\(R\)/gi, '')
|
|
128
|
+
.replace(/\(TM\)/gi, '')
|
|
129
|
+
.replace(/\s+/g, ' ')
|
|
130
|
+
.trim();
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
const recommendedComputeThreads = physicalCores;
|
|
134
|
+
const recommendedBatchThreads = logicalCores;
|
|
135
|
+
|
|
136
|
+
const data = {
|
|
137
|
+
model: cleanModel,
|
|
138
|
+
rawModel,
|
|
139
|
+
vendor,
|
|
140
|
+
physicalCores,
|
|
141
|
+
logicalCores,
|
|
142
|
+
hyperThreading,
|
|
143
|
+
smtMultiplier,
|
|
144
|
+
maxClockMhz,
|
|
145
|
+
socket,
|
|
146
|
+
source,
|
|
147
|
+
recommendedComputeThreads,
|
|
148
|
+
recommendedBatchThreads,
|
|
149
|
+
statusText: hyperThreading
|
|
150
|
+
? `Active (${smtMultiplier}x SMT • ${physicalCores}C/${logicalCores}T)`
|
|
151
|
+
: `Single-Threaded (${physicalCores}C/${logicalCores}T)`,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
cachedCpuMetrics = { data, timestamp: Date.now() };
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function resolveOptimalThreadBudget({ mode = 'auto', requested = null, cpuInfo = null } = {}) {
|
|
159
|
+
const cpu = cpuInfo || { physicalCores: 6, logicalCores: 12, hyperThreading: true };
|
|
160
|
+
const req = Number(requested);
|
|
161
|
+
const hasExplicit = Number.isFinite(req) && req >= 1 && req <= 256;
|
|
162
|
+
|
|
163
|
+
switch (String(mode).toLowerCase()) {
|
|
164
|
+
case 'physical':
|
|
165
|
+
return {
|
|
166
|
+
threads: cpu.physicalCores,
|
|
167
|
+
mode: 'physical',
|
|
168
|
+
explanation: `Locked to ${cpu.physicalCores} physical cores (avoids SMT cache thrashing for optimal LLM generation speed)`,
|
|
169
|
+
};
|
|
170
|
+
case 'smt':
|
|
171
|
+
case 'all':
|
|
172
|
+
case 'hyperthreading':
|
|
173
|
+
return {
|
|
174
|
+
threads: cpu.logicalCores,
|
|
175
|
+
mode: 'smt',
|
|
176
|
+
explanation: `Utilizing all ${cpu.logicalCores} logical threads (maximum parallel prediction throughput)`,
|
|
177
|
+
};
|
|
178
|
+
case 'custom':
|
|
179
|
+
return {
|
|
180
|
+
threads: hasExplicit ? Math.floor(req) : cpu.physicalCores,
|
|
181
|
+
mode: 'custom',
|
|
182
|
+
explanation: `Custom thread count: ${hasExplicit ? Math.floor(req) : cpu.physicalCores}`,
|
|
183
|
+
};
|
|
184
|
+
case 'auto':
|
|
185
|
+
default:
|
|
186
|
+
if (hasExplicit) {
|
|
187
|
+
return {
|
|
188
|
+
threads: Math.min(Math.floor(req), cpu.logicalCores),
|
|
189
|
+
mode: 'custom',
|
|
190
|
+
explanation: `User requested ${Math.floor(req)} threads (within ${cpu.logicalCores} thread hardware ceiling)`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
threads: cpu.physicalCores,
|
|
195
|
+
mode: 'auto',
|
|
196
|
+
explanation: `Auto-tuned to ${cpu.physicalCores} physical cores for optimal single-stream latency`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|