neoagent 3.2.1-beta.6 → 3.2.1-beta.8

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.
@@ -1,7 +1,63 @@
1
- const { GoogleGenerativeAI } = require('@google/generative-ai');
1
+ 'use strict';
2
+
3
+ const { GoogleGenAI } = require('@google/genai');
2
4
  const { BaseProvider } = require('./base');
3
5
  const { fetchResponseText } = require('../../network/http');
4
6
 
7
+ function parseToolArguments(value) {
8
+ try {
9
+ const parsed = JSON.parse(value || '{}');
10
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
11
+ ? parsed
12
+ : {};
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ function normalizeUsage(usage) {
19
+ if (!usage) return null;
20
+ return {
21
+ inputTokens: usage.promptTokenCount || 0,
22
+ outputTokens: usage.candidatesTokenCount || 0,
23
+ reasoningTokens: usage.thoughtsTokenCount || 0,
24
+ cachedReadTokens: usage.cachedContentTokenCount || 0,
25
+ cacheWriteTokens: 0,
26
+ promptTokens: usage.promptTokenCount || 0,
27
+ completionTokens: usage.candidatesTokenCount || 0,
28
+ totalTokens: usage.totalTokenCount || 0
29
+ };
30
+ }
31
+
32
+ function collectResponseParts(response, toolCalls, seenToolCalls = null) {
33
+ let content = '';
34
+ for (const candidate of response?.candidates || []) {
35
+ for (const part of candidate.content?.parts || []) {
36
+ if (part.text && part.thought !== true) content += part.text;
37
+ if (!part.functionCall?.name) continue;
38
+
39
+ const args = part.functionCall.args || {};
40
+ const providerCallId = String(part.functionCall.id || '').trim();
41
+ const signature = providerCallId
42
+ || `${part.functionCall.name}:${JSON.stringify(args)}`;
43
+ if (seenToolCalls?.has(signature)) continue;
44
+ seenToolCalls?.add(signature);
45
+
46
+ toolCalls.push({
47
+ id: providerCallId
48
+ || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
49
+ type: 'function',
50
+ function: {
51
+ name: part.functionCall.name,
52
+ arguments: JSON.stringify(args),
53
+ thought_signature: part.thoughtSignature
54
+ }
55
+ });
56
+ }
57
+ }
58
+ return content;
59
+ }
60
+
5
61
  class GoogleProvider extends BaseProvider {
6
62
  constructor(config = {}) {
7
63
  super(config);
@@ -29,7 +85,7 @@ class GoogleProvider extends BaseProvider {
29
85
  'gemini-1.5-flash': 1048576,
30
86
  };
31
87
  this.apiKey = config.apiKey || process.env.GOOGLE_AI_KEY;
32
- this.genAI = new GoogleGenerativeAI(this.apiKey);
88
+ this.genAI = new GoogleGenAI({ apiKey: this.apiKey });
33
89
  }
34
90
 
35
91
  async listModels(signal = null) {
@@ -78,11 +134,23 @@ class GoogleProvider extends BaseProvider {
78
134
  functionDeclarations: tools.map(tool => ({
79
135
  name: tool.name,
80
136
  description: tool.description,
81
- parameters: tool.parameters || { type: 'object', properties: {} }
137
+ parametersJsonSchema: tool.parameters || { type: 'object', properties: {} }
82
138
  }))
83
139
  }];
84
140
  }
85
141
 
142
+ buildGenerateConfig(systemInstruction, tools, options) {
143
+ const config = {};
144
+ if (systemInstruction) config.systemInstruction = systemInstruction;
145
+ if (tools.length > 0) config.tools = this.formatTools(tools);
146
+ if (options.signal) config.abortSignal = options.signal;
147
+ const maxOutputTokens = Number(options.maxTokens);
148
+ if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0) {
149
+ config.maxOutputTokens = Math.floor(maxOutputTokens);
150
+ }
151
+ return config;
152
+ }
153
+
86
154
  convertMessages(messages) {
87
155
  let systemInstruction = '';
88
156
  const history = [];
@@ -93,14 +161,16 @@ class GoogleProvider extends BaseProvider {
93
161
  continue;
94
162
  }
95
163
  if (msg.role === 'tool') {
164
+ const functionResponse = {
165
+ name: msg.name || 'tool',
166
+ response: { result: msg.content }
167
+ };
168
+ if (msg.tool_call_id) functionResponse.id = msg.tool_call_id;
96
169
  history.push({
97
- role: 'function',
98
- parts: [{
99
- functionResponse: {
100
- name: msg.name || 'tool',
101
- response: { result: msg.content }
102
- }
103
- }]
170
+ // The current Gemini API represents tool results as functionResponse
171
+ // parts on a user turn.
172
+ role: 'user',
173
+ parts: [{ functionResponse }]
104
174
  });
105
175
  continue;
106
176
  }
@@ -111,9 +181,10 @@ class GoogleProvider extends BaseProvider {
111
181
  const functionCallPart = {
112
182
  functionCall: {
113
183
  name: tc.function.name,
114
- args: JSON.parse(tc.function.arguments || '{}')
184
+ args: parseToolArguments(tc.function.arguments)
115
185
  }
116
186
  };
187
+ if (tc.id) functionCallPart.functionCall.id = tc.id;
117
188
  if (tc.function.thought_signature) {
118
189
  functionCallPart.thoughtSignature = tc.function.thought_signature;
119
190
  }
@@ -147,6 +218,18 @@ class GoogleProvider extends BaseProvider {
147
218
  normalizedHistory.push({ role: currentRole, parts: currentParts });
148
219
  }
149
220
 
221
+ // Structured helpers can consist entirely of system instructions. Gemini
222
+ // still requires a user turn to trigger generation.
223
+ if (
224
+ normalizedHistory.length === 0
225
+ || normalizedHistory[normalizedHistory.length - 1].role === 'model'
226
+ ) {
227
+ normalizedHistory.push({
228
+ role: 'user',
229
+ parts: [{ text: 'Provide the requested response using the system instructions.' }]
230
+ });
231
+ }
232
+
150
233
  if (normalizedHistory.length > 0 && normalizedHistory[0].role !== 'user') {
151
234
  normalizedHistory.unshift({
152
235
  role: 'user',
@@ -160,53 +243,18 @@ class GoogleProvider extends BaseProvider {
160
243
  async chat(messages, tools = [], options = {}) {
161
244
  const model = options.model || this.config.model || this.getDefaultModel();
162
245
  const { systemInstruction, history } = this.convertMessages(messages);
163
-
164
- const genModel = this.genAI.getGenerativeModel({
246
+ const response = await this.genAI.models.generateContent({
165
247
  model,
166
- systemInstruction: systemInstruction || undefined,
167
- tools: tools.length > 0 ? this.formatTools(tools) : undefined
248
+ contents: history,
249
+ config: this.buildGenerateConfig(systemInstruction, tools, options)
168
250
  });
169
-
170
- const lastMessage = history.pop();
171
- const chat = genModel.startChat({ history });
172
- const result = await chat.sendMessage(lastMessage.parts, { signal: options.signal });
173
- const response = result.response;
174
-
175
- let content = '';
176
251
  const toolCalls = [];
177
-
178
- for (const candidate of response.candidates || []) {
179
- for (const part of candidate.content?.parts || []) {
180
- if (part.text) content += part.text;
181
- if (part.functionCall) {
182
- toolCalls.push({
183
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
184
- type: 'function',
185
- function: {
186
- name: part.functionCall.name,
187
- arguments: JSON.stringify(part.functionCall.args || {}),
188
- thought_signature: part.thoughtSignature
189
- }
190
- });
191
- }
192
- }
193
- }
194
-
195
- const usage = response.usageMetadata;
252
+ const content = collectResponseParts(response, toolCalls);
196
253
  return {
197
254
  content,
198
255
  toolCalls,
199
256
  finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
200
- usage: usage ? {
201
- inputTokens: usage.promptTokenCount || 0,
202
- outputTokens: usage.candidatesTokenCount || 0,
203
- reasoningTokens: usage.thoughtsTokenCount || 0,
204
- cachedReadTokens: usage.cachedContentTokenCount || 0,
205
- cacheWriteTokens: 0,
206
- promptTokens: usage.promptTokenCount || 0,
207
- completionTokens: usage.candidatesTokenCount || 0,
208
- totalTokens: usage.totalTokenCount || 0
209
- } : null,
257
+ usage: normalizeUsage(response.usageMetadata),
210
258
  model
211
259
  };
212
260
  }
@@ -214,60 +262,32 @@ class GoogleProvider extends BaseProvider {
214
262
  async *stream(messages, tools = [], options = {}) {
215
263
  const model = options.model || this.config.model || this.getDefaultModel();
216
264
  const { systemInstruction, history } = this.convertMessages(messages);
217
-
218
- const genModel = this.genAI.getGenerativeModel({
265
+ const responseStream = await this.genAI.models.generateContentStream({
219
266
  model,
220
- systemInstruction: systemInstruction || undefined,
221
- tools: tools.length > 0 ? this.formatTools(tools) : undefined
267
+ contents: history,
268
+ config: this.buildGenerateConfig(systemInstruction, tools, options)
222
269
  });
223
-
224
- const lastMessage = history.pop();
225
- const chat = genModel.startChat({ history });
226
- const result = await chat.sendMessageStream(lastMessage.parts, { signal: options.signal });
227
-
228
270
  let content = '';
229
271
  const toolCalls = [];
272
+ const seenToolCalls = new Set();
273
+ let usage = null;
230
274
 
231
- for await (const chunk of result.stream) {
232
- for (const candidate of chunk.candidates || []) {
233
- for (const part of candidate.content?.parts || []) {
234
- if (part.text) {
235
- content += part.text;
236
- yield { type: 'content', content: part.text };
237
- }
238
- if (part.functionCall) {
239
- toolCalls.push({
240
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
241
- type: 'function',
242
- function: {
243
- name: part.functionCall.name,
244
- arguments: JSON.stringify(part.functionCall.args || {}),
245
- thought_signature: part.thoughtSignature
246
- }
247
- });
248
- }
249
- }
275
+ for await (const chunk of responseStream) {
276
+ const chunkContent = collectResponseParts(chunk, toolCalls, seenToolCalls);
277
+ if (chunkContent) {
278
+ content += chunkContent;
279
+ yield { type: 'content', content: chunkContent };
250
280
  }
281
+ if (chunk.usageMetadata) usage = chunk.usageMetadata;
251
282
  }
252
283
 
253
- const finalResponse = await result.response;
254
- const usage = finalResponse.usageMetadata;
255
-
256
284
  yield {
257
285
  type: 'done',
258
286
  content,
259
287
  toolCalls,
260
288
  finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
261
- usage: usage ? {
262
- inputTokens: usage.promptTokenCount || 0,
263
- outputTokens: usage.candidatesTokenCount || 0,
264
- reasoningTokens: usage.thoughtsTokenCount || 0,
265
- cachedReadTokens: usage.cachedContentTokenCount || 0,
266
- cacheWriteTokens: 0,
267
- promptTokens: usage.promptTokenCount || 0,
268
- completionTokens: usage.candidatesTokenCount || 0,
269
- totalTokens: usage.totalTokenCount || 0
270
- } : null
289
+ usage: normalizeUsage(usage),
290
+ model
271
291
  };
272
292
  }
273
293
  }
@@ -10,7 +10,13 @@ function normalizeReply(content) {
10
10
  }
11
11
 
12
12
  function hasExternalBlocker(text) {
13
- return /\b(?:blocked|cannot|can't|could not|couldn't|unable to|do not have access|don't have access|missing (?:access|credentials|permission|information)|need you to|requires? your|waiting for your|please (?:provide|send|choose|confirm|authorize|approve))\b/.test(text);
13
+ return /\b(?:blocked|cannot|can't|could not|couldn't|unable to|do not have access|don't have access|missing (?:access|credentials|permission|information)|need you to|requires? your|waiting for your|please (?:provide|send|choose|confirm|authorize|approve)|blockiert|kann nicht|konnte nicht|mir fehlt|ich brauche von dir|warte auf dein(?:e|en)?|bitte (?:gib|schick|sende|nenn|bestätige|waehle|wähle|genehmige))\b/.test(text);
14
+ }
15
+
16
+ function isTerminalQuestionOrBlockerReply(content) {
17
+ const text = normalizeReply(content);
18
+ if (!text) return false;
19
+ return /[??]/.test(text) || hasExternalBlocker(text);
14
20
  }
15
21
 
16
22
  /**
@@ -36,10 +42,16 @@ function isDeferredWorkReply(content) {
36
42
  /^(?:working on it|checking now|on it)[.!…]*$/,
37
43
  /\bi(?:'ll| will)\s+(?:get back to you|update you|report back|let you know|keep you posted)\b/,
38
44
  /\b(?:i(?:'ll| will)\s+follow up|stay tuned)\b/,
45
+ /^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?ich\s+(?:(?:arbeite|pruefe|prüfe|schaue|untersuche|teste|debugge|starte)\s+(?:(?:gerade|aktuell|jetzt|noch)\b|(?:das|dies|die logs?|den code|deine anfrage|deinen auftrag)\b)|(?:kuemmere|kümmere)\s+mich\s+(?:gerade|aktuell|jetzt|noch)\b)/,
46
+ /^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?ich\s+(?:werde|wuerde|würde)\s+(?:jetzt\s+)?(?:pruefen|prüfen|nachsehen|untersuchen|testen|debuggen|starten|fixen|erledigen)\b/,
47
+ /^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?(?:lass|lasst)\s+mich\s+(?:(?:das|dies|die logs?|den code)\s+)?(?:pruefen|prüfen|nachsehen|untersuchen|testen|debuggen)\b/,
48
+ /^(?:gib mir|gebt mir)\s+(?:einen\s+)?(?:moment|augenblick)|^(?:bin dran|mache ich)[.!…]*$/,
49
+ /\bich\s+(?:melde mich|gebe dir bescheid|halte dich auf dem laufenden)\b/,
39
50
  ];
40
51
  return patterns.some((pattern) => pattern.test(text));
41
52
  }
42
53
 
43
54
  module.exports = {
44
55
  isDeferredWorkReply,
56
+ isTerminalQuestionOrBlockerReply,
45
57
  };
@@ -306,6 +306,15 @@ class DockerVMManager {
306
306
  return Boolean(session && isContainerRunning(session.containerId));
307
307
  }
308
308
 
309
+ // A side-effect-free snapshot for request-path capability reporting. Unlike
310
+ // hasVm(), this deliberately avoids a synchronous `docker inspect`, which can
311
+ // block the Node event loop. Tool execution still calls ensureVm() and performs
312
+ // the authoritative liveness check before using a tracked session.
313
+ hasTrackedVm(userId) {
314
+ const key = String(userId || '').trim();
315
+ return Boolean(key && this.instances.has(key));
316
+ }
317
+
309
318
  // Used by validation.js — cached to avoid docker calls on every status poll.
310
319
  getReadiness() {
311
320
  const now = Date.now();
@@ -13,6 +13,8 @@ const GUEST_PAYLOAD_PROFILES = Object.freeze({
13
13
  { source: 'runtime/paths.js', target: 'runtime/paths.js' },
14
14
  { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
15
15
  { source: 'server/services/browser', target: 'server/services/browser' },
16
+ { source: 'server/services/android/process.js', target: 'server/services/android/process.js' },
17
+ { source: 'server/utils/cloud-security.js', target: 'server/utils/cloud-security.js' },
16
18
  ],
17
19
  cli: [
18
20
  { source: 'server/guest-agent.cli.package.json', target: 'package.json' },
@@ -28,6 +30,8 @@ const GUEST_PAYLOAD_PROFILES = Object.freeze({
28
30
  { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
29
31
  { source: 'server/services/cli', target: 'server/services/cli' },
30
32
  { source: 'server/services/browser', target: 'server/services/browser' },
33
+ { source: 'server/services/android/process.js', target: 'server/services/android/process.js' },
34
+ { source: 'server/utils/cloud-security.js', target: 'server/utils/cloud-security.js' },
31
35
  ],
32
36
  android: [
33
37
  { source: 'server/guest-agent.android.package.json', target: 'package.json' },
@@ -36,6 +40,9 @@ const GUEST_PAYLOAD_PROFILES = Object.freeze({
36
40
  { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
37
41
  { source: 'server/services/cli', target: 'server/services/cli' },
38
42
  { source: 'server/services/android', target: 'server/services/android' },
43
+ { source: 'server/utils/abort.js', target: 'server/utils/abort.js' },
44
+ { source: 'server/utils/cloud-security.js', target: 'server/utils/cloud-security.js' },
45
+ { source: 'server/utils/image_payload.js', target: 'server/utils/image_payload.js' },
39
46
  ],
40
47
  });
41
48
 
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
- const { spawnSync } = require('child_process');
6
+ const { spawn, spawnSync } = require('child_process');
7
7
  const { DATA_DIR } = require('../../../runtime/paths');
8
8
  const { stageGuestPayload, normalizeRuntimeProfile } = require('./guest_bootstrap');
9
9
 
@@ -105,6 +105,34 @@ function dockerAvailable() {
105
105
  return !result.error && result.status === 0;
106
106
  }
107
107
 
108
+ function runDockerCommand(args, options = {}) {
109
+ const timeoutMs = Number(options.timeout || BUILD_TIMEOUT_MS);
110
+ const spawnImpl = options.spawnImpl || spawn;
111
+ return new Promise((resolve, reject) => {
112
+ const child = spawnImpl('docker', args, {
113
+ stdio: options.stdio || ['ignore', 'inherit', 'inherit'],
114
+ windowsHide: true,
115
+ });
116
+ let settled = false;
117
+ const finish = (error, status = null) => {
118
+ if (settled) return;
119
+ settled = true;
120
+ clearTimeout(timeout);
121
+ if (error) reject(error);
122
+ else resolve(status);
123
+ };
124
+ const timeout = setTimeout(() => {
125
+ const error = new Error(`docker ${args[0]} timed out after ${timeoutMs}ms`);
126
+ error.code = 'DOCKER_COMMAND_TIMEOUT';
127
+ try { child.kill('SIGKILL'); } catch {}
128
+ finish(error);
129
+ }, timeoutMs);
130
+ timeout.unref?.();
131
+ child.once('error', (error) => finish(error));
132
+ child.once('close', (status) => finish(null, status));
133
+ });
134
+ }
135
+
108
136
  // Builds and caches the per-profile guest-agent Docker image. The image bakes in
109
137
  // the guest agent, its dependencies, and (for browser_cli) the Chromium browser,
110
138
  // so containers start the agent directly with no runtime installation step.
@@ -166,15 +194,16 @@ class GuestImageBuilder {
166
194
  async #build(tag) {
167
195
  console.log(`[GuestImage:${this.profile}] Building guest image ${tag} (one-time; downloads browser + deps)…`);
168
196
  const started = Date.now();
169
- const result = docker(['build', '-t', tag, this.contextDir], {
170
- timeout: BUILD_TIMEOUT_MS,
171
- stdio: ['ignore', 'inherit', 'inherit'],
172
- });
173
- if (result.error) {
174
- throw new Error(`Guest image build failed to start: ${result.error.message}`);
197
+ let status;
198
+ try {
199
+ status = await runDockerCommand(['build', '-t', tag, this.contextDir], {
200
+ timeout: BUILD_TIMEOUT_MS,
201
+ });
202
+ } catch (error) {
203
+ throw new Error(`Guest image build failed: ${error.message}`, { cause: error });
175
204
  }
176
- if (result.status !== 0) {
177
- throw new Error(`Guest image build for ${tag} exited with status ${result.status}`);
205
+ if (status !== 0) {
206
+ throw new Error(`Guest image build for ${tag} exited with status ${status}`);
178
207
  }
179
208
  this.#cachedBuiltAt = Date.now();
180
209
  console.log(`[GuestImage:${this.profile}] Built ${tag} in ${Math.round((Date.now() - started) / 1000)}s`);
@@ -185,5 +214,6 @@ class GuestImageBuilder {
185
214
  module.exports = {
186
215
  GuestImageBuilder,
187
216
  dockerAvailable,
217
+ runDockerCommand,
188
218
  BASE_IMAGE,
189
219
  };
@@ -87,6 +87,41 @@ class RuntimeManager {
87
87
  );
88
88
  }
89
89
 
90
+ getCapabilitySnapshot(userId) {
91
+ const settings = this.getSettings(userId) || {};
92
+ const key = String(userId || '').trim();
93
+ const extensionConnected = this.hasActiveExtensionBrowser(userId);
94
+ const androidController = key && this.androidControllers instanceof Map
95
+ ? this.androidControllers.get(key)
96
+ : null;
97
+ let androidStatus = null;
98
+ if (androidController && typeof androidController.getStatusSync === 'function') {
99
+ try {
100
+ androidStatus = androidController.getStatusSync();
101
+ } catch {
102
+ androidStatus = null;
103
+ }
104
+ }
105
+
106
+ return {
107
+ browser: {
108
+ preferredBackend: settings.browser_backend,
109
+ activeBackend: extensionConnected ? 'extension' : 'vm',
110
+ extensionConnected,
111
+ vmInitialized: Boolean(
112
+ this.browserBackend?.vmManager?.hasTrackedVm?.(userId),
113
+ ),
114
+ },
115
+ android: {
116
+ initialized: Boolean(androidController),
117
+ status: androidStatus,
118
+ },
119
+ desktop: {
120
+ connected: this.hasActiveDesktopCompanion(userId),
121
+ },
122
+ };
123
+ }
124
+
90
125
  resolveBackend(userId, requested) {
91
126
  void userId;
92
127
  return requested === 'browser' ? this.browserBackend : this.cliBackend;