neoagent 2.3.1-beta.85 → 2.3.1-beta.87

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 (55) hide show
  1. package/docs/capabilities.md +2 -0
  2. package/flutter_app/android/app/src/main/AndroidManifest.xml +14 -0
  3. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +84 -0
  4. package/flutter_app/lib/main_chat.dart +156 -2
  5. package/flutter_app/lib/main_controller.dart +137 -10
  6. package/flutter_app/lib/main_models.dart +69 -0
  7. package/flutter_app/lib/main_operations.dart +248 -0
  8. package/flutter_app/lib/main_runtime.dart +11 -2
  9. package/flutter_app/lib/main_settings.dart +173 -176
  10. package/flutter_app/lib/main_shared.dart +78 -0
  11. package/flutter_app/lib/src/app_launch_bridge.dart +39 -10
  12. package/flutter_app/lib/src/backend_client.dart +28 -0
  13. package/package.json +1 -1
  14. package/server/guest-agent.android.package.json +13 -0
  15. package/server/guest-agent.browser.package.json +14 -0
  16. package/server/guest_agent.js +61 -44
  17. package/server/http/routes.js +1 -0
  18. package/server/public/.last_build_id +1 -1
  19. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  20. package/server/public/flutter_bootstrap.js +1 -1
  21. package/server/public/main.dart.js +69936 -69277
  22. package/server/routes/android.js +2 -11
  23. package/server/routes/browser.js +2 -2
  24. package/server/routes/memory.js +90 -0
  25. package/server/routes/social_video.js +62 -0
  26. package/server/services/ai/capabilityHealth.js +6 -14
  27. package/server/services/ai/systemPrompt.js +1 -0
  28. package/server/services/ai/toolResult.js +20 -0
  29. package/server/services/ai/tools.js +29 -0
  30. package/server/services/android/android_bootstrap_worker.js +2 -2
  31. package/server/services/android/controller.js +528 -132
  32. package/server/services/browser/controller.js +51 -68
  33. package/server/services/manager.js +15 -0
  34. package/server/services/memory/llm_transfer.js +217 -0
  35. package/server/services/runtime/backends/local-vm.js +16 -3
  36. package/server/services/runtime/guest_bootstrap.js +224 -56
  37. package/server/services/runtime/manager.js +53 -15
  38. package/server/services/runtime/qemu.js +149 -24
  39. package/server/services/runtime/settings.js +9 -14
  40. package/server/services/runtime/validation.js +10 -11
  41. package/server/services/social_video/adapters/base.js +26 -0
  42. package/server/services/social_video/adapters/index.js +27 -0
  43. package/server/services/social_video/adapters/instagram.js +17 -0
  44. package/server/services/social_video/adapters/tiktok.js +17 -0
  45. package/server/services/social_video/adapters/x.js +17 -0
  46. package/server/services/social_video/adapters/youtube.js +17 -0
  47. package/server/services/social_video/captions.js +187 -0
  48. package/server/services/social_video/frame.js +42 -0
  49. package/server/services/social_video/index.js +7 -0
  50. package/server/services/social_video/metadata.js +63 -0
  51. package/server/services/social_video/result.js +63 -0
  52. package/server/services/social_video/service.js +576 -0
  53. package/server/services/social_video/url.js +83 -0
  54. package/server/utils/deployment.js +4 -4
  55. package/server/guest-agent.package.json +0 -15
@@ -48,7 +48,7 @@ async function getAndroidController(req) {
48
48
  if (runtimeManager && typeof runtimeManager.getAndroidProviderForUser === 'function') {
49
49
  return runtimeManager.getAndroidProviderForUser(req.session?.userId);
50
50
  }
51
- throw new Error('Android controller is unavailable. VM runtime is required.');
51
+ throw new Error('Android controller is unavailable.');
52
52
  }
53
53
 
54
54
  function getAndroidStatusSnapshot(req) {
@@ -77,17 +77,8 @@ function handleAndroidAction(action) {
77
77
 
78
78
  router.get('/status', async (req, res) => {
79
79
  try {
80
- const runtimeManager = req.app?.locals?.runtimeManager;
81
- if (!runtimeManager?.hasVmForUser?.(req.session?.userId)) {
82
- res.json(getAndroidStatusSnapshot(req));
83
- return;
84
- }
85
- if (!await runtimeManager?.isGuestAgentReadyForUser?.(req.session?.userId, 1000)) {
86
- res.json(getAndroidStatusSnapshot(req));
87
- return;
88
- }
89
80
  const controller = await getAndroidController(req);
90
- res.json(await controller.getStatus());
81
+ res.json(await controller.getStatus().catch(() => getAndroidStatusSnapshot(req)));
91
82
  } catch (err) {
92
83
  res.status(500).json({ error: sanitizeError(err) });
93
84
  }
@@ -41,11 +41,11 @@ function getBrowserStatusSnapshot(req) {
41
41
  router.get('/status', async (req, res) => {
42
42
  try {
43
43
  const runtimeManager = req.app?.locals?.runtimeManager;
44
- if (!runtimeManager?.hasVmForUser?.(req.session?.userId)) {
44
+ if (!runtimeManager?.hasVmForUser?.(req.session?.userId, 'browser')) {
45
45
  res.json(getBrowserStatusSnapshot(req));
46
46
  return;
47
47
  }
48
- if (!await runtimeManager?.isGuestAgentReadyForUser?.(req.session?.userId, 1000)) {
48
+ if (!await runtimeManager?.isGuestAgentReadyForUser?.(req.session?.userId, 1000, 'browser')) {
49
49
  res.json(getBrowserStatusSnapshot(req));
50
50
  return;
51
51
  }
@@ -4,6 +4,11 @@ const rateLimit = require('express-rate-limit');
4
4
  const { requireAuth } = require('../middleware/auth');
5
5
  const { sanitizeError } = require('../utils/security');
6
6
  const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
7
+ const {
8
+ buildLlmTransferPrompt,
9
+ parseLlmTransferText,
10
+ importanceForCategory,
11
+ } = require('../services/memory/llm_transfer');
7
12
 
8
13
  router.use(requireAuth);
9
14
 
@@ -15,6 +20,16 @@ const apiKeyMutationLimiter = rateLimit({
15
20
  legacyHeaders: false,
16
21
  });
17
22
 
23
+ const transferImportLimiter = rateLimit({
24
+ windowMs: 10 * 60 * 1000,
25
+ max: 20,
26
+ message: { error: 'Too many memory imports, try again later' },
27
+ standardHeaders: true,
28
+ legacyHeaders: false,
29
+ });
30
+
31
+ const MAX_TRANSFER_TEXT_BYTES = 60 * 1024;
32
+
18
33
  function normalizeMemoryIds(value) {
19
34
  return [...new Set(
20
35
  (Array.isArray(value) ? value : [])
@@ -262,6 +277,81 @@ router.get('/core', (req, res) => {
262
277
  res.json(coreMemory);
263
278
  });
264
279
 
280
+ // ─────────────────────────────────────────────────────────────────────────────
281
+ // LLM Transfer (prompt + import)
282
+ // ─────────────────────────────────────────────────────────────────────────────
283
+
284
+ router.get('/transfer-prompt', (req, res) => {
285
+ const prompt = buildLlmTransferPrompt({ agentLabel: 'NeoAgent' });
286
+ res.json({ prompt });
287
+ });
288
+
289
+ router.post('/transfer-import', transferImportLimiter, async (req, res) => {
290
+ const mm = req.app.locals.memoryManager;
291
+ const userId = req.session.userId;
292
+ const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
293
+ const text = String(req.body?.text || '').trim();
294
+ if (!text) {
295
+ return res.status(400).json({ error: 'text is required' });
296
+ }
297
+ if (Buffer.byteLength(text, 'utf8') > MAX_TRANSFER_TEXT_BYTES) {
298
+ return res.status(413).json({ error: 'text exceeds 60KB limit' });
299
+ }
300
+
301
+ const applyBehaviorNotes = req.body?.applyBehaviorNotes !== false;
302
+ const applyCoreMemory = req.body?.applyCoreMemory !== false;
303
+
304
+ try {
305
+ const parsed = parseLlmTransferText(text);
306
+ let importedCount = 0;
307
+ let skippedCount = 0;
308
+
309
+ for (const memory of parsed.memories) {
310
+ const content = String(memory.content || '').trim();
311
+ if (!content) {
312
+ skippedCount += 1;
313
+ continue;
314
+ }
315
+ await mm.saveMemory(userId, content, memory.category, importanceForCategory(memory.category), {
316
+ agentId,
317
+ sourceRef: {
318
+ sourceType: 'llm_import',
319
+ sourceLabel: 'LLM memory transfer',
320
+ },
321
+ metadata: {
322
+ importedFrom: 'llm_transfer',
323
+ },
324
+ });
325
+ importedCount += 1;
326
+ }
327
+
328
+ let coreUpdatedCount = 0;
329
+ if (applyCoreMemory && parsed.coreEntries) {
330
+ for (const [key, value] of Object.entries(parsed.coreEntries)) {
331
+ if (!key || key === 'active_context') continue;
332
+ mm.updateCore(userId, key, value, { agentId });
333
+ coreUpdatedCount += 1;
334
+ }
335
+ }
336
+
337
+ let behaviorNotesUpdated = false;
338
+ if (applyBehaviorNotes && parsed.behaviorNotes) {
339
+ mm.setAssistantBehaviorNotes(userId, parsed.behaviorNotes, { agentId });
340
+ behaviorNotesUpdated = true;
341
+ }
342
+
343
+ res.json({
344
+ importedCount,
345
+ skippedCount,
346
+ coreUpdatedCount,
347
+ behaviorNotesUpdated,
348
+ warnings: parsed.warnings || [],
349
+ });
350
+ } catch (err) {
351
+ res.status(500).json({ error: sanitizeError(err) });
352
+ }
353
+ });
354
+
265
355
  router.put('/core/:key', (req, res) => {
266
356
  const mm = req.app.locals.memoryManager;
267
357
  const userId = req.session.userId;
@@ -0,0 +1,62 @@
1
+ const express = require('express');
2
+ const { requireAuth } = require('../middleware/auth');
3
+ const { sanitizeError } = require('../utils/security');
4
+
5
+ const router = express.Router();
6
+
7
+ router.use(requireAuth);
8
+
9
+ router.get('/health', async (req, res) => {
10
+ try {
11
+ const service = req.app?.locals?.socialVideoService;
12
+ if (!service || typeof service.getHealthStatus !== 'function') {
13
+ return res.status(503).json({
14
+ ready: false,
15
+ error: 'Social video service is unavailable.',
16
+ });
17
+ }
18
+ const health = await service.getHealthStatus({
19
+ forceRefresh: req.query?.refresh === '1' || req.query?.refresh === 'true',
20
+ });
21
+ return res.json(health);
22
+ } catch (error) {
23
+ return res.status(500).json({ error: sanitizeError(error) });
24
+ }
25
+ });
26
+
27
+ router.post('/extract', async (req, res) => {
28
+ try {
29
+ const service = req.app?.locals?.socialVideoService;
30
+ if (!service || typeof service.extractFromUrl !== 'function') {
31
+ return res.status(503).json({ error: 'Social video service is unavailable.' });
32
+ }
33
+ if (!req.session || !req.session.userId) {
34
+ return res.status(401).json({ error: 'Authentication required.' });
35
+ }
36
+
37
+ const sourceUrl = String(req.body?.url || '').trim();
38
+ if (!sourceUrl) {
39
+ return res.status(400).json({ error: 'url is required.' });
40
+ }
41
+
42
+ const result = await service.extractFromUrl(req.session.userId, sourceUrl, {
43
+ includeFrame: req.body?.include_frame !== false,
44
+ forceStt: req.body?.force_stt === true,
45
+ });
46
+
47
+ if (result?.setup?.ready === false) {
48
+ return res.status(503).json(result);
49
+ }
50
+
51
+ if (Array.isArray(result.errors) && result.errors.length > 0) {
52
+ const failed = !result.title && !result.description && !result.transcript && !result.frameImage;
53
+ return res.status(failed ? 422 : 200).json(result);
54
+ }
55
+
56
+ return res.json(result);
57
+ } catch (error) {
58
+ return res.status(500).json({ error: sanitizeError(error) });
59
+ }
60
+ });
61
+
62
+ module.exports = router;
@@ -1,6 +1,5 @@
1
1
  const db = require('../../db/database');
2
2
  const { getProviderHealthCatalog } = require('./models');
3
- const { resolveBrowserExecutablePath } = require('../browser/controller');
4
3
  const { deriveCloudBrowserBackend } = require('../runtime/settings');
5
4
 
6
5
  function capabilityEntry(overrides = {}) {
@@ -38,7 +37,6 @@ function summarizeCapabilityHealth(health) {
38
37
 
39
38
  async function getBrowserHealth(userId, app, engine) {
40
39
  const runtimeManager = app?.locals?.runtimeManager || engine?.runtimeManager || null;
41
- const executablePath = resolveBrowserExecutablePath();
42
40
  const runtimeSettings = typeof runtimeManager?.getSettings === 'function'
43
41
  ? runtimeManager.getSettings(userId)
44
42
  : null;
@@ -83,12 +81,11 @@ async function getBrowserHealth(userId, app, engine) {
83
81
 
84
82
  if (!controller && resolutionError) {
85
83
  return capabilityEntry({
86
- configured: Boolean(executablePath),
84
+ configured: true,
87
85
  healthy: false,
88
86
  degraded: true,
89
87
  summary: `Browser controller resolution failed: ${resolutionError.message}`,
90
88
  details: {
91
- executablePath: executablePath || null,
92
89
  error: resolutionError.message,
93
90
  },
94
91
  });
@@ -108,20 +105,15 @@ async function getBrowserHealth(userId, app, engine) {
108
105
 
109
106
  return capabilityEntry({
110
107
  connected: launched,
111
- configured: Boolean(executablePath),
112
- healthy: Boolean(executablePath) && !error,
108
+ configured: true,
109
+ healthy: !error,
113
110
  degraded: Boolean(error) || runtimeSettings?.browser_backend === 'extension',
114
111
  summary: error
115
112
  ? `Browser runtime error: ${error}`
116
113
  : runtimeSettings?.browser_backend === 'extension'
117
- ? executablePath
118
- ? `No extension device is active. Falling back to the ${deriveCloudBrowserBackend(runtimeSettings)} browser runtime.`
119
- : 'No extension device is active and no browser executable was found for Puppeteer.'
120
- : executablePath
121
- ? (launched ? 'Browser runtime is ready.' : 'Browser executable is available but not launched.')
122
- : 'No browser executable was found for Puppeteer.',
114
+ ? `No extension device is active. Falling back to the ${deriveCloudBrowserBackend(runtimeSettings)} browser runtime.`
115
+ : (launched ? 'Browser runtime is ready.' : 'Browser runtime is available but not launched.'),
123
116
  details: {
124
- executablePath: executablePath || null,
125
117
  preferredBackend: runtimeSettings?.browser_backend || null,
126
118
  backend: runtimeSettings?.browser_backend === 'extension'
127
119
  ? deriveCloudBrowserBackend(runtimeSettings)
@@ -175,7 +167,7 @@ async function getAndroidHealth(userId, app, engine) {
175
167
  summary: status.lastStartError
176
168
  ? `Android tooling reported: ${status.lastStartError}`
177
169
  : bootstrapped
178
- ? 'Android environment is bootstrapped.'
170
+ ? 'Android environment is ready on this host.'
179
171
  : (canBootstrap ? 'Android environment can be bootstrapped on this host.' : 'Android tooling cannot bootstrap on this host.'),
180
172
  details: status,
181
173
  });
@@ -94,6 +94,7 @@ Separate facts from inferences. If you are inferring from logs, code, or partial
94
94
  When evidence conflicts, state the conflict instead of smoothing it over.
95
95
  Source priority for factual work is: direct tool output and first-party integrations in this run, then authoritative primary sources, then other web sources, then model memory. Search-result snippets, link previews, and remembered facts are leads, not evidence.
96
96
  If the user provides a URL, open or fetch that URL before describing its contents unless the user only wants formatting help with the URL itself.
97
+ If the user sends only a video link with no extra instruction, default to researching and fact-checking the video's key claims and context.
97
98
 
98
99
  DON'T REPEAT YOURSELF
99
100
  State a limitation or error once. If the user pushes back, try a different approach before restating the same failure. Repeating the same dead-end across five messages is useless.
@@ -112,6 +112,26 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
112
112
  });
113
113
  break;
114
114
 
115
+ case 'social_video_extract':
116
+ envelope = trimObject({
117
+ tool: toolName,
118
+ platform: toolResult?.platform,
119
+ sourceUrl: toolResult?.sourceUrl,
120
+ resolvedUrl: toolResult?.resolvedUrl,
121
+ title: clampText(toolResult?.title || '', Math.floor(softLimit * 0.25)),
122
+ description: clampText(toolResult?.description || '', Math.floor(softLimit * 0.25)),
123
+ transcriptSource: toolResult?.transcriptSource,
124
+ transcriptPreview: lineExcerpt(toolResult?.transcript || '', 6, Math.floor(softLimit * 0.35)),
125
+ frameImage: trimObject({
126
+ url: toolResult?.frameImage?.url,
127
+ source: toolResult?.frameImage?.source,
128
+ }),
129
+ setupReady: toolResult?.setup?.ready,
130
+ warningCount: Array.isArray(toolResult?.warnings) ? toolResult.warnings.length : 0,
131
+ errorCount: Array.isArray(toolResult?.errors) ? toolResult.errors.length : 0,
132
+ });
133
+ break;
134
+
115
135
  case 'android_dump_ui':
116
136
  case 'android_observe':
117
137
  envelope = trimObject({
@@ -1267,6 +1267,19 @@ function getAvailableTools(app, options = {}) {
1267
1267
  },
1268
1268
  required: ['query']
1269
1269
  }
1270
+ },
1271
+ {
1272
+ name: 'social_video_extract',
1273
+ description: 'Extract title, description, transcript, and one representative frame image from a public social video URL (YouTube, TikTok, Instagram, or X) without social API keys.',
1274
+ parameters: {
1275
+ type: 'object',
1276
+ properties: {
1277
+ url: { type: 'string', description: 'Public social video URL.' },
1278
+ include_frame: { type: 'boolean', description: 'Whether to return one representative frame image artifact (default true).' },
1279
+ force_stt: { type: 'boolean', description: 'Force speech-to-text fallback even if captions are present.' }
1280
+ },
1281
+ required: ['url']
1282
+ }
1270
1283
  }
1271
1284
  ];
1272
1285
 
@@ -1392,6 +1405,7 @@ async function executeTool(toolName, args, context, engine) {
1392
1405
  const sk = () => app?.locals?.skillRunner || engine.skillRunner;
1393
1406
  const taskRuntime = () => app?.locals?.taskRuntime || engine.taskRuntime;
1394
1407
  const rec = () => app?.locals?.recordingManager || null;
1408
+ const socialVideo = () => app?.locals?.socialVideoService || null;
1395
1409
  const widgets = () => app?.locals?.widgetService || null;
1396
1410
  const artifactStore = app?.locals?.artifactStore || null;
1397
1411
 
@@ -1950,6 +1964,21 @@ async function executeTool(toolName, args, context, engine) {
1950
1964
  };
1951
1965
  }
1952
1966
 
1967
+ case 'social_video_extract': {
1968
+ const service = socialVideo();
1969
+ if (!service || typeof service.extractFromUrl !== 'function') {
1970
+ return { error: 'Social video extraction service is unavailable.' };
1971
+ }
1972
+ const sourceUrl = String(args.url || '').trim();
1973
+ if (!sourceUrl) {
1974
+ return { error: 'url is required' };
1975
+ }
1976
+ return await service.extractFromUrl(userId, sourceUrl, {
1977
+ includeFrame: args.include_frame !== false,
1978
+ forceStt: args.force_stt === true,
1979
+ });
1980
+ }
1981
+
1953
1982
  case 'memory_write': {
1954
1983
  const { MemoryManager } = require('../memory/manager');
1955
1984
  const mm = new MemoryManager();
@@ -10,10 +10,10 @@ function parseBoolean(value) {
10
10
  async function main() {
11
11
  const controller = new AndroidController({
12
12
  userId: process.env.NEOAGENT_ANDROID_BOOTSTRAP_USER_ID || null,
13
- runtimeBackend: 'vm',
13
+ runtimeBackend: 'host',
14
14
  });
15
15
  const headless = parseBoolean(process.env.NEOAGENT_ANDROID_BOOTSTRAP_HEADLESS);
16
- const timeoutMs = Math.max(120000, Number(process.env.NEOAGENT_ANDROID_BOOTSTRAP_TIMEOUT_MS) || 240000);
16
+ const timeoutMs = Math.max(120000, Number(process.env.NEOAGENT_ANDROID_BOOTSTRAP_TIMEOUT_MS) || 600000);
17
17
 
18
18
  try {
19
19
  await controller.bootstrapEmulator({ headless, timeoutMs });