neoagent 2.1.11 → 2.1.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.1.11",
3
+ "version": "2.1.12",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"052f31d115eceda8cbff1b3481fcde4330c4ae
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "1464466977" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "2032032802" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -7,6 +7,7 @@ const { ensureDefaultAiSettings, getAiSettings } = require('./settings');
7
7
  const { selectToolsForTask } = require('./toolSelector');
8
8
  const { compactToolResult } = require('./toolResult');
9
9
  const { salvageTextToolCalls } = require('./toolCallSalvage');
10
+ const { sanitizeModelOutput } = require('./outputSanitizer');
10
11
 
11
12
  function generateTitle(task) {
12
13
  if (!task || typeof task !== 'string') return 'Untitled';
@@ -300,13 +301,16 @@ class AgentEngine {
300
301
  });
301
302
  }
302
303
  };
303
- const { provider, model, providerName } = await getProviderForUser(
304
+ const selectedProvider = await getProviderForUser(
304
305
  userId,
305
306
  userMessage,
306
307
  triggerType === 'subagent',
307
308
  _modelOverride,
308
309
  providerStatusConfig
309
310
  );
311
+ let provider = selectedProvider.provider;
312
+ let model = selectedProvider.model;
313
+ let providerName = selectedProvider.providerName;
310
314
 
311
315
  const runTitle = generateTitle(userMessage);
312
316
  db.prepare(`INSERT OR REPLACE INTO agent_runs(id, user_id, title, status, trigger_type, trigger_source, model)
@@ -381,6 +385,7 @@ class AgentEngine {
381
385
  this.emit(userId, 'run:thinking', { runId, iteration });
382
386
 
383
387
  let response;
388
+ let responseModel = model;
384
389
  let streamContent = '';
385
390
  const callOptions = { model, reasoningEffort: this.getReasoningEffort(providerName, options) };
386
391
 
@@ -391,22 +396,30 @@ class AgentEngine {
391
396
  for await (const chunk of gen) {
392
397
  if (chunk.type === 'content') {
393
398
  streamContent += chunk.content;
394
- this.emit(userId, 'run:stream', { runId, content: streamContent, iteration });
399
+ this.emit(userId, 'run:stream', {
400
+ runId,
401
+ content: sanitizeModelOutput(streamContent, { model }),
402
+ iteration
403
+ });
395
404
  }
396
405
  if (chunk.type === 'done') {
397
406
  response = chunk;
407
+ responseModel = model;
398
408
  }
399
409
  if (chunk.type === 'tool_calls') {
400
410
  response = {
401
411
  content: chunk.content || streamContent,
402
412
  toolCalls: chunk.toolCalls,
413
+ providerContentBlocks: chunk.providerContentBlocks || null,
403
414
  finishReason: 'tool_calls',
404
415
  usage: chunk.usage || null
405
416
  };
417
+ responseModel = model;
406
418
  }
407
419
  }
408
420
  } else {
409
421
  response = await provider.chat(messages, tools, callOptions);
422
+ responseModel = model;
410
423
  }
411
424
  } catch (err) {
412
425
  console.error(`[Engine] Model call failed (${model}):`, err.message);
@@ -419,33 +432,42 @@ class AgentEngine {
419
432
  aiSettings.fallback_model_id,
420
433
  providerStatusConfig
421
434
  );
422
- // Update local state for the retry
423
- const nextProvider = fallback.provider;
424
- const nextModel = fallback.model;
425
- const nextProviderName = fallback.providerName;
435
+ provider = fallback.provider;
436
+ model = fallback.model;
437
+ providerName = fallback.providerName;
426
438
 
427
439
  // Recursive call once
428
- const retryOptions = { ...callOptions, model: nextModel, reasoningEffort: this.getReasoningEffort(nextProviderName, options) };
440
+ const retryOptions = { ...callOptions, model, reasoningEffort: this.getReasoningEffort(providerName, options) };
429
441
 
430
442
  if (options.stream !== false) {
431
- const gen = nextProvider.stream(messages, tools, retryOptions);
443
+ const gen = provider.stream(messages, tools, retryOptions);
432
444
  for await (const chunk of gen) {
433
445
  if (chunk.type === 'content') {
434
446
  streamContent += chunk.content;
435
- this.emit(userId, 'run:stream', { runId, content: streamContent, iteration });
447
+ this.emit(userId, 'run:stream', {
448
+ runId,
449
+ content: sanitizeModelOutput(streamContent, { model }),
450
+ iteration
451
+ });
452
+ }
453
+ if (chunk.type === 'done') {
454
+ response = chunk;
455
+ responseModel = model;
436
456
  }
437
- if (chunk.type === 'done') response = chunk;
438
457
  if (chunk.type === 'tool_calls') {
439
458
  response = {
440
459
  content: chunk.content || streamContent,
441
460
  toolCalls: chunk.toolCalls,
461
+ providerContentBlocks: chunk.providerContentBlocks || null,
442
462
  finishReason: 'tool_calls',
443
463
  usage: chunk.usage || null
444
464
  };
465
+ responseModel = model;
445
466
  }
446
467
  }
447
468
  } else {
448
- response = await nextProvider.chat(messages, tools, retryOptions);
469
+ response = await provider.chat(messages, tools, retryOptions);
470
+ responseModel = model;
449
471
  }
450
472
  } else {
451
473
  throw err;
@@ -463,7 +485,7 @@ class AgentEngine {
463
485
  totalTokens += response.usage.totalTokens || 0;
464
486
  }
465
487
 
466
- lastContent = response.content || streamContent || '';
488
+ lastContent = sanitizeModelOutput(response.content || streamContent || '', { model: responseModel });
467
489
 
468
490
  if ((!response.toolCalls || response.toolCalls.length === 0) && lastContent) {
469
491
  const salvaged = salvageTextToolCalls(lastContent, tools);
@@ -477,6 +499,7 @@ class AgentEngine {
477
499
 
478
500
  const assistantMessage = { role: 'assistant', content: lastContent };
479
501
  if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
502
+ if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
480
503
  messages.push(assistantMessage);
481
504
 
482
505
  if (conversationId) {
@@ -583,10 +606,14 @@ class AgentEngine {
583
606
  model,
584
607
  reasoningEffort: this.getReasoningEffort(providerName, options)
585
608
  });
586
- lastContent = finalResponse.content || '';
609
+ lastContent = sanitizeModelOutput(finalResponse.content || '', { model });
587
610
  forcedFinalResponse = true;
588
611
 
589
- messages.push({ role: 'assistant', content: lastContent });
612
+ const finalAssistantMessage = { role: 'assistant', content: lastContent };
613
+ if (finalResponse.providerContentBlocks?.length) {
614
+ finalAssistantMessage.providerContentBlocks = finalResponse.providerContentBlocks;
615
+ }
616
+ messages.push(finalAssistantMessage);
590
617
  if (conversationId) {
591
618
  db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
592
619
  .run(conversationId, 'assistant', lastContent, finalResponse.usage?.totalTokens || 0);
@@ -0,0 +1,67 @@
1
+ const { sanitizeStreamingToolCallText } = require('./toolCallSalvage');
2
+
3
+ const HAN_CHAR_REGEX = /\p{Script=Han}/gu;
4
+ const LATIN_CHAR_REGEX = /\p{Script=Latin}/gu;
5
+ const LETTER_CHAR_REGEX = /\p{L}/gu;
6
+ const HAN_RUN_REGEX = /[\p{Script=Han}\u3000-\u303F]+/gu;
7
+ const MARKDOWN_CODE_SPAN_REGEX = /(```[\s\S]*?```|`[^`\n]+`)/g;
8
+
9
+ function countMatches(text, regex) {
10
+ const matches = text.match(regex);
11
+ return matches ? matches.length : 0;
12
+ }
13
+
14
+ function shouldStripIncidentalHan(text, model) {
15
+ if (model !== 'MiniMax-M2.7') return false;
16
+
17
+ const hanCount = countMatches(text, HAN_CHAR_REGEX);
18
+ if (hanCount === 0) return false;
19
+ if (hanCount > 24) return false;
20
+
21
+ const latinCount = countMatches(text, LATIN_CHAR_REGEX);
22
+ if (latinCount < 20) return false;
23
+
24
+ const letterCount = countMatches(text, LETTER_CHAR_REGEX);
25
+ if (letterCount > 0 && (hanCount / letterCount) > 0.18) return false;
26
+
27
+ return true;
28
+ }
29
+
30
+ function sanitizePlainText(text) {
31
+ return text
32
+ .replace(/([\p{L}\p{N}])[\p{Script=Han}\u3000-\u303F]+([\p{L}\p{N}])/gu, '$1 $2')
33
+ .replace(HAN_RUN_REGEX, '')
34
+ .replace(/[ \t]{2,}/g, ' ')
35
+ .replace(/[ \t]+\n/g, '\n')
36
+ .replace(/\n[ \t]+/g, '\n')
37
+ .replace(/[ \t]+([,.;:!?)\]}])/g, '$1')
38
+ .replace(/([([{])\s+/g, '$1');
39
+ }
40
+
41
+ function sanitizeMarkdownAware(text) {
42
+ return text
43
+ .split(MARKDOWN_CODE_SPAN_REGEX)
44
+ .map((part) => {
45
+ if (!part) return part;
46
+ if (part.startsWith('```') || part.startsWith('`')) return part;
47
+ return sanitizePlainText(part);
48
+ })
49
+ .join('');
50
+ }
51
+
52
+ function sanitizeModelOutput(text, options = {}) {
53
+ if (typeof text !== 'string' || text.length === 0) return text;
54
+
55
+ let sanitized = text;
56
+
57
+ if (options.model === 'MiniMax-M2.7' && (sanitized.includes('<invoke') || sanitized.includes(':tool_call'))) {
58
+ sanitized = sanitizeStreamingToolCallText(sanitized);
59
+ }
60
+
61
+ if (!shouldStripIncidentalHan(sanitized, options.model)) return sanitized;
62
+ return sanitizeMarkdownAware(sanitized);
63
+ }
64
+
65
+ module.exports = {
66
+ sanitizeModelOutput
67
+ };
@@ -37,6 +37,50 @@ class AnthropicProvider extends BaseProvider {
37
37
  }));
38
38
  }
39
39
 
40
+ normalizeContentBlocks(blocks = []) {
41
+ const normalized = [];
42
+
43
+ for (const block of blocks) {
44
+ if (!block || !block.type) continue;
45
+
46
+ if (block.type === 'thinking') {
47
+ normalized.push({
48
+ type: 'thinking',
49
+ thinking: block.thinking || '',
50
+ ...(block.signature ? { signature: block.signature } : {})
51
+ });
52
+ continue;
53
+ }
54
+
55
+ if (block.type === 'redacted_thinking') {
56
+ normalized.push({
57
+ type: 'redacted_thinking',
58
+ data: block.data
59
+ });
60
+ continue;
61
+ }
62
+
63
+ if (block.type === 'text') {
64
+ normalized.push({
65
+ type: 'text',
66
+ text: block.text || ''
67
+ });
68
+ continue;
69
+ }
70
+
71
+ if (block.type === 'tool_use') {
72
+ normalized.push({
73
+ type: 'tool_use',
74
+ id: block.id,
75
+ name: block.name,
76
+ input: block.input || {}
77
+ });
78
+ }
79
+ }
80
+
81
+ return normalized;
82
+ }
83
+
40
84
  convertMessages(messages) {
41
85
  let system = '';
42
86
  const converted = [];
@@ -60,6 +104,14 @@ class AnthropicProvider extends BaseProvider {
60
104
  }
61
105
 
62
106
  if (msg.role === 'assistant' && msg.tool_calls) {
107
+ if (Array.isArray(msg.providerContentBlocks) && msg.providerContentBlocks.length > 0) {
108
+ converted.push({
109
+ role: 'assistant',
110
+ content: this.normalizeContentBlocks(msg.providerContentBlocks)
111
+ });
112
+ continue;
113
+ }
114
+
63
115
  const content = [];
64
116
  if (msg.content) content.push({ type: 'text', text: msg.content });
65
117
  for (const tc of msg.tool_calls) {
@@ -100,6 +152,7 @@ class AnthropicProvider extends BaseProvider {
100
152
 
101
153
  let content = '';
102
154
  const toolCalls = [];
155
+ const providerContentBlocks = this.normalizeContentBlocks(response.content);
103
156
 
104
157
  for (const block of response.content) {
105
158
  if (block.type === 'text') {
@@ -119,6 +172,7 @@ class AnthropicProvider extends BaseProvider {
119
172
  return {
120
173
  content,
121
174
  toolCalls,
175
+ providerContentBlocks,
122
176
  finishReason: response.stop_reason === 'tool_use' ? 'tool_calls' : 'stop',
123
177
  usage: {
124
178
  promptTokens: response.usage.input_tokens,
@@ -148,31 +202,106 @@ class AnthropicProvider extends BaseProvider {
148
202
  let content = '';
149
203
  let currentToolCalls = [];
150
204
  let currentToolIndex = -1;
205
+ const providerContentBlocks = [];
151
206
 
152
207
  for await (const event of stream) {
153
208
  if (event.type === 'content_block_start') {
154
- if (event.content_block.type === 'tool_use') {
209
+ if (event.content_block.type === 'thinking') {
210
+ providerContentBlocks[event.index] = {
211
+ type: 'thinking',
212
+ thinking: event.content_block.thinking || '',
213
+ signature: event.content_block.signature || ''
214
+ };
215
+ } else if (event.content_block.type === 'redacted_thinking') {
216
+ providerContentBlocks[event.index] = {
217
+ type: 'redacted_thinking',
218
+ data: event.content_block.data
219
+ };
220
+ } else if (event.content_block.type === 'text') {
221
+ providerContentBlocks[event.index] = {
222
+ type: 'text',
223
+ text: event.content_block.text || ''
224
+ };
225
+ } else if (event.content_block.type === 'tool_use') {
155
226
  currentToolIndex++;
156
227
  currentToolCalls.push({
157
228
  id: event.content_block.id,
158
229
  type: 'function',
159
230
  function: { name: event.content_block.name, arguments: '' }
160
231
  });
232
+ providerContentBlocks[event.index] = {
233
+ type: 'tool_use',
234
+ id: event.content_block.id,
235
+ name: event.content_block.name,
236
+ input: {}
237
+ };
161
238
  }
162
239
  } else if (event.type === 'content_block_delta') {
163
240
  if (event.delta.type === 'text_delta') {
164
241
  content += event.delta.text;
242
+ if (providerContentBlocks[event.index]?.type === 'text') {
243
+ providerContentBlocks[event.index].text += event.delta.text;
244
+ }
165
245
  yield { type: 'content', content: event.delta.text };
246
+ } else if (event.delta.type === 'thinking_delta') {
247
+ if (providerContentBlocks[event.index]?.type === 'thinking') {
248
+ providerContentBlocks[event.index].thinking += event.delta.thinking || '';
249
+ }
250
+ } else if (event.delta.type === 'signature_delta') {
251
+ if (providerContentBlocks[event.index]?.type === 'thinking') {
252
+ providerContentBlocks[event.index].signature = event.delta.signature || '';
253
+ }
166
254
  } else if (event.delta.type === 'input_json_delta') {
167
255
  if (currentToolCalls[currentToolIndex]) {
168
256
  currentToolCalls[currentToolIndex].function.arguments += event.delta.partial_json;
169
257
  }
258
+ if (providerContentBlocks[event.index]?.type === 'tool_use') {
259
+ const currentJson = providerContentBlocks[event.index]._inputJson || '';
260
+ providerContentBlocks[event.index]._inputJson = currentJson + (event.delta.partial_json || '');
261
+ }
170
262
  }
171
263
  } else if (event.type === 'message_stop') {
264
+ const normalizedBlocks = providerContentBlocks
265
+ .filter(Boolean)
266
+ .map((block) => {
267
+ if (block.type === 'tool_use') {
268
+ let parsedInput = block.input || {};
269
+ if (typeof block._inputJson === 'string' && block._inputJson.trim()) {
270
+ try {
271
+ parsedInput = JSON.parse(block._inputJson);
272
+ } catch { }
273
+ }
274
+ return {
275
+ type: 'tool_use',
276
+ id: block.id,
277
+ name: block.name,
278
+ input: parsedInput
279
+ };
280
+ }
281
+ if (block.type === 'thinking') {
282
+ return {
283
+ type: 'thinking',
284
+ thinking: block.thinking || '',
285
+ ...(block.signature ? { signature: block.signature } : {})
286
+ };
287
+ }
288
+ if (block.type === 'redacted_thinking') {
289
+ return {
290
+ type: 'redacted_thinking',
291
+ data: block.data
292
+ };
293
+ }
294
+ return {
295
+ type: 'text',
296
+ text: block.text || ''
297
+ };
298
+ });
299
+
172
300
  yield {
173
301
  type: 'done',
174
302
  content,
175
303
  toolCalls: currentToolCalls,
304
+ providerContentBlocks: normalizedBlocks,
176
305
  finishReason: currentToolCalls.length > 0 ? 'tool_calls' : 'stop',
177
306
  usage: null
178
307
  };
@@ -3,14 +3,38 @@ const { v4: uuidv4 } = require('uuid')
3
3
  const INVOKE_OPEN_RE = /(?:[A-Za-z0-9_.-]+:tool_call\s*)?<invoke\s+name="([^"]+)">/g
4
4
  const PARAM_OPEN_RE = /<parameter\s+name="([^"]+)">/g
5
5
  const PARAM_CLOSED_RE = /<parameter\s+name="([^"]+)">([\s\S]*?)<\/parameter>/g
6
+ const TOOL_WRAPPER_RE = /<\/?[A-Za-z0-9_.-]+:tool_call>/g
7
+ const COMPLETE_INLINE_CALL_RE = /(?:[A-Za-z0-9_.-]+:tool_call\s*)?<invoke\s+name="[^"]+">[\s\S]*?<\/invoke>/g
6
8
  const INVOKE_CLOSE = '</invoke>'
7
9
 
8
10
  function trimLooseControlText(text) {
9
11
  return String(text || '')
12
+ .replace(TOOL_WRAPPER_RE, '')
10
13
  .replace(/(?:^|\s)[A-Za-z0-9_.-]+:tool_call\s*$/g, '')
11
14
  .trim()
12
15
  }
13
16
 
17
+ function sanitizeStreamingToolCallText(text) {
18
+ let visible = String(text || '')
19
+ .replace(COMPLETE_INLINE_CALL_RE, '')
20
+ .replace(TOOL_WRAPPER_RE, '')
21
+
22
+ const partialStarts = [
23
+ visible.lastIndexOf('<invoke'),
24
+ visible.lastIndexOf(':tool_call')
25
+ ].filter((index) => index >= 0)
26
+
27
+ if (partialStarts.length > 0) {
28
+ const partialStart = Math.max(...partialStarts)
29
+ const suffix = visible.slice(partialStart)
30
+ if (!suffix.includes(INVOKE_CLOSE)) {
31
+ visible = visible.slice(0, partialStart)
32
+ }
33
+ }
34
+
35
+ return trimLooseControlText(visible).replace(/\n{3,}/g, '\n\n')
36
+ }
37
+
14
38
  function parseParameterMap(body) {
15
39
  const args = {}
16
40
  let sawClosedParam = false
@@ -113,5 +137,6 @@ function salvageTextToolCalls(content, tools = []) {
113
137
  }
114
138
 
115
139
  module.exports = {
116
- salvageTextToolCalls
140
+ salvageTextToolCalls,
141
+ sanitizeStreamingToolCallText
117
142
  }
@@ -64,6 +64,25 @@ function commandExists(command) {
64
64
  return probe.status === 0;
65
65
  }
66
66
 
67
+ function parseResolvedLaunchComponent(output, packageName) {
68
+ const lines = String(output || '')
69
+ .split('\n')
70
+ .map((line) => line.trim())
71
+ .filter(Boolean);
72
+ const normalizedPackage = String(packageName || '').trim();
73
+ const componentPattern = /^[A-Za-z0-9._$]+\/[A-Za-z0-9._$]+$/;
74
+ const relativePattern = /^[A-Za-z0-9._$]+\/\.[A-Za-z0-9._$]+$/;
75
+
76
+ const exact = lines.find((line) =>
77
+ normalizedPackage
78
+ ? line.startsWith(`${normalizedPackage}/`)
79
+ : componentPattern.test(line) || relativePattern.test(line)
80
+ );
81
+ if (exact) return exact;
82
+
83
+ return lines.find((line) => componentPattern.test(line) || relativePattern.test(line)) || null;
84
+ }
85
+
67
86
  function appendState(patch) {
68
87
  const current = readState();
69
88
  const next = {
@@ -1305,7 +1324,20 @@ class AndroidController {
1305
1324
  if (args.activity) {
1306
1325
  await this.#adb(serial, `shell am start -n ${quoteShell(`${args.packageName}/${args.activity}`)}`, { timeout: 20000 });
1307
1326
  } else if (args.packageName) {
1308
- await this.#adb(serial, `shell monkey -p ${quoteShell(args.packageName)} -c android.intent.category.LAUNCHER 1`, { timeout: 30000 });
1327
+ const resolved = await this.#runAllowFailure(
1328
+ `${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell cmd package resolve-activity --brief -c android.intent.category.LAUNCHER ${quoteShell(args.packageName)}`,
1329
+ { timeout: 15000 },
1330
+ );
1331
+ const component = parseResolvedLaunchComponent(
1332
+ `${resolved.stdout || ''}\n${resolved.stderr || ''}`,
1333
+ args.packageName,
1334
+ );
1335
+
1336
+ if (component) {
1337
+ await this.#adb(serial, `shell am start -n ${quoteShell(component)}`, { timeout: 20000 });
1338
+ } else {
1339
+ await this.#adb(serial, `shell monkey -p ${quoteShell(args.packageName)} -c android.intent.category.LAUNCHER 1`, { timeout: 30000 });
1340
+ }
1309
1341
  } else {
1310
1342
  throw new Error('packageName is required for android_open_app');
1311
1343
  }
@@ -1458,6 +1490,7 @@ module.exports = {
1458
1490
  configuredSystemImagePackage,
1459
1491
  configuredSystemImagePlatform,
1460
1492
  formatSystemImageError,
1493
+ parseResolvedLaunchComponent,
1461
1494
  parseLatestCmdlineToolsUrl,
1462
1495
  parseSystemImages,
1463
1496
  sanitizeUiXml,