neoagent 3.2.1-beta.1 → 3.2.1-beta.10

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 (172) 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 +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +111 -20
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +3 -0
  8. package/flutter_app/lib/main_operations.dart +334 -321
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +4 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +78 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +11 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +73083 -72209
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/browser.js +23 -15
  33. package/server/routes/desktop.js +18 -1
  34. package/server/routes/integrations.js +107 -1
  35. package/server/routes/memory.js +1 -0
  36. package/server/routes/settings.js +16 -5
  37. package/server/routes/social_reach.js +12 -3
  38. package/server/routes/social_video.js +4 -0
  39. package/server/services/agents/manager.js +1 -1
  40. package/server/services/ai/capabilityHealth.js +62 -96
  41. package/server/services/ai/compaction.js +7 -2
  42. package/server/services/ai/history.js +45 -6
  43. package/server/services/ai/integrated_tools/http_request.js +8 -0
  44. package/server/services/ai/loop/agent_engine_core.js +390 -162
  45. package/server/services/ai/loop/blank_recovery.js +5 -4
  46. package/server/services/ai/loop/callbacks.js +1 -0
  47. package/server/services/ai/loop/completion_judge.js +121 -5
  48. package/server/services/ai/loop/conversation_loop.js +510 -332
  49. package/server/services/ai/loop/messaging_delivery.js +129 -57
  50. package/server/services/ai/loop/model_call_guard.js +91 -0
  51. package/server/services/ai/loop/model_io.js +20 -45
  52. package/server/services/ai/loop/progress_classification.js +2 -0
  53. package/server/services/ai/loop/tool_dispatch.js +19 -8
  54. package/server/services/ai/loopPolicy.js +48 -21
  55. package/server/services/ai/messagingFallback.js +17 -17
  56. package/server/services/ai/model_discovery.js +227 -0
  57. package/server/services/ai/model_failure_cache.js +108 -0
  58. package/server/services/ai/model_identity.js +71 -0
  59. package/server/services/ai/models.js +68 -163
  60. package/server/services/ai/providerRetry.js +17 -59
  61. package/server/services/ai/provider_selector.js +166 -0
  62. package/server/services/ai/providers/anthropic.js +2 -2
  63. package/server/services/ai/providers/claudeCode.js +21 -33
  64. package/server/services/ai/providers/githubCopilot.js +41 -20
  65. package/server/services/ai/providers/google.js +135 -97
  66. package/server/services/ai/providers/grok.js +4 -3
  67. package/server/services/ai/providers/grokOauth.js +19 -27
  68. package/server/services/ai/providers/nvidia.js +10 -5
  69. package/server/services/ai/providers/ollama.js +111 -84
  70. package/server/services/ai/providers/ollama_stream.js +142 -0
  71. package/server/services/ai/providers/openai.js +39 -5
  72. package/server/services/ai/providers/openaiCodex.js +11 -4
  73. package/server/services/ai/providers/openrouter.js +29 -7
  74. package/server/services/ai/providers/provider_error.js +36 -0
  75. package/server/services/ai/settings.js +26 -2
  76. package/server/services/ai/systemPrompt.js +19 -12
  77. package/server/services/ai/taskAnalysis.js +58 -10
  78. package/server/services/ai/terminal_reply.js +18 -0
  79. package/server/services/ai/toolEvidence.js +350 -29
  80. package/server/services/ai/tools.js +190 -111
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/browser/controller.js +843 -385
  87. package/server/services/browser/extension/gateway.js +40 -16
  88. package/server/services/browser/extension/protocol.js +15 -1
  89. package/server/services/browser/extension/provider.js +71 -47
  90. package/server/services/browser/extension/registry.js +155 -34
  91. package/server/services/cli/executor.js +62 -9
  92. package/server/services/credentials/bitwarden_cli.js +322 -0
  93. package/server/services/credentials/broker.js +594 -0
  94. package/server/services/desktop/gateway.js +41 -4
  95. package/server/services/desktop/protocol.js +3 -0
  96. package/server/services/desktop/provider.js +39 -42
  97. package/server/services/desktop/registry.js +137 -52
  98. package/server/services/integrations/bitwarden/constants.js +14 -0
  99. package/server/services/integrations/bitwarden/provider.js +197 -0
  100. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  101. package/server/services/integrations/figma/provider.js +78 -12
  102. package/server/services/integrations/github/common.js +11 -6
  103. package/server/services/integrations/github/provider.js +52 -53
  104. package/server/services/integrations/google/provider.js +55 -19
  105. package/server/services/integrations/home_assistant/network.js +17 -20
  106. package/server/services/integrations/home_assistant/provider.js +7 -5
  107. package/server/services/integrations/home_assistant/tools.js +17 -5
  108. package/server/services/integrations/http.js +51 -0
  109. package/server/services/integrations/manager.js +159 -53
  110. package/server/services/integrations/microsoft/provider.js +80 -13
  111. package/server/services/integrations/neoarchive/provider.js +55 -29
  112. package/server/services/integrations/neorecall/client.js +17 -10
  113. package/server/services/integrations/neorecall/provider.js +20 -11
  114. package/server/services/integrations/notion/provider.js +16 -13
  115. package/server/services/integrations/oauth_provider.js +115 -51
  116. package/server/services/integrations/registry.js +2 -0
  117. package/server/services/integrations/slack/provider.js +98 -9
  118. package/server/services/integrations/spotify/provider.js +67 -71
  119. package/server/services/integrations/trello/provider.js +21 -7
  120. package/server/services/integrations/weather/provider.js +18 -12
  121. package/server/services/integrations/whatsapp/provider.js +76 -16
  122. package/server/services/manager.js +110 -1
  123. package/server/services/memory/embedding_index.js +20 -8
  124. package/server/services/memory/embeddings.js +151 -90
  125. package/server/services/memory/ingestion.js +50 -9
  126. package/server/services/memory/ingestion_documents.js +13 -3
  127. package/server/services/memory/manager.js +52 -19
  128. package/server/services/messaging/automation.js +85 -10
  129. package/server/services/messaging/formatting_guides.js +7 -4
  130. package/server/services/messaging/http_platforms.js +33 -13
  131. package/server/services/messaging/inbound_queue.js +78 -24
  132. package/server/services/messaging/inbound_store.js +224 -0
  133. package/server/services/messaging/manager.js +326 -51
  134. package/server/services/messaging/typing_keepalive.js +5 -2
  135. package/server/services/messaging/whatsapp.js +22 -14
  136. package/server/services/network/http.js +210 -0
  137. package/server/services/network/safe_request.js +307 -0
  138. package/server/services/runtime/backends/local-vm.js +227 -67
  139. package/server/services/runtime/docker-vm-manager.js +9 -0
  140. package/server/services/runtime/guest_bootstrap.js +30 -4
  141. package/server/services/runtime/guest_image.js +43 -12
  142. package/server/services/runtime/manager.js +77 -23
  143. package/server/services/runtime/validation.js +7 -6
  144. package/server/services/security/tool_categories.js +6 -0
  145. package/server/services/social_reach/channels/github.js +10 -4
  146. package/server/services/social_reach/channels/reddit.js +4 -4
  147. package/server/services/social_reach/channels/rss.js +2 -2
  148. package/server/services/social_reach/channels/social_video.js +12 -7
  149. package/server/services/social_reach/channels/v2ex.js +21 -8
  150. package/server/services/social_reach/channels/x.js +2 -2
  151. package/server/services/social_reach/channels/xueqiu.js +5 -5
  152. package/server/services/social_reach/service.js +9 -6
  153. package/server/services/social_reach/utils.js +65 -14
  154. package/server/services/social_video/service.js +160 -50
  155. package/server/services/tasks/integration_runtime.js +18 -8
  156. package/server/services/tasks/runtime.js +39 -4
  157. package/server/services/voice/agentBridge.js +17 -4
  158. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  159. package/server/services/voice/liveSession.js +31 -0
  160. package/server/services/voice/message.js +1 -1
  161. package/server/services/voice/openaiSpeech.js +33 -8
  162. package/server/services/voice/providers.js +233 -151
  163. package/server/services/voice/runtime.js +2 -2
  164. package/server/services/voice/runtimeManager.js +118 -20
  165. package/server/services/voice/turnRunner.js +6 -0
  166. package/server/services/wearable/firmware_manifest.js +51 -13
  167. package/server/services/wearable/service.js +1 -0
  168. package/server/utils/abort.js +96 -0
  169. package/server/utils/cloud-security.js +110 -3
  170. package/server/utils/files.js +31 -0
  171. package/server/utils/image_payload.js +95 -0
  172. package/server/utils/retry.js +107 -0
@@ -34,6 +34,8 @@ const { DesktopCompanionRegistry } = require('./desktop/registry');
34
34
  const { DesktopProvider } = require('./desktop/provider');
35
35
  const { TimelineService } = require('./timeline/service');
36
36
  const { WearableService } = require('./wearable/service');
37
+ const { BitwardenCli } = require('./credentials/bitwarden_cli');
38
+ const { CredentialBroker } = require('./credentials/broker');
37
39
  const { getRuntimeValidation } = require('./runtime/validation');
38
40
  const {
39
41
  getErrorMessage,
@@ -142,6 +144,17 @@ function createIntegrationManager(app) {
142
144
  return integrationManager;
143
145
  }
144
146
 
147
+ function createCredentialBroker(app) {
148
+ const bitwardenCli = registerLocal(app, 'bitwardenCli', new BitwardenCli());
149
+ const credentialBroker = registerLocal(
150
+ app,
151
+ 'credentialBroker',
152
+ new CredentialBroker({ bitwarden: bitwardenCli }),
153
+ );
154
+ logServiceReady('Credential broker ready');
155
+ return credentialBroker;
156
+ }
157
+
145
158
  function createMemoryIngestionService(app, { memoryManager, integrationManager }) {
146
159
  const memoryIngestionService = registerLocal(
147
160
  app,
@@ -469,10 +482,12 @@ async function startServices(app, io) {
469
482
  const memoryManager = createMemoryManager(app);
470
483
  const mcpClient = createMcpClient(app);
471
484
  createAuthProviderManager(app);
485
+ const credentialBroker = createCredentialBroker(app);
472
486
  const integrationManager = createIntegrationManager(app);
473
487
  createMemoryIngestionService(app, { memoryManager, integrationManager });
474
488
  const browserController = createBrowserController(app, artifactStore);
475
489
  const runtimeManager = createRuntimeManager(app);
490
+ credentialBroker.setRuntimeManager(runtimeManager);
476
491
  const runtimeValidation = getRuntimeValidation(runtimeManager);
477
492
  registerLocal(app, 'runtimeValidation', runtimeValidation);
478
493
  if (!runtimeValidation.ready) {
@@ -566,7 +581,27 @@ async function stopServices(app) {
566
581
  console.error('[ApprovalGate] Shutdown error:', getErrorMessage(err));
567
582
  }
568
583
  }
569
- if (app.locals.agentEngine && typeof app.locals.agentEngine.interruptAllActiveRuns === 'function') {
584
+ if (app.locals.agentEngine && typeof app.locals.agentEngine.shutdown === 'function') {
585
+ try {
586
+ tasks.push(
587
+ app.locals.agentEngine.shutdown().then((status) => {
588
+ if (status.timedOut) {
589
+ console.warn(
590
+ `[AgentEngine] Shutdown timed out with ${status.pendingCount} operation(s) still settling`,
591
+ );
592
+ } else {
593
+ logServiceReady('Agent engine shutdown complete');
594
+ }
595
+ }),
596
+ );
597
+ logServiceReady('Active runs and background work marked interrupted');
598
+ } catch (err) {
599
+ console.error('[AgentEngine] Shutdown error:', getErrorMessage(err));
600
+ }
601
+ } else if (
602
+ app.locals.agentEngine
603
+ && typeof app.locals.agentEngine.interruptAllActiveRuns === 'function'
604
+ ) {
570
605
  try {
571
606
  app.locals.agentEngine.interruptAllActiveRuns();
572
607
  logServiceReady('Active runs marked interrupted');
@@ -595,6 +630,23 @@ async function stopServices(app) {
595
630
  );
596
631
  }
597
632
 
633
+ if (app.locals.messagingAutomationRuntime) {
634
+ tasks.push(
635
+ Promise.resolve()
636
+ .then(() => app.locals.messagingAutomationRuntime.shutdown())
637
+ .then((status) => {
638
+ if (status.timedOut) {
639
+ console.warn('[MessagingAutomation] Shutdown timed out while work was settling');
640
+ } else {
641
+ logServiceReady('Messaging automation shutdown complete');
642
+ }
643
+ })
644
+ .catch((err) => {
645
+ console.error('[MessagingAutomation] Shutdown error:', getErrorMessage(err));
646
+ }),
647
+ );
648
+ }
649
+
598
650
  if (app.locals.streamHub) {
599
651
  try {
600
652
  await app.locals.streamHub.shutdown();
@@ -617,6 +669,22 @@ async function stopServices(app) {
617
669
  );
618
670
  }
619
671
 
672
+ if (
673
+ app.locals.integrationManager &&
674
+ typeof app.locals.integrationManager.shutdown === 'function'
675
+ ) {
676
+ tasks.push(
677
+ Promise.resolve()
678
+ .then(() => app.locals.integrationManager.shutdown())
679
+ .then((status) => {
680
+ logServiceReady(`Official integrations shutdown complete (${status.state})`);
681
+ })
682
+ .catch((err) => {
683
+ console.error('[Integrations] Shutdown error:', getErrorMessage(err));
684
+ }),
685
+ );
686
+ }
687
+
620
688
  if (app.locals.mcpClient) {
621
689
  tasks.push(
622
690
  app.locals.mcpClient.shutdown().catch((err) => {
@@ -641,6 +709,22 @@ async function stopServices(app) {
641
709
  );
642
710
  }
643
711
 
712
+ if (app.locals.browserExtensionGateway?.close) {
713
+ tasks.push(
714
+ app.locals.browserExtensionGateway.close().catch((err) => {
715
+ console.error('[BrowserExtensionGateway] Shutdown error:', getErrorMessage(err));
716
+ }),
717
+ );
718
+ }
719
+
720
+ if (app.locals.desktopCompanionGateway?.close) {
721
+ tasks.push(
722
+ app.locals.desktopCompanionGateway.close().catch((err) => {
723
+ console.error('[DesktopCompanionGateway] Shutdown error:', getErrorMessage(err));
724
+ }),
725
+ );
726
+ }
727
+
644
728
  if (app.locals.browserControllers instanceof Map) {
645
729
  for (const controller of app.locals.browserControllers.values()) {
646
730
  tasks.push(
@@ -659,6 +743,23 @@ async function stopServices(app) {
659
743
  );
660
744
  }
661
745
 
746
+ if (
747
+ app.locals.voiceRuntimeManager
748
+ && typeof app.locals.voiceRuntimeManager.shutdown === 'function'
749
+ ) {
750
+ tasks.push(
751
+ app.locals.voiceRuntimeManager.shutdown().then((status) => {
752
+ if (status.timedOut) {
753
+ console.warn('[VoiceRuntime] Shutdown timed out while sessions were closing');
754
+ } else {
755
+ logServiceReady('Voice runtime shutdown complete');
756
+ }
757
+ }).catch((err) => {
758
+ console.error('[VoiceRuntime] Shutdown error:', getErrorMessage(err));
759
+ }),
760
+ );
761
+ }
762
+
662
763
  if (app.locals.runtimeManager) {
663
764
  tasks.push(
664
765
  app.locals.runtimeManager.shutdown().catch((err) => {
@@ -667,6 +768,14 @@ async function stopServices(app) {
667
768
  );
668
769
  }
669
770
 
771
+ if (app.locals.bitwardenCli) {
772
+ tasks.push(
773
+ app.locals.bitwardenCli.shutdown().catch((err) => {
774
+ console.error('[Bitwarden] Shutdown error:', getErrorMessage(err));
775
+ }),
776
+ );
777
+ }
778
+
670
779
  if (app.locals.widgetService) {
671
780
  const widgetService = app.locals.widgetService;
672
781
  const cleanupMethod = ['shutdown', 'close', 'stop', 'dispose'].find(
@@ -95,6 +95,8 @@ function findEmbeddingCandidates(db, {
95
95
  userId,
96
96
  agentId,
97
97
  embedding,
98
+ embeddingProvider = null,
99
+ embeddingModel = null,
98
100
  limit = DEFAULT_CANDIDATE_LIMIT,
99
101
  }) {
100
102
  const vector = typeof embedding === 'string'
@@ -104,17 +106,25 @@ function findEmbeddingCandidates(db, {
104
106
  if (!probesByBand.length) return [];
105
107
 
106
108
  const matches = new Map();
109
+ const embeddingSpace = embeddingProvider && embeddingModel
110
+ ? `${embeddingProvider}:${embeddingModel}`
111
+ : '';
107
112
  for (const band of probesByBand) {
108
113
  const placeholders = band.values.map(() => '?').join(', ');
109
114
  const rows = db.prepare(
110
- `SELECT memory_id
111
- FROM memory_embedding_bands
112
- WHERE user_id = ?
113
- AND agent_id = ?
114
- AND dimension = ?
115
- AND index_version = ?
116
- AND band_index = ?
117
- AND band_value IN (${placeholders})`
115
+ `SELECT bands.memory_id
116
+ FROM memory_embedding_bands bands
117
+ JOIN memories memory ON memory.id = bands.memory_id
118
+ WHERE bands.user_id = ?
119
+ AND bands.agent_id = ?
120
+ AND bands.dimension = ?
121
+ AND bands.index_version = ?
122
+ AND bands.band_index = ?
123
+ AND bands.band_value IN (${placeholders})
124
+ AND (
125
+ ? = ''
126
+ OR (memory.embedding_provider || ':' || memory.embedding_model) = ?
127
+ )`
118
128
  ).all(
119
129
  userId,
120
130
  agentId || '',
@@ -122,6 +132,8 @@ function findEmbeddingCandidates(db, {
122
132
  INDEX_VERSION,
123
133
  band.bandIndex,
124
134
  ...band.values,
135
+ embeddingSpace,
136
+ embeddingSpace,
125
137
  );
126
138
  for (const row of rows) {
127
139
  matches.set(row.memory_id, (matches.get(row.memory_id) || 0) + 1);
@@ -4,134 +4,192 @@
4
4
  * Embedding helpers for the semantic memory system.
5
5
  *
6
6
  * Provider selection (in priority order):
7
- * 1. Google (text-embedding-004, 768 dims) — when provider hint is 'google' and GOOGLE_AI_KEY is set
7
+ * 1. Google (gemini-embedding-2, 768 dims) — when provider hint is 'google' and GOOGLE_AI_KEY is set
8
8
  * 2. OpenAI (text-embedding-3-small, 1536 dims) — when OPENAI_API_KEY is set
9
9
  * 3. Keyword fallback — when no API key is available
10
10
  */
11
11
 
12
12
  const https = require('https');
13
+ const {
14
+ createAbortError,
15
+ isAbortError,
16
+ throwIfAborted,
17
+ } = require('../../utils/abort');
13
18
 
14
19
  const OPENAI_MODEL = 'text-embedding-3-small';
15
20
  const OPENAI_DIM = 1536;
16
- const GOOGLE_MODEL = 'text-embedding-004';
21
+ const GOOGLE_MODEL = 'gemini-embedding-2';
17
22
  const GOOGLE_DIM = 768;
23
+ const EMBEDDING_TIMEOUT_MS = 15000;
24
+ const MAX_EMBEDDING_RESPONSE_BYTES = 2 * 1024 * 1024;
18
25
 
19
26
  // Exported so callers can sanity-check stored vector dimensions if needed
20
27
  const EMBED_DIM = OPENAI_DIM;
21
28
  const EMBED_DIM_GOOGLE = GOOGLE_DIM;
22
29
 
23
- async function getGeminiEmbedding(text) {
24
- const apiKey = process.env.GOOGLE_AI_KEY;
25
- if (!apiKey) return null;
26
- if (!text || !text.trim()) return null;
27
-
28
- const truncated = text.slice(0, 25000);
30
+ function toEmbeddingVector(value, expectedDimensions) {
31
+ if (!Array.isArray(value) || value.length !== expectedDimensions) return null;
32
+ const vector = new Float32Array(value.length);
33
+ for (let index = 0; index < value.length; index += 1) {
34
+ const number = Number(value[index]);
35
+ if (!Number.isFinite(number)) return null;
36
+ vector[index] = number;
37
+ }
38
+ return vector;
39
+ }
29
40
 
30
- return new Promise((resolve) => {
41
+ function requestEmbeddingJson({ hostname, path, headers, body, signal }) {
42
+ throwIfAborted(signal, 'Embedding request aborted.');
43
+ return new Promise((resolve, reject) => {
31
44
  let settled = false;
32
- const done = (value) => {
45
+ let request = null;
46
+ let response = null;
47
+ let timer = null;
48
+
49
+ const cleanup = () => {
50
+ if (timer) clearTimeout(timer);
51
+ signal?.removeEventListener('abort', onAbort);
52
+ };
53
+ const finish = (error, value = null) => {
33
54
  if (settled) return;
34
55
  settled = true;
35
- resolve(value);
56
+ cleanup();
57
+ if (error) reject(error);
58
+ else resolve(value);
59
+ };
60
+ const onAbort = () => {
61
+ const error = createAbortError(signal, 'Embedding request aborted.');
62
+ response?.destroy(error);
63
+ request?.destroy(error);
64
+ finish(error);
36
65
  };
66
+ signal?.addEventListener('abort', onAbort, { once: true });
37
67
 
38
- const body = JSON.stringify({
39
- model: `models/${GOOGLE_MODEL}`,
40
- content: { parts: [{ text: truncated }] }
41
- });
68
+ timer = setTimeout(() => {
69
+ request?.destroy();
70
+ response?.destroy();
71
+ finish(null, null);
72
+ }, EMBEDDING_TIMEOUT_MS);
42
73
 
43
- const path = `/v1beta/models/${GOOGLE_MODEL}:embedContent?key=${apiKey}`;
44
- const options = {
45
- hostname: 'generativelanguage.googleapis.com',
74
+ request = https.request({
75
+ hostname,
46
76
  path,
47
77
  method: 'POST',
48
78
  headers: {
79
+ ...headers,
49
80
  'Content-Type': 'application/json',
50
- 'Content-Length': Buffer.byteLength(body)
81
+ 'Content-Length': Buffer.byteLength(body),
82
+ },
83
+ }, (incoming) => {
84
+ response = incoming;
85
+ if (settled) {
86
+ incoming.destroy();
87
+ return;
88
+ }
89
+ if (incoming.statusCode < 200 || incoming.statusCode >= 300) {
90
+ incoming.destroy();
91
+ finish(null, null);
92
+ return;
93
+ }
94
+ const contentLength = Number(incoming.headers?.['content-length']);
95
+ if (
96
+ Number.isFinite(contentLength)
97
+ && contentLength > MAX_EMBEDDING_RESPONSE_BYTES
98
+ ) {
99
+ incoming.destroy();
100
+ finish(null, null);
101
+ return;
51
102
  }
52
- };
53
103
 
54
- const req = https.request(options, (res) => {
55
- let data = '';
56
- res.on('data', chunk => { data += chunk; });
57
- res.on('end', () => {
104
+ const chunks = [];
105
+ let totalBytes = 0;
106
+ incoming.on('data', (chunk) => {
107
+ if (settled) return;
108
+ const buffer = Buffer.from(chunk);
109
+ totalBytes += buffer.byteLength;
110
+ if (totalBytes > MAX_EMBEDDING_RESPONSE_BYTES) {
111
+ incoming.destroy();
112
+ finish(null, null);
113
+ return;
114
+ }
115
+ chunks.push(buffer);
116
+ });
117
+ incoming.on('end', () => {
118
+ if (settled) return;
58
119
  try {
59
- const parsed = JSON.parse(data);
60
- const vec = parsed.embedding?.values;
61
- if (!vec) return done(null);
62
- done(new Float32Array(vec));
120
+ finish(null, JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8')));
63
121
  } catch {
64
- done(null);
122
+ finish(null, null);
65
123
  }
66
124
  });
125
+ incoming.on('error', (error) => {
126
+ if (isAbortError(error, signal)) finish(createAbortError(signal));
127
+ else finish(null, null);
128
+ });
129
+ incoming.on('aborted', () => {
130
+ if (signal?.aborted) finish(createAbortError(signal));
131
+ else finish(null, null);
132
+ });
67
133
  });
68
134
 
69
- req.on('error', () => done(null));
70
- req.setTimeout(15000, () => {
71
- req.destroy();
72
- done(null);
135
+ request.on('error', (error) => {
136
+ if (isAbortError(error, signal)) finish(createAbortError(signal));
137
+ else finish(null, null);
73
138
  });
74
- req.write(body);
75
- req.end();
139
+ request.end(body);
76
140
  });
77
141
  }
78
142
 
79
- async function getOpenAIEmbedding(text) {
80
- const apiKey = process.env.OPENAI_API_KEY;
143
+ function formatGoogleEmbeddingInput(text, inputType) {
144
+ if (inputType === 'query') return `task: search result | query: ${text}`;
145
+ if (inputType === 'document') return `title: none | text: ${text}`;
146
+ return `task: sentence similarity | query: ${text}`;
147
+ }
148
+
149
+ async function getGeminiEmbedding(text, options = {}) {
150
+ const apiKey = process.env.GOOGLE_AI_KEY;
81
151
  if (!apiKey) return null;
82
152
  if (!text || !text.trim()) return null;
83
153
 
84
- const truncated = text.slice(0, 25000);
85
-
86
- return new Promise((resolve) => {
87
- let settled = false;
88
- const done = (value) => {
89
- if (settled) return;
90
- settled = true;
91
- resolve(value);
92
- };
93
-
94
- const body = JSON.stringify({
95
- model: OPENAI_MODEL,
96
- input: truncated,
97
- encoding_format: 'float'
98
- });
154
+ const truncated = formatGoogleEmbeddingInput(
155
+ text.slice(0, 25000),
156
+ options.inputType,
157
+ );
158
+ const body = JSON.stringify({
159
+ model: `models/${GOOGLE_MODEL}`,
160
+ content: { parts: [{ text: truncated }] },
161
+ output_dimensionality: GOOGLE_DIM,
162
+ });
163
+ const data = await requestEmbeddingJson({
164
+ hostname: 'generativelanguage.googleapis.com',
165
+ path: `/v1beta/models/${GOOGLE_MODEL}:embedContent`,
166
+ headers: { 'x-goog-api-key': apiKey },
167
+ body,
168
+ signal: options.signal,
169
+ });
170
+ return toEmbeddingVector(data?.embedding?.values, GOOGLE_DIM);
171
+ }
99
172
 
100
- const options = {
101
- hostname: 'api.openai.com',
102
- path: '/v1/embeddings',
103
- method: 'POST',
104
- headers: {
105
- 'Authorization': `Bearer ${apiKey}`,
106
- 'Content-Type': 'application/json',
107
- 'Content-Length': Buffer.byteLength(body)
108
- }
109
- };
173
+ async function getOpenAIEmbedding(text, options = {}) {
174
+ const apiKey = process.env.OPENAI_API_KEY;
175
+ if (!apiKey) return null;
176
+ if (!text || !text.trim()) return null;
110
177
 
111
- const req = https.request(options, (res) => {
112
- let data = '';
113
- res.on('data', chunk => { data += chunk; });
114
- res.on('end', () => {
115
- try {
116
- const parsed = JSON.parse(data);
117
- if (parsed.error) return done(null);
118
- const vec = parsed.data?.[0]?.embedding;
119
- if (!vec) return done(null);
120
- done(new Float32Array(vec));
121
- } catch {
122
- done(null);
123
- }
124
- });
125
- });
178
+ const truncated = text.slice(0, 25000);
126
179
 
127
- req.on('error', () => done(null));
128
- req.setTimeout(15000, () => {
129
- req.destroy();
130
- done(null);
131
- });
132
- req.write(body);
133
- req.end();
180
+ const body = JSON.stringify({
181
+ model: OPENAI_MODEL,
182
+ input: truncated,
183
+ encoding_format: 'float',
184
+ });
185
+ const data = await requestEmbeddingJson({
186
+ hostname: 'api.openai.com',
187
+ path: '/v1/embeddings',
188
+ headers: { Authorization: `Bearer ${apiKey}` },
189
+ body,
190
+ signal: options.signal,
134
191
  });
192
+ return toEmbeddingVector(data?.data?.[0]?.embedding, OPENAI_DIM);
135
193
  }
136
194
 
137
195
  /**
@@ -140,15 +198,16 @@ async function getOpenAIEmbedding(text) {
140
198
  * @param {string} [provider] - 'google' to prefer Gemini embeddings
141
199
  * @returns {Float32Array|null}
142
200
  */
143
- async function getEmbedding(text, provider) {
144
- const result = await getEmbeddingWithMetadata(text, provider);
201
+ async function getEmbedding(text, provider, options = {}) {
202
+ const result = await getEmbeddingWithMetadata(text, provider, options);
145
203
  return result?.vector || null;
146
204
  }
147
205
 
148
- async function getEmbeddingWithMetadata(text, provider) {
206
+ async function getEmbeddingWithMetadata(text, provider, options = {}) {
149
207
  if (!text || !text.trim()) return null;
208
+ throwIfAborted(options.signal, 'Embedding request aborted.');
150
209
  if (provider === 'google' && process.env.GOOGLE_AI_KEY) {
151
- const vec = await getGeminiEmbedding(text);
210
+ const vec = await getGeminiEmbedding(text, options);
152
211
  if (vec) {
153
212
  return {
154
213
  vector: vec,
@@ -158,7 +217,7 @@ async function getEmbeddingWithMetadata(text, provider) {
158
217
  };
159
218
  }
160
219
  }
161
- const vec = await getOpenAIEmbedding(text);
220
+ const vec = await getOpenAIEmbedding(text, options);
162
221
  if (!vec) return null;
163
222
  return {
164
223
  vector: vec,
@@ -228,5 +287,7 @@ module.exports = {
228
287
  deserializeEmbedding,
229
288
  keywordSimilarity,
230
289
  EMBED_DIM,
231
- EMBED_DIM_GOOGLE
290
+ EMBED_DIM_GOOGLE,
291
+ GOOGLE_MODEL,
292
+ OPENAI_MODEL,
232
293
  };