neoagent 3.2.1-beta.2 → 3.2.1-beta.3

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 (144) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/network/http.js +210 -0
  116. package/server/services/network/safe_request.js +307 -0
  117. package/server/services/runtime/backends/local-vm.js +214 -66
  118. package/server/services/runtime/manager.js +17 -12
  119. package/server/services/social_reach/channels/github.js +10 -4
  120. package/server/services/social_reach/channels/reddit.js +4 -4
  121. package/server/services/social_reach/channels/rss.js +2 -2
  122. package/server/services/social_reach/channels/social_video.js +12 -7
  123. package/server/services/social_reach/channels/v2ex.js +21 -8
  124. package/server/services/social_reach/channels/x.js +2 -2
  125. package/server/services/social_reach/channels/xueqiu.js +5 -5
  126. package/server/services/social_reach/service.js +9 -6
  127. package/server/services/social_reach/utils.js +65 -14
  128. package/server/services/social_video/service.js +160 -50
  129. package/server/services/tasks/integration_runtime.js +18 -8
  130. package/server/services/tasks/runtime.js +39 -4
  131. package/server/services/voice/agentBridge.js +17 -4
  132. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  133. package/server/services/voice/liveSession.js +31 -0
  134. package/server/services/voice/openaiSpeech.js +33 -8
  135. package/server/services/voice/providers.js +233 -151
  136. package/server/services/voice/runtimeManager.js +118 -20
  137. package/server/services/voice/turnRunner.js +6 -0
  138. package/server/services/wearable/firmware_manifest.js +51 -13
  139. package/server/services/wearable/service.js +1 -0
  140. package/server/utils/abort.js +96 -0
  141. package/server/utils/cloud-security.js +110 -3
  142. package/server/utils/files.js +31 -0
  143. package/server/utils/image_payload.js +95 -0
  144. package/server/utils/retry.js +107 -0
@@ -1,7 +1,9 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { analyzeImageForUser } = require('./imageAnalysis');
4
- const { isPrivateHost, validateCloudUrl } = require('../../utils/cloud-security');
4
+ const { validateCloudUrlWithDns } = require('../../utils/cloud-security');
5
+ const { isAbortError } = require('../../utils/abort');
6
+ const { fetchResponseText } = require('../network/http');
5
7
  const db = require('../../db/database');
6
8
  const { DATA_DIR } = require('../../../runtime/paths');
7
9
  const { isMainAgent } = require('../agents/manager');
@@ -10,11 +12,13 @@ const {
10
12
  normalizeOutgoingMessageForPlatform,
11
13
  } = require('../messaging/formatting_guides');
12
14
  const { INTERIM_KINDS, normalizeInterimKind } = require('./interim');
15
+ const { isDeferredWorkReply } = require('./terminal_reply');
13
16
  const { normalizeWhatsAppId } = require('../../utils/whatsapp');
14
17
  const {
15
18
  executeIntegratedTool,
16
19
  getIntegratedToolDefinitions,
17
20
  } = require('./integrated_tools');
21
+ const { executeHttpRequest } = require('./integrated_tools/http_request');
18
22
 
19
23
  function compactText(text, maxChars = 120) {
20
24
  const str = String(text || '').replace(/\s+/g, ' ').trim();
@@ -34,6 +38,14 @@ function compactToolDefinition(tool, options = {}) {
34
38
  }
35
39
  };
36
40
 
41
+ // Keep execution-only access metadata on the internal tool definition. The
42
+ // provider adapters serialize only name/description/parameters, so this is
43
+ // never sent as part of an LLM tool schema. The loop uses it to distinguish
44
+ // official-integration reads from writes without hard-coding provider names.
45
+ if (typeof tool.access === 'string' && tool.access.trim()) {
46
+ compact.access = tool.access.trim().toLowerCase();
47
+ }
48
+
37
49
  if (options.includeDescriptions) {
38
50
  compact.description = compactText(tool.description, 320);
39
51
  }
@@ -878,7 +890,7 @@ function getAvailableTools(app, options = {}) {
878
890
  },
879
891
  {
880
892
  name: 'send_message',
881
- description: `Send a message on a connected messaging platform. Supports WhatsApp (text/media), Telnyx Voice (phone calls — TTS), Discord, Telegram, Slack, Google Chat, Microsoft Teams, Matrix, Signal, iMessage/BlueBubbles, IRC, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, WeChat, WebChat, and configurable webhook bridges. ${buildSendMessageFormattingReference()} For WhatsApp: use media_path to attach files. Use content "[NO RESPONSE]" only when the user explicitly asked for silence/no reply, or when a background task intentionally decides no user-visible update is needed with purpose="no_response". For background task or schedule runs, set purpose to final_result, blocker, or no_response.`,
893
+ description: `Send a final message on a connected messaging platform. Use send_interim_update, not this tool, for an ongoing status reply to the originating chat. Supports WhatsApp (text/media), Telnyx Voice (phone calls — TTS), Discord, Telegram, Slack, Google Chat, Microsoft Teams, Matrix, Signal, iMessage/BlueBubbles, IRC, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, WeChat, WebChat, and configurable webhook bridges. ${buildSendMessageFormattingReference()} For WhatsApp: use media_path to attach files. Use content "[NO RESPONSE]" only when the user explicitly asked for silence/no reply, or when a background task intentionally decides no user-visible update is needed with purpose="no_response". For background task or schedule runs, set purpose to final_result, blocker, or no_response.`,
882
894
  parameters: {
883
895
  type: 'object',
884
896
  properties: {
@@ -1632,7 +1644,8 @@ async function executeTool(toolName, args, context, engine) {
1632
1644
  taskId,
1633
1645
  widgetId,
1634
1646
  deliveryState = null,
1635
- allowMultipleProactiveMessages = false
1647
+ allowMultipleProactiveMessages = false,
1648
+ signal = null,
1636
1649
  } = context;
1637
1650
  const runtime = () => app?.locals?.runtimeManager || engine.runtimeManager || null;
1638
1651
  const bc = async () => {
@@ -1641,7 +1654,10 @@ async function executeTool(toolName, args, context, engine) {
1641
1654
  const backend = typeof manager.getActiveBrowserBackend === 'function'
1642
1655
  ? await Promise.resolve(manager.getActiveBrowserBackend(userId))
1643
1656
  : 'vm';
1644
- return { provider: await manager.getBrowserProviderForUser(userId), backend };
1657
+ return {
1658
+ provider: await manager.getBrowserProviderForUser(userId, { signal }),
1659
+ backend,
1660
+ };
1645
1661
  }
1646
1662
  throw new Error('Browser provider is unavailable. VM runtime is required.');
1647
1663
  };
@@ -1672,7 +1688,13 @@ async function executeTool(toolName, args, context, engine) {
1672
1688
 
1673
1689
  const integrationManager = integrations();
1674
1690
  if (integrationManager) {
1675
- const integrationResult = await integrationManager.executeTool(userId, toolName, args, agentId);
1691
+ const integrationResult = await integrationManager.executeTool(
1692
+ userId,
1693
+ toolName,
1694
+ args,
1695
+ agentId,
1696
+ { signal },
1697
+ );
1676
1698
  if (
1677
1699
  integrationResult &&
1678
1700
  typeof integrationResult === 'object' &&
@@ -1724,6 +1746,7 @@ async function executeTool(toolName, args, context, engine) {
1724
1746
  stdinInput: args.stdin_input,
1725
1747
  pty: args.pty === true,
1726
1748
  inputs: Array.isArray(args.inputs) ? args.inputs : [],
1749
+ signal,
1727
1750
  };
1728
1751
  if (typeof runtimeManager.executeCliCommand === 'function') {
1729
1752
  return await runtimeManager.executeCliCommand(userId, args.command, execOptions);
@@ -1736,7 +1759,7 @@ async function executeTool(toolName, args, context, engine) {
1736
1759
  }
1737
1760
 
1738
1761
  case 'browser_navigate': {
1739
- const urlCheck = validateCloudUrl(args.url);
1762
+ const urlCheck = await validateCloudUrlWithDns(args.url, { signal });
1740
1763
  if (!urlCheck.allowed) return { error: 'URL is not allowed: blocked scheme or private/internal network address.' };
1741
1764
  const { provider, backend } = await bc();
1742
1765
  if (!provider) return { error: 'Browser controller not available' };
@@ -1745,14 +1768,15 @@ async function executeTool(toolName, args, context, engine) {
1745
1768
  waitFor: args.waitFor,
1746
1769
  fullPage: args.fullPage,
1747
1770
  referrerMode: args.referrerMode,
1748
- challengeRetry: args.challengeRetry
1771
+ challengeRetry: args.challengeRetry,
1772
+ signal,
1749
1773
  }), backend };
1750
1774
  }
1751
1775
 
1752
1776
  case 'browser_click': {
1753
1777
  const { provider, backend } = await bc();
1754
1778
  if (!provider) return { error: 'Browser controller not available' };
1755
- return { ...await provider.click(args.selector, args.text, args.screenshot !== false), backend };
1779
+ return { ...await provider.click(args.selector, args.text, args.screenshot !== false, { signal }), backend };
1756
1780
  }
1757
1781
 
1758
1782
  case 'browser_type': {
@@ -1760,20 +1784,21 @@ async function executeTool(toolName, args, context, engine) {
1760
1784
  if (!provider) return { error: 'Browser controller not available' };
1761
1785
  return { ...await provider.type(args.selector, args.text, {
1762
1786
  clear: args.clear !== false,
1763
- pressEnter: args.pressEnter
1787
+ pressEnter: args.pressEnter,
1788
+ signal,
1764
1789
  }), backend };
1765
1790
  }
1766
1791
 
1767
1792
  case 'browser_extract': {
1768
1793
  const { provider, backend } = await bc();
1769
1794
  if (!provider) return { error: 'Browser controller not available' };
1770
- return { ...await provider.extract(args.selector, args.attribute, args.all), backend };
1795
+ return { ...await provider.extract(args.selector, args.attribute, args.all, { signal }), backend };
1771
1796
  }
1772
1797
 
1773
1798
  case 'browser_screenshot': {
1774
1799
  const { provider, backend } = await bc();
1775
1800
  if (!provider) return { error: 'Browser controller not available' };
1776
- return { ...await provider.screenshot({ fullPage: args.fullPage, selector: args.selector }), backend };
1801
+ return { ...await provider.screenshot({ fullPage: args.fullPage, selector: args.selector, signal }), backend };
1777
1802
  }
1778
1803
 
1779
1804
  case 'browser_evaluate': {
@@ -1781,13 +1806,13 @@ async function executeTool(toolName, args, context, engine) {
1781
1806
  if (!provider) return { error: 'Browser controller not available' };
1782
1807
  const script = args.script ?? args.javascript;
1783
1808
  if (!script) return { error: 'browser_evaluate requires a "script" argument' };
1784
- return { ...await provider.evaluate(script), backend };
1809
+ return { ...await provider.evaluate(script, { signal }), backend };
1785
1810
  }
1786
1811
 
1787
1812
  case 'android_start_emulator': {
1788
1813
  const controller = await ac();
1789
1814
  if (!controller) return { error: 'Android controller not available' };
1790
- return await controller.startEmulator(args || {});
1815
+ return await controller.startEmulator({ ...(args || {}), signal });
1791
1816
  }
1792
1817
 
1793
1818
  case 'desktop_list_devices': {
@@ -1814,6 +1839,7 @@ async function executeTool(toolName, args, context, engine) {
1814
1839
  return await controller.observe({
1815
1840
  deviceId: args.device_id,
1816
1841
  includeTree: args.includeTree === true,
1842
+ signal,
1817
1843
  });
1818
1844
  }
1819
1845
 
@@ -1823,6 +1849,7 @@ async function executeTool(toolName, args, context, engine) {
1823
1849
  return await controller.clickPoint(args.x, args.y, {
1824
1850
  deviceId: args.device_id,
1825
1851
  button: args.button,
1852
+ signal,
1826
1853
  });
1827
1854
  }
1828
1855
 
@@ -1836,6 +1863,7 @@ async function executeTool(toolName, args, context, engine) {
1836
1863
  x2: args.x2,
1837
1864
  y2: args.y2,
1838
1865
  durationMs: args.durationMs,
1866
+ signal,
1839
1867
  });
1840
1868
  }
1841
1869
 
@@ -1846,6 +1874,7 @@ async function executeTool(toolName, args, context, engine) {
1846
1874
  deviceId: args.device_id,
1847
1875
  deltaX: args.deltaX,
1848
1876
  deltaY: args.deltaY,
1877
+ signal,
1849
1878
  });
1850
1879
  }
1851
1880
 
@@ -1855,6 +1884,7 @@ async function executeTool(toolName, args, context, engine) {
1855
1884
  return await controller.typeText(args.text, {
1856
1885
  deviceId: args.device_id,
1857
1886
  pressEnter: args.pressEnter === true,
1887
+ signal,
1858
1888
  });
1859
1889
  }
1860
1890
 
@@ -1863,6 +1893,7 @@ async function executeTool(toolName, args, context, engine) {
1863
1893
  if (!controller) return { error: 'Desktop provider not available' };
1864
1894
  return await controller.pressKey(args.key, {
1865
1895
  deviceId: args.device_id,
1896
+ signal,
1866
1897
  });
1867
1898
  }
1868
1899
 
@@ -1872,6 +1903,7 @@ async function executeTool(toolName, args, context, engine) {
1872
1903
  return await controller.launchApp({
1873
1904
  deviceId: args.device_id,
1874
1905
  app: args.app,
1906
+ signal,
1875
1907
  });
1876
1908
  }
1877
1909
 
@@ -1880,112 +1912,111 @@ async function executeTool(toolName, args, context, engine) {
1880
1912
  if (!controller) return { error: 'Desktop provider not available' };
1881
1913
  return await controller.getAccessibilityTree({
1882
1914
  deviceId: args.device_id,
1915
+ signal,
1883
1916
  });
1884
1917
  }
1885
1918
 
1886
1919
  case 'android_stop_emulator': {
1887
1920
  const controller = await ac();
1888
1921
  if (!controller) return { error: 'Android controller not available' };
1889
- return await controller.stopEmulator();
1922
+ return await controller.stopEmulator({ signal });
1890
1923
  }
1891
1924
 
1892
1925
  case 'android_list_devices': {
1893
1926
  const controller = await ac();
1894
1927
  if (!controller) return { error: 'Android controller not available' };
1895
- return { devices: await controller.listDevices() };
1928
+ return { devices: await controller.listDevices({ signal }) };
1896
1929
  }
1897
1930
 
1898
1931
  case 'android_open_app': {
1899
1932
  const controller = await ac();
1900
1933
  if (!controller) return { error: 'Android controller not available' };
1901
- return await controller.openApp(args || {});
1934
+ return await controller.openApp({ ...(args || {}), signal });
1902
1935
  }
1903
1936
 
1904
1937
  case 'android_open_intent': {
1905
1938
  const controller = await ac();
1906
1939
  if (!controller) return { error: 'Android controller not available' };
1907
- return await controller.openIntent(args || {});
1940
+ return await controller.openIntent({ ...(args || {}), signal });
1908
1941
  }
1909
1942
 
1910
1943
  case 'android_tap': {
1911
1944
  const controller = await ac();
1912
1945
  if (!controller) return { error: 'Android controller not available' };
1913
- return await controller.tap(args || {});
1946
+ return await controller.tap({ ...(args || {}), signal });
1914
1947
  }
1915
1948
 
1916
1949
  case 'android_long_press': {
1917
1950
  const controller = await ac();
1918
1951
  if (!controller) return { error: 'Android controller not available' };
1919
- return await controller.longPress(args || {});
1952
+ return await controller.longPress({ ...(args || {}), signal });
1920
1953
  }
1921
1954
 
1922
1955
  case 'android_type': {
1923
1956
  const controller = await ac();
1924
1957
  if (!controller) return { error: 'Android controller not available' };
1925
- return await controller.type(args || {});
1958
+ return await controller.type({ ...(args || {}), signal });
1926
1959
  }
1927
1960
 
1928
1961
  case 'android_swipe': {
1929
1962
  const controller = await ac();
1930
1963
  if (!controller) return { error: 'Android controller not available' };
1931
- return await controller.swipe(args || {});
1964
+ return await controller.swipe({ ...(args || {}), signal });
1932
1965
  }
1933
1966
 
1934
1967
  case 'android_press_key': {
1935
1968
  const controller = await ac();
1936
1969
  if (!controller) return { error: 'Android controller not available' };
1937
- return await controller.pressKey(args || {});
1970
+ return await controller.pressKey({ ...(args || {}), signal });
1938
1971
  }
1939
1972
 
1940
1973
  case 'android_wait_for': {
1941
1974
  const controller = await ac();
1942
1975
  if (!controller) return { error: 'Android controller not available' };
1943
- return await controller.waitFor(args || {});
1976
+ return await controller.waitFor({ ...(args || {}), signal });
1944
1977
  }
1945
1978
 
1946
1979
  case 'android_observe': {
1947
1980
  const controller = await ac();
1948
1981
  if (!controller) return { error: 'Android controller not available' };
1949
- return await controller.observe(args || {});
1982
+ return await controller.observe({ ...(args || {}), signal });
1950
1983
  }
1951
1984
 
1952
1985
  case 'android_dump_ui': {
1953
1986
  const controller = await ac();
1954
1987
  if (!controller) return { error: 'Android controller not available' };
1955
- return await controller.dumpUi(args || {});
1988
+ return await controller.dumpUi({ ...(args || {}), signal });
1956
1989
  }
1957
1990
 
1958
1991
  case 'android_screenshot': {
1959
1992
  const controller = await ac();
1960
1993
  if (!controller) return { error: 'Android controller not available' };
1961
- return await controller.screenshot(args || {});
1994
+ return await controller.screenshot({ ...(args || {}), signal });
1962
1995
  }
1963
1996
 
1964
1997
  case 'android_list_apps': {
1965
1998
  const controller = await ac();
1966
1999
  if (!controller) return { error: 'Android controller not available' };
1967
- return await controller.listApps(args || {});
2000
+ return await controller.listApps({ ...(args || {}), signal });
1968
2001
  }
1969
2002
 
1970
2003
  case 'android_install_apk': {
1971
2004
  const controller = await ac();
1972
2005
  if (!controller) return { error: 'Android controller not available' };
1973
- return await controller.installApk(args || {});
2006
+ return await controller.installApk({ ...(args || {}), signal });
1974
2007
  }
1975
2008
 
1976
2009
  case 'android_shell': {
1977
2010
  const controller = await ac();
1978
2011
  if (!controller) return { error: 'Android controller not available' };
1979
- return await controller.shell(args || {});
2012
+ return await controller.shell({ ...(args || {}), signal });
1980
2013
  }
1981
2014
 
1982
2015
  case 'web_search': {
1983
2016
  const apiKey = process.env.BRAVE_SEARCH_API_KEY;
1984
2017
  if (!apiKey) return { error: 'BRAVE_SEARCH_API_KEY is not configured' };
1985
2018
 
1986
- const controller = new AbortController();
1987
2019
  const timeoutMs = 20000;
1988
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1989
2020
 
1990
2021
  try {
1991
2022
  const limit = Math.max(1, Math.min(Number(args.count) || 5, 10));
@@ -2000,15 +2031,20 @@ async function executeTool(toolName, args, context, engine) {
2000
2031
  if (args.search_lang) params.set('search_lang', String(args.search_lang).toLowerCase());
2001
2032
  if (args.freshness) params.set('freshness', args.freshness);
2002
2033
 
2003
- const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params.toString()}`, {
2034
+ const { response, text } = await fetchResponseText(
2035
+ `https://api.search.brave.com/res/v1/web/search?${params.toString()}`,
2036
+ {
2004
2037
  headers: {
2005
2038
  Accept: 'application/json',
2006
2039
  'X-Subscription-Token': apiKey
2007
2040
  },
2008
- signal: controller.signal
2009
- });
2041
+ maxResponseBytes: 2 * 1024 * 1024,
2042
+ serviceName: 'Brave Search',
2043
+ signal,
2044
+ timeoutMs,
2045
+ },
2046
+ );
2010
2047
 
2011
- const text = await res.text();
2012
2048
  let data = null;
2013
2049
  try {
2014
2050
  data = JSON.parse(text);
@@ -2016,9 +2052,9 @@ async function executeTool(toolName, args, context, engine) {
2016
2052
  data = null;
2017
2053
  }
2018
2054
 
2019
- if (!res.ok) {
2055
+ if (!response.ok) {
2020
2056
  return {
2021
- error: `Brave Search API request failed with status ${res.status}`,
2057
+ error: `Brave Search API request failed with status ${response.status}`,
2022
2058
  details: data || text.slice(0, 1000)
2023
2059
  };
2024
2060
  }
@@ -2040,10 +2076,11 @@ async function executeTool(toolName, args, context, engine) {
2040
2076
  results
2041
2077
  };
2042
2078
  } catch (err) {
2043
- if (err.name === 'AbortError') return { error: `Brave Search API request timed out after ${timeoutMs} ms` };
2079
+ if (isAbortError(err, signal)) throw err;
2080
+ if (err.code === 'HTTP_TIMEOUT') {
2081
+ return { error: `Brave Search API request timed out after ${timeoutMs}ms` };
2082
+ }
2044
2083
  return { error: err.message };
2045
- } finally {
2046
- clearTimeout(timer);
2047
2084
  }
2048
2085
  }
2049
2086
 
@@ -2051,7 +2088,13 @@ async function executeTool(toolName, args, context, engine) {
2051
2088
  const { MemoryManager } = require('../memory/manager');
2052
2089
  const mm = new MemoryManager();
2053
2090
  const content = typeof args.content === 'string' ? args.content : args.value;
2054
- const id = await mm.saveMemory(userId, content, args.category || 'episodic', args.importance || 5, { agentId });
2091
+ const id = await mm.saveMemory(
2092
+ userId,
2093
+ content,
2094
+ args.category || 'episodic',
2095
+ args.importance || 5,
2096
+ { agentId, signal },
2097
+ );
2055
2098
  if (!id) {
2056
2099
  return {
2057
2100
  success: true,
@@ -2065,7 +2108,12 @@ async function executeTool(toolName, args, context, engine) {
2065
2108
  case 'memory_recall': {
2066
2109
  const { MemoryManager } = require('../memory/manager');
2067
2110
  const mm = new MemoryManager();
2068
- const results = await mm.recallMemory(userId, args.query, args.limit || 6, { agentId });
2111
+ const results = await mm.recallMemory(
2112
+ userId,
2113
+ args.query,
2114
+ args.limit || 6,
2115
+ { agentId, signal },
2116
+ );
2069
2117
  if (!results.length) return { results: [], message: 'Nothing found' };
2070
2118
  return { results };
2071
2119
  }
@@ -2198,6 +2246,7 @@ async function executeTool(toolName, args, context, engine) {
2198
2246
  includeFrame: args.include_frame !== false,
2199
2247
  forceStt: args.force_stt === true,
2200
2248
  agentId,
2249
+ signal,
2201
2250
  });
2202
2251
  }
2203
2252
 
@@ -2206,7 +2255,7 @@ async function executeTool(toolName, args, context, engine) {
2206
2255
  if (!service || typeof service.getStatus !== 'function') {
2207
2256
  return { error: 'Social reach service is unavailable.' };
2208
2257
  }
2209
- return await service.getStatus(userId);
2258
+ return await service.getStatus(userId, { signal });
2210
2259
  }
2211
2260
 
2212
2261
  case 'social_reach_read': {
@@ -2214,7 +2263,7 @@ async function executeTool(toolName, args, context, engine) {
2214
2263
  if (!service || typeof service.read !== 'function') {
2215
2264
  return { error: 'Social reach service is unavailable.' };
2216
2265
  }
2217
- return await service.read(userId, args || {});
2266
+ return await service.read(userId, args || {}, { signal });
2218
2267
  }
2219
2268
 
2220
2269
  case 'social_reach_search': {
@@ -2222,7 +2271,7 @@ async function executeTool(toolName, args, context, engine) {
2222
2271
  if (!service || typeof service.search !== 'function') {
2223
2272
  return { error: 'Social reach service is unavailable.' };
2224
2273
  }
2225
- return await service.search(userId, args || {});
2274
+ return await service.search(userId, args || {}, { signal });
2226
2275
  }
2227
2276
 
2228
2277
  case 'memory_write': {
@@ -2305,6 +2354,23 @@ async function executeTool(toolName, args, context, engine) {
2305
2354
  stripNoResponseMarker: false
2306
2355
  });
2307
2356
  const suppressReply = normalizedMessage === '[NO RESPONSE]';
2357
+ const originDelivery = isOriginMessagingDelivery({
2358
+ triggerSource,
2359
+ source: context.source,
2360
+ chatId: context.chatId,
2361
+ platform: args.platform,
2362
+ to: args.to,
2363
+ });
2364
+ if (
2365
+ !suppressReply
2366
+ && triggerSource === 'messaging'
2367
+ && originDelivery
2368
+ && isDeferredWorkReply(normalizedMessage)
2369
+ ) {
2370
+ return {
2371
+ error: 'send_message cannot end the run with a promise or progress-only reply. Continue the work, or use send_interim_update for a factual interim update.',
2372
+ };
2373
+ }
2308
2374
  if (isProactiveTrigger(triggerSource)) {
2309
2375
  const proactiveValidation = validateProactiveSendMessageArgs({
2310
2376
  purpose: args.purpose,
@@ -2360,24 +2426,22 @@ async function executeTool(toolName, args, context, engine) {
2360
2426
  };
2361
2427
  }
2362
2428
 
2429
+ if (triggerSource === 'messaging' && originDelivery) {
2430
+ await engine?.stopMessagingProgressSupervisor?.(runId);
2431
+ }
2363
2432
  const sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content, {
2364
2433
  agentId,
2365
2434
  mediaPath: args.media_path,
2366
2435
  runId,
2367
- persistConversation: triggerSource === 'schedule' || triggerSource === 'tasks'
2436
+ persistConversation: triggerSource === 'schedule' || triggerSource === 'tasks',
2437
+ signal,
2368
2438
  });
2369
2439
  // Track that the agent explicitly sent a message during this run
2370
2440
  if (
2371
2441
  !suppressReply
2372
2442
  && sendResult?.success === true
2373
2443
  && sendResult?.suppressed !== true
2374
- && isOriginMessagingDelivery({
2375
- triggerSource,
2376
- source: context.source,
2377
- chatId: context.chatId,
2378
- platform: args.platform,
2379
- to: args.to,
2380
- })
2444
+ && originDelivery
2381
2445
  ) {
2382
2446
  markProactiveMessageSent({ runState, deliveryState, content: normalizedMessage });
2383
2447
  if (runState && triggerSource === 'messaging') {
@@ -2557,66 +2621,14 @@ async function executeTool(toolName, args, context, engine) {
2557
2621
  }
2558
2622
 
2559
2623
  case 'http_request': {
2560
- let parsedUrl;
2561
- try { parsedUrl = new URL(args.url); } catch {
2562
- return { error: 'Invalid URL' };
2563
- }
2564
- const scheme = parsedUrl.protocol.replace(/:$/, '').toLowerCase();
2565
- if (!['http', 'https'].includes(scheme)) {
2566
- return { error: 'URL scheme not allowed. Only http and https are permitted.' };
2567
- }
2568
- const h = parsedUrl.hostname.toLowerCase().replace(/^\[|\]$/g, '');
2569
- if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h.endsWith('.localhost')) {
2570
- return { error: 'Loopback addresses are not permitted.' };
2571
- }
2572
- const allowPrivate = process.env.NEOAGENT_HTTP_ALLOW_PRIVATE !== 'false';
2573
- if (!allowPrivate && isPrivateHost(parsedUrl.hostname)) {
2574
- return { error: 'Private/internal network addresses are not permitted.' };
2575
- }
2576
- const controller = new AbortController();
2577
- const timeoutMs = args.timeout_ms || 30000;
2578
- const timer = setTimeout(() => controller.abort(), timeoutMs);
2579
2624
  try {
2580
- const options = {
2581
- method: args.method || 'GET',
2582
- headers: args.headers || {},
2583
- signal: controller.signal
2584
- };
2585
- if (args.body && ['POST', 'PUT', 'PATCH'].includes(options.method)) {
2586
- options.body = args.body;
2587
- if (!options.headers['Content-Type']) {
2588
- options.headers['Content-Type'] = 'application/json';
2589
- }
2590
- }
2591
- const res = await fetch(args.url, options);
2592
- const MAX_BODY = 512 * 1024;
2593
- const reader = res.body.getReader();
2594
- const chunks = [];
2595
- let total = 0;
2596
- let truncated = false;
2597
- while (true) {
2598
- const { done, value } = await reader.read();
2599
- if (done) break;
2600
- total += value.length;
2601
- if (total > MAX_BODY) {
2602
- const take = MAX_BODY - (total - value.length);
2603
- if (take > 0) chunks.push(value.slice(0, take));
2604
- truncated = true;
2605
- break;
2606
- }
2607
- chunks.push(value);
2608
- }
2609
- const text = Buffer.concat(chunks.map(c => Buffer.from(c))).toString('utf-8');
2610
- return {
2611
- status: res.status,
2612
- headers: Object.fromEntries(res.headers.entries()),
2613
- body: truncated ? text + '\n...[truncated]' : text,
2614
- };
2625
+ return await executeHttpRequest(args, {
2626
+ allowPrivate: process.env.NEOAGENT_HTTP_ALLOW_PRIVATE === 'true',
2627
+ signal,
2628
+ });
2615
2629
  } catch (err) {
2616
- if (err.name === 'AbortError') return { error: `Request timed out after ${timeoutMs} ms` };
2630
+ if (isAbortError(err, signal)) throw err;
2617
2631
  return { error: err.message };
2618
- } finally {
2619
- clearTimeout(timer);
2620
2632
  }
2621
2633
  }
2622
2634
 
@@ -2762,7 +2774,8 @@ async function executeTool(toolName, args, context, engine) {
2762
2774
  const sendResult = await manager.sendMessage(userId, target.platform, target.to, message, {
2763
2775
  agentId,
2764
2776
  runId,
2765
- persistConversation: true
2777
+ persistConversation: true,
2778
+ signal,
2766
2779
  });
2767
2780
  if (taskId && taskConfig && (taskConfig.notifyPlatform !== target.platform || taskConfig.notifyTo !== target.to)) {
2768
2781
  taskConfig.notifyPlatform = target.platform;