neoagent 2.1.13 → 2.1.14
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/public/main.dart.js +19820 -19800
- package/server/routes/android.js +6 -4
- package/server/services/ai/engine.js +16 -6
- package/server/services/ai/history.js +63 -1
- package/server/services/ai/tools.js +2 -2
- package/server/services/android/controller.js +93 -17
package/server/routes/android.js
CHANGED
|
@@ -11,6 +11,7 @@ router.use(requireAuth);
|
|
|
11
11
|
|
|
12
12
|
const androidApkUploadDir = path.join(DATA_DIR, 'uploads', 'android-apks');
|
|
13
13
|
fs.mkdirSync(androidApkUploadDir, { recursive: true });
|
|
14
|
+
const INSTALLABLE_ANDROID_PACKAGE_EXTENSIONS = new Set(['.apk', '.apks']);
|
|
14
15
|
|
|
15
16
|
const androidApkUpload = multer({
|
|
16
17
|
storage: multer.diskStorage({
|
|
@@ -28,8 +29,9 @@ const androidApkUpload = multer({
|
|
|
28
29
|
},
|
|
29
30
|
}),
|
|
30
31
|
fileFilter: (_req, file, cb) => {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const extension = path.extname(String(file.originalname || '')).toLowerCase();
|
|
33
|
+
if (!INSTALLABLE_ANDROID_PACKAGE_EXTENSIONS.has(extension)) {
|
|
34
|
+
cb(new Error('Only .apk or .apks files can be installed.'));
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
35
37
|
cb(null, true);
|
|
@@ -192,7 +194,7 @@ router.post('/install-apk', (req, res) => {
|
|
|
192
194
|
const message =
|
|
193
195
|
uploadError instanceof multer.MulterError &&
|
|
194
196
|
uploadError.code === 'LIMIT_FILE_SIZE'
|
|
195
|
-
? '
|
|
197
|
+
? 'Android app upload is too large. Limit is 512MB.'
|
|
196
198
|
: sanitizeError(uploadError);
|
|
197
199
|
res.status(400).json({ error: message });
|
|
198
200
|
return;
|
|
@@ -200,7 +202,7 @@ router.post('/install-apk', (req, res) => {
|
|
|
200
202
|
|
|
201
203
|
const uploadedApkPath = req.file?.path;
|
|
202
204
|
if (!uploadedApkPath) {
|
|
203
|
-
res.status(400).json({ error: 'No APK
|
|
205
|
+
res.status(400).json({ error: 'No APK or APK bundle was uploaded.' });
|
|
204
206
|
return;
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -2,7 +2,12 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const db = require('../../db/database');
|
|
4
4
|
const { compact } = require('./compaction');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
getConversationContext,
|
|
7
|
+
buildSummaryCarrier,
|
|
8
|
+
refreshConversationSummary,
|
|
9
|
+
sanitizeConversationMessages
|
|
10
|
+
} = require('./history');
|
|
6
11
|
const { ensureDefaultAiSettings, getAiSettings } = require('./settings');
|
|
7
12
|
const { selectToolsForTask } = require('./toolSelector');
|
|
8
13
|
const { compactToolResult } = require('./toolResult');
|
|
@@ -438,6 +443,7 @@ class AgentEngine {
|
|
|
438
443
|
|
|
439
444
|
let messages = this.buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg);
|
|
440
445
|
messages.push(this.buildUserMessage(userMessage, options));
|
|
446
|
+
messages = sanitizeConversationMessages(messages);
|
|
441
447
|
|
|
442
448
|
if (conversationId) {
|
|
443
449
|
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
|
|
@@ -461,11 +467,13 @@ class AgentEngine {
|
|
|
461
467
|
conversationId
|
|
462
468
|
});
|
|
463
469
|
messages = steeringAtLoopStart.messages;
|
|
470
|
+
messages = sanitizeConversationMessages(messages);
|
|
464
471
|
|
|
465
472
|
let metrics = this.estimatePromptMetrics(messages, tools);
|
|
466
473
|
const contextWindow = provider.getContextWindow(model);
|
|
467
474
|
if (metrics.totalEstimatedTokens > contextWindow * 0.7) {
|
|
468
475
|
messages = await compact(messages, provider, model);
|
|
476
|
+
messages = sanitizeConversationMessages(messages);
|
|
469
477
|
this.emit(userId, 'run:compaction', { runId, iteration });
|
|
470
478
|
metrics = this.estimatePromptMetrics(messages, tools);
|
|
471
479
|
}
|
|
@@ -480,9 +488,10 @@ class AgentEngine {
|
|
|
480
488
|
const callOptions = { model, reasoningEffort: this.getReasoningEffort(providerName, options) };
|
|
481
489
|
|
|
482
490
|
const tryModelCall = async (retryForFallback = true) => {
|
|
491
|
+
const requestMessages = sanitizeConversationMessages(messages);
|
|
483
492
|
try {
|
|
484
493
|
if (options.stream !== false) {
|
|
485
|
-
const gen = provider.stream(
|
|
494
|
+
const gen = provider.stream(requestMessages, tools, callOptions);
|
|
486
495
|
for await (const chunk of gen) {
|
|
487
496
|
if (chunk.type === 'content') {
|
|
488
497
|
streamContent += chunk.content;
|
|
@@ -508,7 +517,7 @@ class AgentEngine {
|
|
|
508
517
|
}
|
|
509
518
|
}
|
|
510
519
|
} else {
|
|
511
|
-
response = await provider.chat(
|
|
520
|
+
response = await provider.chat(requestMessages, tools, callOptions);
|
|
512
521
|
responseModel = model;
|
|
513
522
|
}
|
|
514
523
|
} catch (err) {
|
|
@@ -528,9 +537,10 @@ class AgentEngine {
|
|
|
528
537
|
|
|
529
538
|
// Recursive call once
|
|
530
539
|
const retryOptions = { ...callOptions, model, reasoningEffort: this.getReasoningEffort(providerName, options) };
|
|
540
|
+
const retryMessages = sanitizeConversationMessages(messages);
|
|
531
541
|
|
|
532
542
|
if (options.stream !== false) {
|
|
533
|
-
const gen = provider.stream(
|
|
543
|
+
const gen = provider.stream(retryMessages, tools, retryOptions);
|
|
534
544
|
for await (const chunk of gen) {
|
|
535
545
|
if (chunk.type === 'content') {
|
|
536
546
|
streamContent += chunk.content;
|
|
@@ -556,7 +566,7 @@ class AgentEngine {
|
|
|
556
566
|
}
|
|
557
567
|
}
|
|
558
568
|
} else {
|
|
559
|
-
response = await provider.chat(
|
|
569
|
+
response = await provider.chat(retryMessages, tools, retryOptions);
|
|
560
570
|
responseModel = model;
|
|
561
571
|
}
|
|
562
572
|
} else {
|
|
@@ -704,7 +714,7 @@ class AgentEngine {
|
|
|
704
714
|
|
|
705
715
|
if ((iteration >= maxIterations && messages[messages.length - 1]?.role === 'tool')
|
|
706
716
|
|| (iteration < maxIterations && stepIndex > 0 && !lastContent.trim() && messages[messages.length - 1]?.role !== 'tool')) {
|
|
707
|
-
const finalResponse = await provider.chat(messages, [], {
|
|
717
|
+
const finalResponse = await provider.chat(sanitizeConversationMessages(messages), [], {
|
|
708
718
|
model,
|
|
709
719
|
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
710
720
|
});
|
|
@@ -34,6 +34,67 @@ function normalizeHistoryRows(rows) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function sanitizeConversationMessages(messages) {
|
|
38
|
+
const sanitized = [];
|
|
39
|
+
let pendingToolSequence = null;
|
|
40
|
+
|
|
41
|
+
const dropPendingSequence = () => {
|
|
42
|
+
pendingToolSequence = null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const flushPendingSequence = () => {
|
|
46
|
+
if (!pendingToolSequence) return;
|
|
47
|
+
if (pendingToolSequence.pendingIds.size === 0) {
|
|
48
|
+
sanitized.push(...pendingToolSequence.messages);
|
|
49
|
+
}
|
|
50
|
+
pendingToolSequence = null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (const msg of messages || []) {
|
|
54
|
+
if (!msg || !msg.role) continue;
|
|
55
|
+
|
|
56
|
+
if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
57
|
+
const toolCallIds = msg.tool_calls
|
|
58
|
+
.map((toolCall) => toolCall?.id)
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
|
|
61
|
+
if (toolCallIds.length === 0) {
|
|
62
|
+
dropPendingSequence();
|
|
63
|
+
sanitized.push(msg);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
dropPendingSequence();
|
|
68
|
+
pendingToolSequence = {
|
|
69
|
+
messages: [msg],
|
|
70
|
+
pendingIds: new Set(toolCallIds)
|
|
71
|
+
};
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (msg.role === 'tool') {
|
|
76
|
+
if (
|
|
77
|
+
pendingToolSequence
|
|
78
|
+
&& msg.tool_call_id
|
|
79
|
+
&& pendingToolSequence.pendingIds.has(msg.tool_call_id)
|
|
80
|
+
) {
|
|
81
|
+
pendingToolSequence.messages.push(msg);
|
|
82
|
+
pendingToolSequence.pendingIds.delete(msg.tool_call_id);
|
|
83
|
+
if (pendingToolSequence.pendingIds.size === 0) {
|
|
84
|
+
flushPendingSequence();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
dropPendingSequence();
|
|
91
|
+
sanitized.push(msg);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
flushPendingSequence();
|
|
95
|
+
return sanitized;
|
|
96
|
+
}
|
|
97
|
+
|
|
37
98
|
function serializeHistoryForSummary(messages) {
|
|
38
99
|
return messages.map((msg) => {
|
|
39
100
|
if (msg.role === 'tool') {
|
|
@@ -143,7 +204,7 @@ function getConversationContext(conversationId, recentLimit) {
|
|
|
143
204
|
return {
|
|
144
205
|
summary: convo?.summary || '',
|
|
145
206
|
summaryCount: Number(convo?.summary_message_count || 0),
|
|
146
|
-
recentMessages: normalizeHistoryRows(recent),
|
|
207
|
+
recentMessages: sanitizeConversationMessages(normalizeHistoryRows(recent)),
|
|
147
208
|
totalMessages: db.prepare('SELECT COUNT(*) AS count FROM conversation_messages WHERE conversation_id = ?').get(conversationId).count
|
|
148
209
|
};
|
|
149
210
|
}
|
|
@@ -184,5 +245,6 @@ module.exports = {
|
|
|
184
245
|
getWebChatContext,
|
|
185
246
|
refreshConversationSummary,
|
|
186
247
|
refreshWebChatSummary,
|
|
248
|
+
sanitizeConversationMessages,
|
|
187
249
|
summarizeMessages
|
|
188
250
|
};
|
|
@@ -332,11 +332,11 @@ function getAvailableTools(app, options = {}) {
|
|
|
332
332
|
},
|
|
333
333
|
{
|
|
334
334
|
name: 'android_install_apk',
|
|
335
|
-
description: 'Install or replace an APK on the Android emulator.',
|
|
335
|
+
description: 'Install or replace an APK or universal .apks bundle on the Android emulator.',
|
|
336
336
|
parameters: {
|
|
337
337
|
type: 'object',
|
|
338
338
|
properties: {
|
|
339
|
-
apkPath: { type: 'string', description: 'Absolute path to
|
|
339
|
+
apkPath: { type: 'string', description: 'Absolute path to an .apk file or universal .apks bundle on disk' }
|
|
340
340
|
},
|
|
341
341
|
required: ['apkPath']
|
|
342
342
|
}
|
|
@@ -276,6 +276,49 @@ function extractZip(zipPath, destDir) {
|
|
|
276
276
|
throw new Error('Neither unzip nor ditto is available to extract Android SDK archives');
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
function listFilesRecursive(rootDir, predicate, bucket = []) {
|
|
280
|
+
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
281
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
282
|
+
if (entry.isDirectory()) {
|
|
283
|
+
listFilesRecursive(fullPath, predicate, bucket);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!predicate || predicate(fullPath, entry)) {
|
|
287
|
+
bucket.push(fullPath);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return bucket;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveBundleInstallTargets(bundleDir) {
|
|
294
|
+
const apkFiles = listFilesRecursive(bundleDir, (filePath) => path.extname(filePath).toLowerCase() === '.apk')
|
|
295
|
+
.sort((a, b) => a.localeCompare(b));
|
|
296
|
+
if (apkFiles.length === 0) {
|
|
297
|
+
throw new Error('APK bundle did not contain any installable .apk files.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const universalApk = apkFiles.find((filePath) => path.basename(filePath).toLowerCase() === 'universal.apk');
|
|
301
|
+
if (universalApk) {
|
|
302
|
+
return {
|
|
303
|
+
mode: 'single',
|
|
304
|
+
installPaths: [universalApk],
|
|
305
|
+
layout: 'universal',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (apkFiles.length === 1) {
|
|
310
|
+
return {
|
|
311
|
+
mode: 'single',
|
|
312
|
+
installPaths: apkFiles,
|
|
313
|
+
layout: 'single-apk',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
throw new Error(
|
|
318
|
+
'APK bundles must include a universal APK. Export a universal .apks bundle or upload a single .apk instead.'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
279
322
|
function parseLatestCmdlineToolsUrl(xml) {
|
|
280
323
|
const tag = platformTag() === 'mac' ? 'macosx' : 'linux';
|
|
281
324
|
const packageMatch = xml.match(new RegExp(`<remotePackage\\s+path="cmdline-tools;latest">([\\s\\S]*?)<\\/remotePackage>`));
|
|
@@ -1162,16 +1205,18 @@ class AndroidController {
|
|
|
1162
1205
|
}
|
|
1163
1206
|
}
|
|
1164
1207
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1208
|
+
if (options.uiDump !== false) {
|
|
1209
|
+
try {
|
|
1210
|
+
const dump = await this.dumpUi({
|
|
1211
|
+
serial: resolvedSerial,
|
|
1212
|
+
includeNodes: options.includeNodes !== false,
|
|
1213
|
+
});
|
|
1214
|
+
observation.uiDumpPath = dump.uiDumpPath;
|
|
1215
|
+
observation.nodeCount = dump.nodeCount;
|
|
1216
|
+
observation.preview = dump.preview;
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
observation.observationWarnings.push(`ui_dump: ${err.message}`);
|
|
1219
|
+
}
|
|
1175
1220
|
}
|
|
1176
1221
|
|
|
1177
1222
|
if (observation.observationWarnings.length === 0) {
|
|
@@ -1233,7 +1278,7 @@ class AndroidController {
|
|
|
1233
1278
|
}
|
|
1234
1279
|
|
|
1235
1280
|
await this.#adb(serial, `shell input tap ${Math.round(x)} ${Math.round(y)}`, { timeout: 15000 });
|
|
1236
|
-
const observation = await this.#captureObservation(serial);
|
|
1281
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1237
1282
|
return {
|
|
1238
1283
|
success: true,
|
|
1239
1284
|
serial,
|
|
@@ -1267,7 +1312,7 @@ class AndroidController {
|
|
|
1267
1312
|
`shell input swipe ${Math.round(x)} ${Math.round(y)} ${Math.round(x)} ${Math.round(y)} ${Math.round(durationMs)}`,
|
|
1268
1313
|
{ timeout: Math.max(15000, durationMs + 5000) },
|
|
1269
1314
|
);
|
|
1270
|
-
const observation = await this.#captureObservation(serial);
|
|
1315
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1271
1316
|
return {
|
|
1272
1317
|
success: true,
|
|
1273
1318
|
serial,
|
|
@@ -1294,6 +1339,8 @@ class AndroidController {
|
|
|
1294
1339
|
description: args.description,
|
|
1295
1340
|
className: args.className,
|
|
1296
1341
|
clickable: true,
|
|
1342
|
+
screenshot: false,
|
|
1343
|
+
uiDump: false,
|
|
1297
1344
|
}).catch(() => {});
|
|
1298
1345
|
}
|
|
1299
1346
|
|
|
@@ -1301,7 +1348,7 @@ class AndroidController {
|
|
|
1301
1348
|
if (args.pressEnter) {
|
|
1302
1349
|
await this.#adb(serial, 'shell input keyevent 66', { timeout: 10000 });
|
|
1303
1350
|
}
|
|
1304
|
-
const observation = await this.#captureObservation(serial);
|
|
1351
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1305
1352
|
return {
|
|
1306
1353
|
success: true,
|
|
1307
1354
|
serial,
|
|
@@ -1321,7 +1368,7 @@ class AndroidController {
|
|
|
1321
1368
|
throw new Error('x1, y1, x2, and y2 are required for android_swipe');
|
|
1322
1369
|
}
|
|
1323
1370
|
await this.#adb(serial, `shell input swipe ${Math.round(x1)} ${Math.round(y1)} ${Math.round(x2)} ${Math.round(y2)} ${Math.round(duration)}`, { timeout: 15000 });
|
|
1324
|
-
const observation = await this.#captureObservation(serial);
|
|
1371
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1325
1372
|
return {
|
|
1326
1373
|
success: true,
|
|
1327
1374
|
serial,
|
|
@@ -1335,7 +1382,7 @@ class AndroidController {
|
|
|
1335
1382
|
const keyCode = Number.isFinite(Number(raw)) ? Number(raw) : (DEFAULT_KEYEVENTS[raw] || null);
|
|
1336
1383
|
if (!keyCode) throw new Error(`Unsupported Android key: ${args.key}`);
|
|
1337
1384
|
await this.#adb(serial, `shell input keyevent ${keyCode}`, { timeout: 10000 });
|
|
1338
|
-
const observation = await this.#captureObservation(serial);
|
|
1385
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1339
1386
|
return {
|
|
1340
1387
|
success: true,
|
|
1341
1388
|
serial,
|
|
@@ -1363,6 +1410,8 @@ class AndroidController {
|
|
|
1363
1410
|
if (node) {
|
|
1364
1411
|
const observation = await this.#captureObservation(dump.serial, {
|
|
1365
1412
|
screenshot: args.screenshot !== false,
|
|
1413
|
+
uiDump: args.uiDump !== false,
|
|
1414
|
+
includeNodes: args.includeNodes,
|
|
1366
1415
|
});
|
|
1367
1416
|
return {
|
|
1368
1417
|
success: true,
|
|
@@ -1400,7 +1449,7 @@ class AndroidController {
|
|
|
1400
1449
|
} else {
|
|
1401
1450
|
throw new Error('packageName is required for android_open_app');
|
|
1402
1451
|
}
|
|
1403
|
-
const observation = await this.#captureObservation(serial);
|
|
1452
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1404
1453
|
return {
|
|
1405
1454
|
success: true,
|
|
1406
1455
|
serial,
|
|
@@ -1426,7 +1475,7 @@ class AndroidController {
|
|
|
1426
1475
|
}
|
|
1427
1476
|
|
|
1428
1477
|
await this.#adb(serial, parts.join(' '), { timeout: 20000 });
|
|
1429
|
-
const observation = await this.#captureObservation(serial);
|
|
1478
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1430
1479
|
return {
|
|
1431
1480
|
success: true,
|
|
1432
1481
|
serial,
|
|
@@ -1456,11 +1505,38 @@ class AndroidController {
|
|
|
1456
1505
|
const apkPath = path.resolve(String(args.apkPath || ''));
|
|
1457
1506
|
if (!apkPath || !fs.existsSync(apkPath)) throw new Error(`APK not found: ${apkPath}`);
|
|
1458
1507
|
const serial = await this.ensureDevice();
|
|
1508
|
+
const extension = path.extname(apkPath).toLowerCase();
|
|
1509
|
+
|
|
1510
|
+
if (extension === '.aab') {
|
|
1511
|
+
throw new Error('.aab app bundles are not directly installable. Export a .apks bundle or .apk first.');
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
if (extension === '.apks') {
|
|
1515
|
+
const extractDir = fs.mkdtempSync(path.join(TMP_DIR, 'apk-bundle-'));
|
|
1516
|
+
try {
|
|
1517
|
+
extractZip(apkPath, extractDir);
|
|
1518
|
+
const bundle = resolveBundleInstallTargets(extractDir);
|
|
1519
|
+
await this.#adb(serial, `install -r ${quoteShell(bundle.installPaths[0])}`, { timeout: 300000 });
|
|
1520
|
+
return {
|
|
1521
|
+
success: true,
|
|
1522
|
+
serial,
|
|
1523
|
+
apkPath,
|
|
1524
|
+
artifactType: 'apks',
|
|
1525
|
+
installedPaths: bundle.installPaths,
|
|
1526
|
+
bundleLayout: bundle.layout,
|
|
1527
|
+
};
|
|
1528
|
+
} finally {
|
|
1529
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1459
1533
|
await this.#adb(serial, `install -r ${quoteShell(apkPath)}`, { timeout: 300000 });
|
|
1460
1534
|
return {
|
|
1461
1535
|
success: true,
|
|
1462
1536
|
serial,
|
|
1463
1537
|
apkPath,
|
|
1538
|
+
artifactType: 'apk',
|
|
1539
|
+
installedPaths: [apkPath],
|
|
1464
1540
|
};
|
|
1465
1541
|
}
|
|
1466
1542
|
|