kiku-agent 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +69 -12
  2. package/package.json +2 -1
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/modules/evaluation/runner.js +4 -14
  18. package/server/agent/eval/modules/evaluation/scoring.js +15 -3
  19. package/server/agent/eval/modules/server/evalApi.js +5 -5
  20. package/server/agent/eval/modules/ui/evalUi.js +7 -4
  21. package/server/agent/eventBus.js +1 -0
  22. package/server/agent/ink/main.jsx +196 -191
  23. package/server/agent/jobManager.js +11 -1
  24. package/server/agent/lanDiscovery.js +721 -23
  25. package/server/agent/llmClient.js +317 -71
  26. package/server/agent/lmStudioWakeup.js +517 -65
  27. package/server/agent/locationService.js +797 -0
  28. package/server/agent/mcpQuizData.js +509 -0
  29. package/server/agent/memory/contextAllocator.js +19 -7
  30. package/server/agent/memory.js +3 -1
  31. package/server/agent/modelCompatibility.js +352 -0
  32. package/server/agent/modelDefaults.js +1 -0
  33. package/server/agent/modelManager.js +46 -2
  34. package/server/agent/newsWatch.js +9 -3
  35. package/server/agent/processManager.js +229 -0
  36. package/server/agent/prompts.js +174 -8
  37. package/server/agent/routingInterceptor.js +9 -1
  38. package/server/agent/sanitizer.js +41 -6
  39. package/server/agent/searchQueryPlanner.js +202 -0
  40. package/server/agent/session.js +59 -4
  41. package/server/agent/tests/agentFollowUps.test.js +128 -2
  42. package/server/agent/tests/agentMusicControl.test.js +245 -0
  43. package/server/agent/tests/apiRouter.test.js +1 -1
  44. package/server/agent/tests/autoContext.test.js +176 -0
  45. package/server/agent/tests/bilibiliEmbed.test.js +45 -0
  46. package/server/agent/tests/chatHardening.test.js +121 -0
  47. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  48. package/server/agent/tests/clarifyingQuestions.test.js +33 -0
  49. package/server/agent/tests/cliAutomation.test.js +38 -1
  50. package/server/agent/tests/cloudProviders.test.js +91 -3
  51. package/server/agent/tests/commands.test.js +92 -3
  52. package/server/agent/tests/contextIntelligence.test.js +50 -3
  53. package/server/agent/tests/cpuMetrics.test.js +101 -0
  54. package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
  55. package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
  56. package/server/agent/tests/engineService.test.js +436 -0
  57. package/server/agent/tests/engineUnload.test.js +94 -0
  58. package/server/agent/tests/envCommand.test.js +69 -0
  59. package/server/agent/tests/evalMetrics.test.js +8 -0
  60. package/server/agent/tests/fetchApi.test.js +99 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +2 -0
  62. package/server/agent/tests/lanDiscovery.test.js +5 -0
  63. package/server/agent/tests/lanInterfaces.test.js +510 -0
  64. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  65. package/server/agent/tests/locationTool.test.js +278 -0
  66. package/server/agent/tests/memoryRoute.test.js +173 -0
  67. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  68. package/server/agent/tests/modelCompatibility.test.js +148 -0
  69. package/server/agent/tests/modelManager.test.js +32 -0
  70. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +4 -2
  72. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  73. package/server/agent/tests/openDomainSearch.test.js +121 -0
  74. package/server/agent/tests/processManager.test.js +144 -0
  75. package/server/agent/tests/providerPersistence.test.js +10 -5
  76. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  77. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  78. package/server/agent/tests/securityAuditRemediation.test.js +16 -9
  79. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  80. package/server/agent/tests/securityHardening.test.js +4 -4
  81. package/server/agent/tests/sessionAndCopy.test.js +39 -4
  82. package/server/agent/tests/sidebarControls.test.js +34 -0
  83. package/server/agent/tests/statefulMemory.test.js +8 -1
  84. package/server/agent/tests/textProtocolLoop.test.js +71 -0
  85. package/server/agent/tests/textToolParsing.test.js +48 -0
  86. package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
  87. package/server/agent/tests/unifiedEndpoints.test.js +13 -6
  88. package/server/agent/tests/v110Features.test.js +1 -1
  89. package/server/agent/tests/vramEstimator.test.js +75 -0
  90. package/server/agent/tests/webChatDocs.test.js +199 -0
  91. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  92. package/server/agent/tests/webChatInspection.test.js +256 -5
  93. package/server/agent/tests/webChatMcp.test.js +94 -0
  94. package/server/agent/tests/webExtract.test.js +23 -0
  95. package/server/agent/tests/webSources.test.js +46 -12
  96. package/server/agent/textToolProtocol.js +173 -46
  97. package/server/agent/toolRouter.js +318 -44
  98. package/server/agent/tools.js +877 -96
  99. package/server/agent/ui.js +39 -34
  100. package/server/agent/vramService.js +35 -54
  101. package/server/agent/webSearch.js +441 -162
  102. package/server/agent/websearch/orchestrator.js +15 -5
  103. package/server/agent/websearch/resilience/cache.js +9 -2
  104. package/server/agent/websearch/security/ssrf.js +41 -10
  105. package/server/agent/youtubeAuth.js +5 -20
  106. package/server/apiRouter.js +5558 -4422
  107. package/server/mcp/data/discography.js +2 -2
  108. package/server/mcp/examples/antigravity.example.json +17 -0
  109. package/server/mcp/handler.js +8 -4
  110. package/server/mcp/security.js +220 -21
  111. package/server/mcp/server.js +2 -1
  112. package/server/mcp/tests/connection.test.js +14 -8
  113. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  114. package/server/mcp/tools.js +7 -1
  115. package/server/supabase_migration_prefix.sql +102 -0
  116. package/server/supabase_schema.sql +18 -18
  117. package/server/web-chat/core/connection.js +179 -218
  118. package/server/web-chat/core/conversationContext.js +9 -2
  119. package/server/web-chat/core/directCatalogEngine.js +52 -28
  120. package/server/web-chat/core/dramaKnowledge.js +33 -22
  121. package/server/web-chat/core/followUpEngine.js +735 -81
  122. package/server/web-chat/core/futureEventGuard.js +5 -1
  123. package/server/web-chat/core/musicControlAction.js +70 -0
  124. package/server/web-chat/core/security.js +32 -21
  125. package/server/web-chat/core/toolInspector.js +6 -6
  126. package/server/web-chat/core/urlGuard.js +45 -0
  127. package/server/web-chat/router.js +204 -157
  128. package/server/web-chat/routes/chatRoute.js +885 -488
  129. package/server/web-chat/routes/engineRoute.js +270 -0
  130. package/server/web-chat/routes/exportRoute.js +1 -1
  131. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  132. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  133. package/server/web-chat/routes/mcpRoute.js +386 -0
  134. package/server/web-chat/routes/memoryRoute.js +253 -0
  135. package/server/web-chat/routes/providerRoute.js +49 -16
  136. package/server/web-chat/routes/staticRoute.js +306 -17
  137. package/server/web-chat/routes/statusRoute.js +194 -73
  138. package/server/web-chat/routes/suggestRoute.js +11 -3
  139. package/server/web-chat/routes/verifyRoute.js +26 -4
  140. package/server/web-chat/server.js +7 -6
  141. package/server/web-chat/ui/docsBuilder.js +3558 -0
  142. package/server/web-chat/ui/htmlBuilder.js +12 -0
  143. package/server/web-chat/ui/scripts/client.js +3401 -1302
  144. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  145. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  146. package/server/web-chat/ui/styles/lan-card.css +165 -0
  147. package/server/web-chat/ui/styles/main.css +4289 -447
  148. package/server/web-chat/ui/sw.js +64 -0
  149. package/server/web-chat/ui/templates/body.html +407 -281
  150. package/src/data/awardsData.js +146 -146
  151. package/src/data/musicVideoData.js +6 -4
  152. package/src/lib/vram/estimator.js +195 -19
@@ -0,0 +1,352 @@
1
+ const MODEL_PROFILES = [
2
+ {
3
+ id: 'qwen3-coder',
4
+ match: /qwen3[-_]?coder/i,
5
+ toolProtocol: 'qwen3-xml',
6
+ reasoningProtocol: 'qwen3',
7
+ supports: { nativeTools: true, legacyFunctionCall: true, textToolCalls: true, parallelTools: true },
8
+ contextLength: 32768,
9
+ },
10
+ {
11
+ id: 'qwen3',
12
+ match: /(?:qwen3|qwq)/i,
13
+ toolProtocol: 'hermes',
14
+ reasoningProtocol: 'qwen3',
15
+ supports: { nativeTools: true, legacyFunctionCall: true, textToolCalls: true, parallelTools: true },
16
+ contextLength: 32768,
17
+ },
18
+ {
19
+ id: 'qwen2.5',
20
+ match: /qwen(?:2[._]?5|2\.5)/i,
21
+ toolProtocol: 'hermes',
22
+ reasoningProtocol: 'none',
23
+ supports: { nativeTools: true, legacyFunctionCall: true, textToolCalls: true, parallelTools: true },
24
+ contextLength: 32768,
25
+ },
26
+ {
27
+ id: 'deepseek-reasoning',
28
+ match: /deepseek.*(?:r1|reasoner|reasoning)|deepseek-r1/i,
29
+ toolProtocol: 'openai',
30
+ reasoningProtocol: 'deepseek',
31
+ supports: { nativeTools: true, legacyFunctionCall: true, textToolCalls: true, parallelTools: false },
32
+ contextLength: 32768,
33
+ },
34
+ {
35
+ id: 'llama3',
36
+ match: /llama[-_]?3|llama4/i,
37
+ toolProtocol: 'llama-json',
38
+ reasoningProtocol: 'none',
39
+ supports: { nativeTools: true, legacyFunctionCall: false, textToolCalls: true, parallelTools: false },
40
+ contextLength: 131072,
41
+ },
42
+ {
43
+ id: 'mistral',
44
+ match: /mistral|devstral|magistral/i,
45
+ toolProtocol: 'mistral',
46
+ reasoningProtocol: 'none',
47
+ supports: { nativeTools: true, legacyFunctionCall: false, textToolCalls: true, parallelTools: true },
48
+ contextLength: 32768,
49
+ },
50
+ {
51
+ id: 'gemma',
52
+ match: /gemma/i,
53
+ toolProtocol: 'openai',
54
+ reasoningProtocol: 'none',
55
+ supports: { nativeTools: true, legacyFunctionCall: false, textToolCalls: true, parallelTools: true },
56
+ contextLength: 131072,
57
+ },
58
+ {
59
+ id: 'phi4',
60
+ match: /phi[-_]?4/i,
61
+ toolProtocol: 'phi4',
62
+ reasoningProtocol: 'none',
63
+ supports: { nativeTools: true, legacyFunctionCall: false, textToolCalls: true, parallelTools: false },
64
+ contextLength: 131072,
65
+ },
66
+ ];
67
+
68
+ const DEFAULT_PROFILE = Object.freeze({
69
+ id: 'openai-compatible',
70
+ toolProtocol: 'openai',
71
+ reasoningProtocol: 'generic',
72
+ supports: { nativeTools: true, legacyFunctionCall: true, textToolCalls: true, parallelTools: true },
73
+ contextLength: null,
74
+ });
75
+
76
+ export function getModelCompatibility(provider, model) {
77
+ const matchText = `${provider || ''} ${model || ''}`;
78
+ const profile = MODEL_PROFILES.find(candidate => candidate.match.test(matchText));
79
+ return profile ? { ...profile, supports: { ...profile.supports } } : { ...DEFAULT_PROFILE, supports: { ...DEFAULT_PROFILE.supports } };
80
+ }
81
+
82
+ export function applyModelRequestOptions(payload, { provider, model, thinkingMode = null } = {}) {
83
+ const profile = getModelCompatibility(provider, model);
84
+ const next = { ...payload };
85
+ if (profile.id === 'qwen3' || profile.id === 'qwen3-coder') {
86
+ if (thinkingMode === 'show' || thinkingMode === 'hide') {
87
+ next.chat_template_kwargs = { enable_thinking: thinkingMode === 'show' };
88
+ }
89
+ }
90
+ return next;
91
+ }
92
+
93
+ function normalizeContent(content) {
94
+ if (Array.isArray(content)) {
95
+ return content.map(part => typeof part === 'string' ? part : part?.text || '').join('');
96
+ }
97
+ return content == null ? '' : String(content);
98
+ }
99
+
100
+ function parseArguments(argumentsValue) {
101
+ if (argumentsValue == null || argumentsValue === '') return '{}';
102
+ if (typeof argumentsValue === 'string') return argumentsValue;
103
+ try { return JSON.stringify(argumentsValue); } catch { return '{}'; }
104
+ }
105
+
106
+ function parsePythonArgs(argStr = '') {
107
+ const args = {};
108
+ if (!argStr || !argStr.trim()) return args;
109
+
110
+ const regex = /([a-zA-Z0-9_]+)\s*=\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\[[\s\S]*?\]|\{[\s\S]*?\}|true|false|null|-?\d+(?:\.\d+)?)/gi;
111
+ let m;
112
+ while ((m = regex.exec(argStr)) !== null) {
113
+ const key = m[1];
114
+ let valStr = m[2];
115
+ if ((valStr.startsWith('"') && valStr.endsWith('"')) || (valStr.startsWith("'") && valStr.endsWith("'"))) {
116
+ args[key] = valStr.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"');
117
+ } else if (valStr.toLowerCase() === 'true') {
118
+ args[key] = true;
119
+ } else if (valStr.toLowerCase() === 'false') {
120
+ args[key] = false;
121
+ } else if (valStr.toLowerCase() === 'null') {
122
+ args[key] = null;
123
+ } else if (!Number.isNaN(Number(valStr))) {
124
+ args[key] = Number(valStr);
125
+ } else {
126
+ try { args[key] = JSON.parse(valStr); } catch { args[key] = valStr; }
127
+ }
128
+ }
129
+ return args;
130
+ }
131
+
132
+ function extractTaggedCalls(content) {
133
+ const calls = [];
134
+ const patterns = [
135
+ /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/gi,
136
+ /\[TOOL_CALLS\]\s*([\s\S]*?)(?=\[\/TOOL_CALLS\]|$)/gi,
137
+ ];
138
+ for (const pattern of patterns) {
139
+ for (const match of content.matchAll(pattern)) {
140
+ let raw = match[1].trim();
141
+ raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
142
+ let parsed;
143
+ try { parsed = JSON.parse(raw); } catch { parsed = null; }
144
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
145
+ for (const entry of entries) {
146
+ if (!entry || typeof entry !== 'object') continue;
147
+ const fn = entry.function || entry;
148
+ const name = fn.name || entry.name || entry.tool;
149
+ if (!name) continue;
150
+ const args = fn.arguments ?? entry.arguments ?? entry.parameters ?? entry.args ?? {};
151
+ calls.push({
152
+ id: entry.id || `call_compat_${Date.now()}_${calls.length}`,
153
+ type: 'function',
154
+ function: { name: String(name), arguments: parseArguments(args) },
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+
161
+ const pythonTagPattern = /<\|python_tag\|>([a-zA-Z0-9_]+)\(([\s\S]*?)\)/gi;
162
+ for (const match of content.matchAll(pythonTagPattern)) {
163
+ const name = match[1];
164
+ const argStr = match[2];
165
+ calls.push({
166
+ id: `call_compat_${Date.now()}_${calls.length}`,
167
+ type: 'function',
168
+ function: { name: String(name), arguments: JSON.stringify(parsePythonArgs(argStr)) },
169
+ });
170
+ }
171
+
172
+ return calls;
173
+ }
174
+
175
+ export function normalizeAssistantMessage(message = {}) {
176
+ const normalized = { ...message, role: message.role || 'assistant' };
177
+ let content = normalizeContent(message.content);
178
+ let reasoning = message.reasoning_content || message.reasoning || message.thinking || '';
179
+
180
+ const thinkPattern = /<think>\s*([\s\S]*?)(?:<\/think>|$)/gi;
181
+ const thinkMatches = [...content.matchAll(thinkPattern)];
182
+ if (thinkMatches.length > 0) {
183
+ for (const match of thinkMatches) {
184
+ if (match[1]) {
185
+ reasoning = `${reasoning}${reasoning ? '\n' : ''}${match[1]}`.trim();
186
+ }
187
+ }
188
+ content = content.replace(thinkPattern, '').trim();
189
+ }
190
+ normalized.content = content;
191
+ if (reasoning) normalized.reasoning_content = String(reasoning);
192
+
193
+ let toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls.map((call, index) => ({
194
+ id: call.id || `call_compat_${Date.now()}_${index}`,
195
+ type: call.type || 'function',
196
+ function: {
197
+ name: call.function?.name || call.name || '',
198
+ arguments: parseArguments(call.function?.arguments ?? call.arguments),
199
+ },
200
+ })) : [];
201
+ if (toolCalls.length === 0 && message.function_call) {
202
+ toolCalls = [{
203
+ id: `call_compat_${Date.now()}_0`,
204
+ type: 'function',
205
+ function: {
206
+ name: message.function_call.name || '',
207
+ arguments: parseArguments(message.function_call.arguments),
208
+ },
209
+ }];
210
+ }
211
+ if (toolCalls.length === 0) toolCalls = extractTaggedCalls(content);
212
+ if (toolCalls.length > 0) {
213
+ normalized.tool_calls = toolCalls;
214
+ normalized.content = content
215
+ .replace(/<tool_call>\s*[\s\S]*?\s*<\/tool_call>/gi, '')
216
+ .replace(/\[TOOL_CALLS\][\s\S]*?(?:\[\/TOOL_CALLS\]|$)/gi, '')
217
+ .replace(/<\|python_tag\|>[a-zA-Z0-9_]+\([\s\S]*?\)/gi, '')
218
+ .trim();
219
+ }
220
+ return normalized;
221
+ }
222
+
223
+ export class StreamContentNormalizer {
224
+ constructor() {
225
+ this.buffer = '';
226
+ this.inThinking = false;
227
+ this.inToolTag = null;
228
+ this.toolCallBuffer = '';
229
+ }
230
+
231
+ push(chunk) {
232
+ this.buffer += String(chunk || '');
233
+ const answer = [];
234
+ const reasoning = [];
235
+
236
+ while (this.buffer) {
237
+ if (this.inThinking) {
238
+ const end = this.buffer.search(/<\/think>/i);
239
+ if (end < 0) {
240
+ reasoning.push(this.buffer);
241
+ this.buffer = '';
242
+ break;
243
+ }
244
+ reasoning.push(this.buffer.slice(0, end));
245
+ this.buffer = this.buffer.slice(end).replace(/^<\/think>/i, '');
246
+ this.inThinking = false;
247
+ continue;
248
+ }
249
+
250
+ if (this.inToolTag === 'tool_call') {
251
+ const end = this.buffer.search(/<\/tool_call>/i);
252
+ if (end < 0) {
253
+ this.toolCallBuffer += this.buffer;
254
+ this.buffer = '';
255
+ break;
256
+ }
257
+ this.toolCallBuffer += this.buffer.slice(0, end);
258
+ this.buffer = this.buffer.slice(end).replace(/^<\/tool_call>/i, '');
259
+ this.inToolTag = null;
260
+ continue;
261
+ }
262
+
263
+ if (this.inToolTag === 'tool_calls') {
264
+ const end = this.buffer.search(/\[\/TOOL_CALLS\]/i);
265
+ if (end < 0) {
266
+ this.toolCallBuffer += this.buffer;
267
+ this.buffer = '';
268
+ break;
269
+ }
270
+ this.toolCallBuffer += this.buffer.slice(0, end);
271
+ this.buffer = this.buffer.slice(end).replace(/^\[\/TOOL_CALLS\]/i, '');
272
+ this.inToolTag = null;
273
+ continue;
274
+ }
275
+
276
+ if (this.inToolTag === 'python_tag') {
277
+ const end = this.buffer.search(/\)/);
278
+ if (end < 0) {
279
+ this.toolCallBuffer += this.buffer;
280
+ this.buffer = '';
281
+ break;
282
+ }
283
+ this.toolCallBuffer += this.buffer.slice(0, end + 1);
284
+ this.buffer = this.buffer.slice(end + 1);
285
+ this.inToolTag = null;
286
+ continue;
287
+ }
288
+
289
+ const thinkIdx = this.buffer.search(/<think>/i);
290
+ const toolCallIdx = this.buffer.search(/<tool_call>/i);
291
+ const toolCallsIdx = this.buffer.search(/\[TOOL_CALLS\]/i);
292
+ const pythonTagIdx = this.buffer.search(/<\|python_tag\|>/i);
293
+
294
+ const indices = [
295
+ { type: 'think', idx: thinkIdx },
296
+ { type: 'tool_call', idx: toolCallIdx },
297
+ { type: 'tool_calls', idx: toolCallsIdx },
298
+ { type: 'python_tag', idx: pythonTagIdx },
299
+ ].filter(item => item.idx >= 0).sort((a, b) => a.idx - b.idx);
300
+
301
+ if (indices.length === 0) {
302
+ const suffix = this.buffer.match(/(?:<(?:t(?:h(?:i(?:n(?:k)?)?)?)?|t(?:o(?:o(?:l(?:_(?:c(?:a(?:l(?:l)?)?)?)?)?)?)?)?)?|\[(?:T(?:O(?:O(?:L(?:_(?:C(?:A(?:L(?:L(?:S)?)?)?)?)?)?)?)?)?)?|<\|(?:p(?:y(?:t(?:h(?:o(?:n(?:_(?:t(?:a(?:g)?)?)?)?)?)?)?)?)?)?)$/i)?.[0] || '';
303
+ if (suffix) {
304
+ answer.push(this.buffer.slice(0, -suffix.length));
305
+ this.buffer = suffix;
306
+ } else {
307
+ answer.push(this.buffer);
308
+ this.buffer = '';
309
+ }
310
+ break;
311
+ }
312
+
313
+ const next = indices[0];
314
+ answer.push(this.buffer.slice(0, next.idx));
315
+ if (next.type === 'think') {
316
+ this.buffer = this.buffer.slice(next.idx).replace(/^<think>/i, '');
317
+ this.inThinking = true;
318
+ } else if (next.type === 'tool_call') {
319
+ this.buffer = this.buffer.slice(next.idx).replace(/^<tool_call>/i, '');
320
+ this.inToolTag = 'tool_call';
321
+ } else if (next.type === 'tool_calls') {
322
+ this.buffer = this.buffer.slice(next.idx).replace(/^\[TOOL_CALLS\]/i, '');
323
+ this.inToolTag = 'tool_calls';
324
+ } else if (next.type === 'python_tag') {
325
+ this.buffer = this.buffer.slice(next.idx).replace(/^<\|python_tag\|>/i, '');
326
+ this.inToolTag = 'python_tag';
327
+ }
328
+ }
329
+
330
+ return { answer: answer.join(''), reasoning: reasoning.join('') };
331
+ }
332
+
333
+ flush() {
334
+ let reasoning = '';
335
+ let answer = '';
336
+ if (this.inThinking) {
337
+ reasoning = this.buffer;
338
+ } else if (this.inToolTag) {
339
+ this.toolCallBuffer += this.buffer;
340
+ } else {
341
+ answer = this.buffer;
342
+ }
343
+ const toolCallBuffer = this.toolCallBuffer;
344
+ this.buffer = '';
345
+ this.toolCallBuffer = '';
346
+ return {
347
+ answer,
348
+ reasoning,
349
+ ...(toolCallBuffer ? { toolCallBuffer } : {}),
350
+ };
351
+ }
352
+ }
@@ -0,0 +1 @@
1
+ export const KIKU_DEFAULT_MODEL = 'google/gemma-4-e4b';
@@ -7,6 +7,7 @@ export class ModelManager {
7
7
  if (!client) throw new Error('ModelManager requires an LLM client.');
8
8
  this.client = client;
9
9
  this.requestedModel = options.requestedModel || client.model;
10
+ this.strictModel = options.strictModel === true;
10
11
  this.fallbackModel = options.fallbackModel || null;
11
12
  this.fallbackModels = Array.isArray(options.fallbackModels) ? options.fallbackModels : [];
12
13
  this.lastResolution = null;
@@ -67,7 +68,30 @@ export class ModelManager {
67
68
  }
68
69
 
69
70
  async resolve(options = {}) {
70
- const health = await this.client.checkConnection();
71
+ let health = await this.client.checkConnection();
72
+ const isLocalLmStudio = this.client.provider === 'lmstudio' &&
73
+ typeof this.client.baseUrl === 'string' &&
74
+ (this.client.baseUrl.includes('127.0.0.1:1234') || this.client.baseUrl.includes('localhost:1234') || this.client.baseUrl.includes(':1234'));
75
+
76
+ if ((!health.connected || (health.models || []).length === 0) && options.autoWakeup && isLocalLmStudio) {
77
+ try {
78
+ const { wakeUpLmStudio } = await import('./lmStudioWakeup.js');
79
+ const { getPersistedModelLoadSettings } = await import('./config.js');
80
+ const targetModel = options.requestedModel || this.requestedModel || this.client.model || 'google/gemma-4-e4b';
81
+ const loadSettings = getPersistedModelLoadSettings();
82
+ const wakeRes = await wakeUpLmStudio({
83
+ baseUrl: this.client.baseUrl,
84
+ model: targetModel,
85
+ quiet: options.quiet ?? false,
86
+ gpuOffload: options.gpuOffload !== undefined ? options.gpuOffload : loadSettings.gpuOffload,
87
+ cpuThreads: options.cpuThreads !== undefined ? options.cpuThreads : loadSettings.cpuThreads,
88
+ });
89
+ if (wakeRes.ok) {
90
+ health = await this.client.checkConnection();
91
+ }
92
+ } catch {}
93
+ }
94
+
71
95
  if (!health.connected) {
72
96
  return this._record({ state: 'server_offline', connected: false, models: [], error: health.error });
73
97
  }
@@ -112,15 +136,35 @@ export class ModelManager {
112
136
  : models.find(id => !NON_CHAT_MODEL.test(id));
113
137
 
114
138
  const selected = exact || fuzzyRequested || (activeLoaded && matchModelIdentifier(requested, activeLoaded) ? activeLoaded : null) || configuredFallback || recommendedFallback || activeLoaded || gemma || chatFallback;
139
+ if (this.strictModel && !exact && !fuzzyRequested) {
140
+ return this._record({
141
+ state: 'requested_model_missing',
142
+ connected: true,
143
+ models,
144
+ requestedModel: requested,
145
+ selectedModel: null,
146
+ isModelAvailable: false,
147
+ usedFallback: false,
148
+ error: `Requested model is not available: ${requested}`
149
+ });
150
+ }
115
151
  if (!selected) {
116
152
  return this._record({ state: 'no_chat_model', connected: true, models, error: 'No chat-capable model was found.' });
117
153
  }
118
154
 
119
155
  this.client.model = selected;
156
+ const isLocalEngine = this.client.provider === 'lmstudio' || this.client.provider === 'vllm';
157
+ const isLoadedInVram = isLocalEngine ? Boolean(activeLoaded) : true;
158
+
120
159
  return this._record({
121
- state: (exact || fuzzyRequested) ? 'ready' : 'fallback_selected',
160
+ state: !isLoadedInVram && isLocalEngine
161
+ ? 'server_online_model_not_loaded'
162
+ : ((exact || fuzzyRequested) ? 'ready' : 'fallback_selected'),
122
163
  connected: true,
123
164
  models,
165
+ loadedModels: health.loadedModels || (activeLoaded ? [activeLoaded] : []),
166
+ activeLoadedModel: activeLoaded || null,
167
+ isModelLoaded: isLoadedInVram,
124
168
  selectedModel: selected,
125
169
  requestedModel: requested,
126
170
  usedFallback: !(exact || (fuzzyRequested && matchModelIdentifier(requested, selected))),
@@ -62,11 +62,17 @@ export async function checkNewsWatch({ topic, days = 7, fetcher, state = null, s
62
62
 
63
63
  export function startNewsWatchLoop({ topic, days = 7, intervalMinutes = 30, fetcher, controller, state = null, statePath = WATCH_STATE_PATH, onDelta, onError } = {}) {
64
64
  const sleep = ms => new Promise(resolve => {
65
- const timer = setTimeout(resolve, ms);
66
- controller.signal.addEventListener('abort', () => {
65
+ let timer;
66
+ const onAbort = () => {
67
67
  clearTimeout(timer);
68
+ controller.signal.removeEventListener('abort', onAbort);
68
69
  resolve();
69
- }, { once: true });
70
+ };
71
+ timer = setTimeout(() => {
72
+ controller.signal.removeEventListener('abort', onAbort);
73
+ resolve();
74
+ }, ms);
75
+ controller.signal.addEventListener('abort', onAbort, { once: true });
70
76
  });
71
77
 
72
78
  return (async () => {
@@ -0,0 +1,229 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ const _activeProcesses = new Map();
4
+ let _exitHooksInstalled = false;
5
+
6
+ export function isPidAlive(pid) {
7
+ if (!pid || typeof pid !== 'number' || pid <= 0) return false;
8
+ try {
9
+ process.kill(pid, 0);
10
+ return true;
11
+ } catch (err) {
12
+ return err.code === 'EPERM';
13
+ }
14
+ }
15
+
16
+ export function killProcessTreeSync(pid, signal = 'SIGTERM') {
17
+ if (!pid || typeof pid !== 'number' || pid <= 0) return false;
18
+ if (process.platform === 'win32') {
19
+ try {
20
+ const res = spawnSync('taskkill', ['/pid', String(pid), '/f', '/t'], {
21
+ stdio: 'ignore',
22
+ windowsHide: true,
23
+ timeout: 5000,
24
+ });
25
+ return res.status === 0 || !isPidAlive(pid);
26
+ } catch {
27
+ try {
28
+ process.kill(pid, 'SIGKILL');
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+ }
35
+
36
+ try {
37
+ process.kill(-pid, signal);
38
+ return true;
39
+ } catch {
40
+ try {
41
+ process.kill(pid, signal);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ }
48
+
49
+ export async function killProcessTree(pid, signal = 'SIGTERM') {
50
+ return killProcessTreeSync(pid, signal);
51
+ }
52
+
53
+ export function killWslProcessByName(distro, pattern) {
54
+ if (!distro || !pattern || process.platform !== 'win32') return false;
55
+ try {
56
+ const res = spawnSync('wsl', ['-d', String(distro), 'pkill', '-f', String(pattern)], {
57
+ stdio: 'ignore',
58
+ windowsHide: true,
59
+ timeout: 5000,
60
+ });
61
+ return res.status === 0;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ export function registerBackgroundProcess(name, childOrPid, options = {}) {
68
+ if (!name || typeof name !== 'string') {
69
+ throw new Error('Process name must be a non-empty string');
70
+ }
71
+
72
+ ensureProcessExitHooks();
73
+
74
+ const isChildObj = childOrPid && typeof childOrPid === 'object' && typeof childOrPid.pid === 'number';
75
+ const pid = isChildObj ? childOrPid.pid : (typeof childOrPid === 'number' ? childOrPid : null);
76
+
77
+ if (!pid || pid <= 0) {
78
+ throw new Error(`Invalid PID for background process "${name}"`);
79
+ }
80
+
81
+ const existing = _activeProcesses.get(name);
82
+ if (existing && existing.status === 'running' && isPidAlive(existing.pid) && existing.pid !== pid) {
83
+ killProcessTreeSync(existing.pid);
84
+ }
85
+
86
+ const persistOnExit = Boolean(options.persistOnExit || options.metadata?.persistOnExit);
87
+
88
+ const record = {
89
+ id: `${name}-${pid}-${Date.now()}`,
90
+ name,
91
+ pid,
92
+ command: options.command || (isChildObj ? childOrPid.spawnfile : null) || 'unknown',
93
+ args: options.args || (isChildObj ? childOrPid.spawnargs : []) || [],
94
+ startedAt: options.startedAt || Date.now(),
95
+ status: 'running',
96
+ exitCode: null,
97
+ exitSignal: null,
98
+ persistOnExit,
99
+ metadata: { ...options.metadata, persistOnExit },
100
+ childProcess: isChildObj ? childOrPid : null,
101
+ };
102
+
103
+ if (isChildObj) {
104
+ childOrPid.once('exit', (code, signal) => {
105
+ record.status = code === 0 ? 'stopped' : 'exited';
106
+ record.exitCode = code;
107
+ record.exitSignal = signal;
108
+ });
109
+ childOrPid.once('error', (err) => {
110
+ record.status = 'failed';
111
+ record.metadata.lastError = err?.message || String(err);
112
+ });
113
+ }
114
+
115
+ _activeProcesses.set(name, record);
116
+ return record;
117
+ }
118
+
119
+ export function unregisterBackgroundProcess(name) {
120
+ return _activeProcesses.delete(name);
121
+ }
122
+
123
+ export function getBackgroundProcess(name) {
124
+ const rec = _activeProcesses.get(name);
125
+ if (!rec) return null;
126
+ const alive = isPidAlive(rec.pid);
127
+ if (!alive && rec.status === 'running') {
128
+ rec.status = 'stopped';
129
+ }
130
+ return {
131
+ ...rec,
132
+ isAlive: alive,
133
+ uptimeSeconds: Math.max(0, Math.floor((Date.now() - rec.startedAt) / 1000)),
134
+ };
135
+ }
136
+
137
+ export function listBackgroundProcesses() {
138
+ const results = [];
139
+ for (const [name, rec] of _activeProcesses.entries()) {
140
+ const alive = isPidAlive(rec.pid);
141
+ if (!alive && rec.status === 'running') {
142
+ rec.status = 'stopped';
143
+ }
144
+ results.push({
145
+ name,
146
+ pid: rec.pid,
147
+ status: rec.status,
148
+ isAlive: alive,
149
+ command: rec.command,
150
+ startedAt: rec.startedAt,
151
+ uptimeSeconds: Math.max(0, Math.floor((Date.now() - rec.startedAt) / 1000)),
152
+ metadata: { ...rec.metadata },
153
+ });
154
+ }
155
+ return results;
156
+ }
157
+
158
+ export function stopBackgroundProcess(name, options = {}) {
159
+ const rec = _activeProcesses.get(name);
160
+ if (!rec) {
161
+ return { ok: false, reason: `Process "${name}" not found in registry` };
162
+ }
163
+
164
+ const pid = rec.pid;
165
+ const signal = options.signal || 'SIGTERM';
166
+ let killed = false;
167
+
168
+ if (isPidAlive(pid)) {
169
+ killed = killProcessTreeSync(pid, signal);
170
+ } else {
171
+ killed = true;
172
+ }
173
+
174
+ if (rec.metadata?.isWsl && rec.metadata?.distro && rec.metadata?.wslProcessPattern) {
175
+ killWslProcessByName(rec.metadata.distro, rec.metadata.wslProcessPattern);
176
+ }
177
+
178
+ rec.status = 'stopped';
179
+ rec.exitSignal = signal;
180
+
181
+ return {
182
+ ok: killed,
183
+ name,
184
+ pid,
185
+ status: 'stopped',
186
+ };
187
+ }
188
+
189
+ export function stopAllBackgroundProcessesSync(options = {}) {
190
+ const force = Boolean(options && options.force);
191
+ const stopped = [];
192
+ for (const [name, rec] of _activeProcesses.entries()) {
193
+ if (!force && (rec.persistOnExit || rec.metadata?.persistOnExit)) {
194
+ continue;
195
+ }
196
+ if (rec.status === 'running' || isPidAlive(rec.pid)) {
197
+ killProcessTreeSync(rec.pid);
198
+ if (rec.metadata?.isWsl && rec.metadata?.distro && rec.metadata?.wslProcessPattern) {
199
+ killWslProcessByName(rec.metadata.distro, rec.metadata.wslProcessPattern);
200
+ }
201
+ rec.status = 'stopped';
202
+ stopped.push({ name, pid: rec.pid });
203
+ }
204
+ }
205
+ return stopped;
206
+ }
207
+
208
+ export function stopAllBackgroundProcesses(options = {}) {
209
+ return stopAllBackgroundProcessesSync(options);
210
+ }
211
+
212
+ export function clearProcessRegistry() {
213
+ _activeProcesses.clear();
214
+ }
215
+
216
+ export function ensureProcessExitHooks() {
217
+ if (_exitHooksInstalled) return;
218
+ _exitHooksInstalled = true;
219
+
220
+ const cleanupSync = () => {
221
+ stopAllBackgroundProcessesSync({ force: false });
222
+ };
223
+
224
+ process.once('exit', cleanupSync);
225
+ }
226
+
227
+ export function initProcessExitHooks() {
228
+ ensureProcessExitHooks();
229
+ }