neoagent 2.1.10 → 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/README.md +3 -0
- package/docs/configuration.md +1 -1
- package/package.json +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +53915 -53242
- package/server/routes/android.js +87 -0
- package/server/services/ai/engine.js +52 -14
- package/server/services/ai/outputSanitizer.js +67 -0
- package/server/services/ai/providers/anthropic.js +130 -1
- package/server/services/ai/systemPrompt.js +1 -0
- package/server/services/ai/toolCallSalvage.js +142 -0
- package/server/services/ai/toolResult.js +10 -0
- package/server/services/ai/tools.js +43 -0
- package/server/services/android/controller.js +161 -2
package/server/routes/android.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const multer = require('multer');
|
|
2
5
|
const router = express.Router();
|
|
6
|
+
const { DATA_DIR } = require('../../runtime/paths');
|
|
3
7
|
const { requireAuth } = require('../middleware/auth');
|
|
4
8
|
const { sanitizeError } = require('../utils/security');
|
|
5
9
|
|
|
6
10
|
router.use(requireAuth);
|
|
7
11
|
|
|
12
|
+
const androidApkUploadDir = path.join(DATA_DIR, 'uploads', 'android-apks');
|
|
13
|
+
fs.mkdirSync(androidApkUploadDir, { recursive: true });
|
|
14
|
+
|
|
15
|
+
const androidApkUpload = multer({
|
|
16
|
+
storage: multer.diskStorage({
|
|
17
|
+
destination: (_req, _file, cb) => cb(null, androidApkUploadDir),
|
|
18
|
+
filename: (_req, file, cb) => {
|
|
19
|
+
const extension = path.extname(file.originalname || '').toLowerCase();
|
|
20
|
+
const stem = path.basename(file.originalname || 'upload', extension)
|
|
21
|
+
.replace(/[^a-z0-9._-]+/gi, '-')
|
|
22
|
+
.replace(/^-+|-+$/g, '')
|
|
23
|
+
.slice(0, 64) || 'upload';
|
|
24
|
+
cb(
|
|
25
|
+
null,
|
|
26
|
+
`${Date.now()}-${Math.random().toString(16).slice(2)}-${stem}${extension || '.apk'}`
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
fileFilter: (_req, file, cb) => {
|
|
31
|
+
if (!String(file.originalname || '').toLowerCase().endsWith('.apk')) {
|
|
32
|
+
cb(new Error('Only .apk files can be installed.'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
cb(null, true);
|
|
36
|
+
},
|
|
37
|
+
limits: {
|
|
38
|
+
fileSize: 512 * 1024 * 1024,
|
|
39
|
+
files: 1,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
8
43
|
router.get('/status', async (req, res) => {
|
|
9
44
|
try {
|
|
10
45
|
const controller = req.app.locals.androidController;
|
|
@@ -97,6 +132,15 @@ router.post('/tap', async (req, res) => {
|
|
|
97
132
|
}
|
|
98
133
|
});
|
|
99
134
|
|
|
135
|
+
router.post('/long-press', async (req, res) => {
|
|
136
|
+
try {
|
|
137
|
+
const controller = req.app.locals.androidController;
|
|
138
|
+
res.json(await controller.longPress(req.body || {}));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
100
144
|
router.post('/type', async (req, res) => {
|
|
101
145
|
try {
|
|
102
146
|
const controller = req.app.locals.androidController;
|
|
@@ -133,4 +177,47 @@ router.post('/wait-for', async (req, res) => {
|
|
|
133
177
|
}
|
|
134
178
|
});
|
|
135
179
|
|
|
180
|
+
router.post('/install-apk', (req, res) => {
|
|
181
|
+
androidApkUpload.single('apk')(req, res, async (uploadError) => {
|
|
182
|
+
if (uploadError) {
|
|
183
|
+
const message =
|
|
184
|
+
uploadError instanceof multer.MulterError &&
|
|
185
|
+
uploadError.code === 'LIMIT_FILE_SIZE'
|
|
186
|
+
? 'APK upload is too large. Limit is 512MB.'
|
|
187
|
+
: sanitizeError(uploadError);
|
|
188
|
+
res.status(400).json({ error: message });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const uploadedApkPath = req.file?.path;
|
|
193
|
+
if (!uploadedApkPath) {
|
|
194
|
+
res.status(400).json({ error: 'No APK file was uploaded.' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const controller = req.app.locals.androidController;
|
|
200
|
+
const result = await controller.installApk({ apkPath: uploadedApkPath });
|
|
201
|
+
res.json({
|
|
202
|
+
...result,
|
|
203
|
+
filename: req.file.originalname,
|
|
204
|
+
size: req.file.size,
|
|
205
|
+
});
|
|
206
|
+
} catch (err) {
|
|
207
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
208
|
+
} finally {
|
|
209
|
+
fs.promises.unlink(uploadedApkPath).catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
router.post('/shell', async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const controller = req.app.locals.androidController;
|
|
217
|
+
res.json(await controller.shell(req.body || {}));
|
|
218
|
+
} catch (err) {
|
|
219
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
136
223
|
module.exports = router;
|
|
@@ -6,6 +6,8 @@ const { getConversationContext, buildSummaryCarrier, refreshConversationSummary
|
|
|
6
6
|
const { ensureDefaultAiSettings, getAiSettings } = require('./settings');
|
|
7
7
|
const { selectToolsForTask } = require('./toolSelector');
|
|
8
8
|
const { compactToolResult } = require('./toolResult');
|
|
9
|
+
const { salvageTextToolCalls } = require('./toolCallSalvage');
|
|
10
|
+
const { sanitizeModelOutput } = require('./outputSanitizer');
|
|
9
11
|
|
|
10
12
|
function generateTitle(task) {
|
|
11
13
|
if (!task || typeof task !== 'string') return 'Untitled';
|
|
@@ -299,13 +301,16 @@ class AgentEngine {
|
|
|
299
301
|
});
|
|
300
302
|
}
|
|
301
303
|
};
|
|
302
|
-
const
|
|
304
|
+
const selectedProvider = await getProviderForUser(
|
|
303
305
|
userId,
|
|
304
306
|
userMessage,
|
|
305
307
|
triggerType === 'subagent',
|
|
306
308
|
_modelOverride,
|
|
307
309
|
providerStatusConfig
|
|
308
310
|
);
|
|
311
|
+
let provider = selectedProvider.provider;
|
|
312
|
+
let model = selectedProvider.model;
|
|
313
|
+
let providerName = selectedProvider.providerName;
|
|
309
314
|
|
|
310
315
|
const runTitle = generateTitle(userMessage);
|
|
311
316
|
db.prepare(`INSERT OR REPLACE INTO agent_runs(id, user_id, title, status, trigger_type, trigger_source, model)
|
|
@@ -380,6 +385,7 @@ class AgentEngine {
|
|
|
380
385
|
this.emit(userId, 'run:thinking', { runId, iteration });
|
|
381
386
|
|
|
382
387
|
let response;
|
|
388
|
+
let responseModel = model;
|
|
383
389
|
let streamContent = '';
|
|
384
390
|
const callOptions = { model, reasoningEffort: this.getReasoningEffort(providerName, options) };
|
|
385
391
|
|
|
@@ -390,22 +396,30 @@ class AgentEngine {
|
|
|
390
396
|
for await (const chunk of gen) {
|
|
391
397
|
if (chunk.type === 'content') {
|
|
392
398
|
streamContent += chunk.content;
|
|
393
|
-
this.emit(userId, 'run:stream', {
|
|
399
|
+
this.emit(userId, 'run:stream', {
|
|
400
|
+
runId,
|
|
401
|
+
content: sanitizeModelOutput(streamContent, { model }),
|
|
402
|
+
iteration
|
|
403
|
+
});
|
|
394
404
|
}
|
|
395
405
|
if (chunk.type === 'done') {
|
|
396
406
|
response = chunk;
|
|
407
|
+
responseModel = model;
|
|
397
408
|
}
|
|
398
409
|
if (chunk.type === 'tool_calls') {
|
|
399
410
|
response = {
|
|
400
411
|
content: chunk.content || streamContent,
|
|
401
412
|
toolCalls: chunk.toolCalls,
|
|
413
|
+
providerContentBlocks: chunk.providerContentBlocks || null,
|
|
402
414
|
finishReason: 'tool_calls',
|
|
403
415
|
usage: chunk.usage || null
|
|
404
416
|
};
|
|
417
|
+
responseModel = model;
|
|
405
418
|
}
|
|
406
419
|
}
|
|
407
420
|
} else {
|
|
408
421
|
response = await provider.chat(messages, tools, callOptions);
|
|
422
|
+
responseModel = model;
|
|
409
423
|
}
|
|
410
424
|
} catch (err) {
|
|
411
425
|
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
@@ -418,33 +432,42 @@ class AgentEngine {
|
|
|
418
432
|
aiSettings.fallback_model_id,
|
|
419
433
|
providerStatusConfig
|
|
420
434
|
);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const nextProviderName = fallback.providerName;
|
|
435
|
+
provider = fallback.provider;
|
|
436
|
+
model = fallback.model;
|
|
437
|
+
providerName = fallback.providerName;
|
|
425
438
|
|
|
426
439
|
// Recursive call once
|
|
427
|
-
const retryOptions = { ...callOptions, model
|
|
440
|
+
const retryOptions = { ...callOptions, model, reasoningEffort: this.getReasoningEffort(providerName, options) };
|
|
428
441
|
|
|
429
442
|
if (options.stream !== false) {
|
|
430
|
-
const gen =
|
|
443
|
+
const gen = provider.stream(messages, tools, retryOptions);
|
|
431
444
|
for await (const chunk of gen) {
|
|
432
445
|
if (chunk.type === 'content') {
|
|
433
446
|
streamContent += chunk.content;
|
|
434
|
-
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;
|
|
435
456
|
}
|
|
436
|
-
if (chunk.type === 'done') response = chunk;
|
|
437
457
|
if (chunk.type === 'tool_calls') {
|
|
438
458
|
response = {
|
|
439
459
|
content: chunk.content || streamContent,
|
|
440
460
|
toolCalls: chunk.toolCalls,
|
|
461
|
+
providerContentBlocks: chunk.providerContentBlocks || null,
|
|
441
462
|
finishReason: 'tool_calls',
|
|
442
463
|
usage: chunk.usage || null
|
|
443
464
|
};
|
|
465
|
+
responseModel = model;
|
|
444
466
|
}
|
|
445
467
|
}
|
|
446
468
|
} else {
|
|
447
|
-
response = await
|
|
469
|
+
response = await provider.chat(messages, tools, retryOptions);
|
|
470
|
+
responseModel = model;
|
|
448
471
|
}
|
|
449
472
|
} else {
|
|
450
473
|
throw err;
|
|
@@ -462,10 +485,21 @@ class AgentEngine {
|
|
|
462
485
|
totalTokens += response.usage.totalTokens || 0;
|
|
463
486
|
}
|
|
464
487
|
|
|
465
|
-
lastContent = response.content || streamContent || '';
|
|
488
|
+
lastContent = sanitizeModelOutput(response.content || streamContent || '', { model: responseModel });
|
|
489
|
+
|
|
490
|
+
if ((!response.toolCalls || response.toolCalls.length === 0) && lastContent) {
|
|
491
|
+
const salvaged = salvageTextToolCalls(lastContent, tools);
|
|
492
|
+
if (salvaged.toolCalls.length > 0) {
|
|
493
|
+
response.toolCalls = salvaged.toolCalls;
|
|
494
|
+
response.finishReason = 'tool_calls';
|
|
495
|
+
response.content = salvaged.content;
|
|
496
|
+
lastContent = salvaged.content;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
466
499
|
|
|
467
500
|
const assistantMessage = { role: 'assistant', content: lastContent };
|
|
468
501
|
if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
|
|
502
|
+
if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
|
|
469
503
|
messages.push(assistantMessage);
|
|
470
504
|
|
|
471
505
|
if (conversationId) {
|
|
@@ -572,10 +606,14 @@ class AgentEngine {
|
|
|
572
606
|
model,
|
|
573
607
|
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
574
608
|
});
|
|
575
|
-
lastContent = finalResponse.content || '';
|
|
609
|
+
lastContent = sanitizeModelOutput(finalResponse.content || '', { model });
|
|
576
610
|
forcedFinalResponse = true;
|
|
577
611
|
|
|
578
|
-
|
|
612
|
+
const finalAssistantMessage = { role: 'assistant', content: lastContent };
|
|
613
|
+
if (finalResponse.providerContentBlocks?.length) {
|
|
614
|
+
finalAssistantMessage.providerContentBlocks = finalResponse.providerContentBlocks;
|
|
615
|
+
}
|
|
616
|
+
messages.push(finalAssistantMessage);
|
|
579
617
|
if (conversationId) {
|
|
580
618
|
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
581
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
|
};
|
|
@@ -56,6 +56,7 @@ The tools listed in this call are exactly what you have. Trust the list. If a to
|
|
|
56
56
|
|
|
57
57
|
SHELL COMMANDS
|
|
58
58
|
When you use execute_command, treat timed out or killed commands as unfinished work, not success. For installs, updates, restarts, config changes, or other state-changing shell actions, verify the outcome with a follow-up command before telling the user it is done.
|
|
59
|
+
If you restart or stop the NeoAgent service, this run ends immediately. Warn the user before doing it and say you cannot continue the current run after the restart.
|
|
59
60
|
|
|
60
61
|
SKILLS
|
|
61
62
|
If a multi-step task produces a reusable pattern, save or improve it as a skill when appropriate.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid')
|
|
2
|
+
|
|
3
|
+
const INVOKE_OPEN_RE = /(?:[A-Za-z0-9_.-]+:tool_call\s*)?<invoke\s+name="([^"]+)">/g
|
|
4
|
+
const PARAM_OPEN_RE = /<parameter\s+name="([^"]+)">/g
|
|
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
|
|
8
|
+
const INVOKE_CLOSE = '</invoke>'
|
|
9
|
+
|
|
10
|
+
function trimLooseControlText(text) {
|
|
11
|
+
return String(text || '')
|
|
12
|
+
.replace(TOOL_WRAPPER_RE, '')
|
|
13
|
+
.replace(/(?:^|\s)[A-Za-z0-9_.-]+:tool_call\s*$/g, '')
|
|
14
|
+
.trim()
|
|
15
|
+
}
|
|
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
|
+
|
|
38
|
+
function parseParameterMap(body) {
|
|
39
|
+
const args = {}
|
|
40
|
+
let sawClosedParam = false
|
|
41
|
+
let match
|
|
42
|
+
|
|
43
|
+
PARAM_CLOSED_RE.lastIndex = 0
|
|
44
|
+
while ((match = PARAM_CLOSED_RE.exec(body)) !== null) {
|
|
45
|
+
sawClosedParam = true
|
|
46
|
+
args[match[1]] = match[2].trim()
|
|
47
|
+
}
|
|
48
|
+
if (sawClosedParam) return args
|
|
49
|
+
|
|
50
|
+
const markers = []
|
|
51
|
+
PARAM_OPEN_RE.lastIndex = 0
|
|
52
|
+
while ((match = PARAM_OPEN_RE.exec(body)) !== null) {
|
|
53
|
+
markers.push({
|
|
54
|
+
name: match[1],
|
|
55
|
+
valueStart: match.index + match[0].length,
|
|
56
|
+
openIndex: match.index
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < markers.length; i++) {
|
|
61
|
+
const current = markers[i]
|
|
62
|
+
const next = markers[i + 1]
|
|
63
|
+
const rawValue = body.slice(current.valueStart, next ? next.openIndex : body.length)
|
|
64
|
+
args[current.name] = rawValue.trim()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return args
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function salvageTextToolCalls(content, tools = []) {
|
|
71
|
+
const text = String(content || '')
|
|
72
|
+
if (!text.includes('<invoke')) {
|
|
73
|
+
return { content: text, toolCalls: [] }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const allowedToolNames = new Set(
|
|
77
|
+
Array.isArray(tools) ? tools.map((tool) => tool?.name).filter(Boolean) : []
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const openings = []
|
|
81
|
+
let match
|
|
82
|
+
INVOKE_OPEN_RE.lastIndex = 0
|
|
83
|
+
while ((match = INVOKE_OPEN_RE.exec(text)) !== null) {
|
|
84
|
+
openings.push({
|
|
85
|
+
index: match.index,
|
|
86
|
+
openText: match[0],
|
|
87
|
+
name: match[1]
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (openings.length === 0) {
|
|
92
|
+
return { content: text, toolCalls: [] }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cleanedParts = []
|
|
96
|
+
const toolCalls = []
|
|
97
|
+
let cursor = 0
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < openings.length; i++) {
|
|
100
|
+
const current = openings[i]
|
|
101
|
+
const next = openings[i + 1]
|
|
102
|
+
const searchEnd = next ? next.index : text.length
|
|
103
|
+
const bodyStart = current.index + current.openText.length
|
|
104
|
+
const closeIndex = text.indexOf(INVOKE_CLOSE, bodyStart)
|
|
105
|
+
const blockEnd = closeIndex !== -1 && closeIndex < searchEnd
|
|
106
|
+
? closeIndex + INVOKE_CLOSE.length
|
|
107
|
+
: searchEnd
|
|
108
|
+
const bodyEnd = closeIndex !== -1 && closeIndex < searchEnd
|
|
109
|
+
? closeIndex
|
|
110
|
+
: searchEnd
|
|
111
|
+
|
|
112
|
+
cleanedParts.push(text.slice(cursor, current.index))
|
|
113
|
+
cursor = blockEnd
|
|
114
|
+
|
|
115
|
+
if (allowedToolNames.size > 0 && !allowedToolNames.has(current.name)) {
|
|
116
|
+
cleanedParts.push(text.slice(current.index, blockEnd))
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const args = parseParameterMap(text.slice(bodyStart, bodyEnd))
|
|
121
|
+
toolCalls.push({
|
|
122
|
+
id: `salvaged_${uuidv4()}`,
|
|
123
|
+
type: 'function',
|
|
124
|
+
function: {
|
|
125
|
+
name: current.name,
|
|
126
|
+
arguments: JSON.stringify(args)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cleanedParts.push(text.slice(cursor))
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
content: trimLooseControlText(cleanedParts.join('').replace(/\n{3,}/g, '\n\n')),
|
|
135
|
+
toolCalls
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
salvageTextToolCalls,
|
|
141
|
+
sanitizeStreamingToolCallText
|
|
142
|
+
}
|
|
@@ -108,6 +108,16 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
|
|
|
108
108
|
});
|
|
109
109
|
break;
|
|
110
110
|
|
|
111
|
+
case 'android_shell':
|
|
112
|
+
envelope = trimObject({
|
|
113
|
+
tool: toolName,
|
|
114
|
+
serial: toolResult?.serial,
|
|
115
|
+
command: toolArgs.command,
|
|
116
|
+
screenshotPath: toolResult?.screenshotPath,
|
|
117
|
+
excerpt: lineExcerpt(toolResult?.stdout || toolResult?.result || toolResult, 18, Math.floor(softLimit * 0.65))
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
|
|
111
121
|
case 'http_request':
|
|
112
122
|
envelope = trimObject({
|
|
113
123
|
tool: toolName,
|