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 +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/services/ai/engine.js +41 -14
- package/server/services/ai/outputSanitizer.js +67 -0
- package/server/services/ai/providers/anthropic.js +130 -1
- package/server/services/ai/toolCallSalvage.js +26 -1
- package/server/services/android/controller.js +34 -1
package/package.json
CHANGED
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"052f31d115eceda8cbff1b3481fcde4330c4ae
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
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
|
|
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', {
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
440
|
+
const retryOptions = { ...callOptions, model, reasoningEffort: this.getReasoningEffort(providerName, options) };
|
|
429
441
|
|
|
430
442
|
if (options.stream !== false) {
|
|
431
|
-
const gen =
|
|
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', {
|
|
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
|
|
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
|
-
|
|
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 === '
|
|
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.#
|
|
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,
|