neoagent 2.5.2-beta.2 → 2.5.2-beta.21

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 (33) hide show
  1. package/package.json +1 -1
  2. package/server/public/.last_build_id +1 -1
  3. package/server/public/flutter_bootstrap.js +1 -1
  4. package/server/public/main.dart.js +4 -4
  5. package/server/services/ai/deliverables/artifact_helpers.js +109 -16
  6. package/server/services/ai/deliverables/selector.js +17 -0
  7. package/server/services/ai/engine.js +2 -4312
  8. package/server/services/ai/loop/agent_engine_core.js +1590 -0
  9. package/server/services/ai/loop/blank_recovery.js +37 -0
  10. package/server/services/ai/loop/callbacks.js +151 -0
  11. package/server/services/ai/loop/completion_judge.js +252 -0
  12. package/server/services/ai/loop/conversation_loop.js +2398 -0
  13. package/server/services/ai/loop/delivery_state.js +27 -0
  14. package/server/services/ai/loop/error_recovery.js +38 -0
  15. package/server/services/ai/loop/iteration_budget.js +24 -0
  16. package/server/services/ai/loop/messaging_delivery.js +296 -0
  17. package/server/services/ai/loop/model_io.js +258 -0
  18. package/server/services/ai/loop/progress_classification.js +177 -0
  19. package/server/services/ai/loop/progress_monitor.js +81 -0
  20. package/server/services/ai/loop/run_state.js +356 -0
  21. package/server/services/ai/loop/tool_dispatch.js +230 -0
  22. package/server/services/ai/loopPolicy.js +13 -6
  23. package/server/services/ai/messagingFallback.js +20 -0
  24. package/server/services/ai/repetitionGuard.js +47 -2
  25. package/server/services/ai/systemPrompt.js +5 -0
  26. package/server/services/ai/taskAnalysis.js +6 -0
  27. package/server/services/ai/toolEvidence.js +8 -1
  28. package/server/services/ai/tools.js +82 -11
  29. package/server/services/integrations/github/repos.js +39 -29
  30. package/server/services/messaging/manager.js +7 -0
  31. package/server/services/runtime/backends/local-vm.js +7 -7
  32. package/server/services/runtime/docker-vm-manager.js +21 -1
  33. package/server/services/workspace/manager.js +1 -0
@@ -490,29 +490,6 @@ const githubToolDefinitions = [
490
490
  required: ['owner_repo'],
491
491
  },
492
492
  },
493
- {
494
- name: 'github_get_content',
495
- access: 'read',
496
- description: 'Get file or directory contents from a repository.',
497
- parameters: {
498
- type: 'object',
499
- properties: {
500
- owner_repo: {
501
- type: 'string',
502
- description: 'Repository in format "owner/repo".',
503
- },
504
- path: {
505
- type: 'string',
506
- description: 'File or directory path.',
507
- },
508
- ref: {
509
- type: 'string',
510
- description: 'Git ref (branch, tag, or SHA).',
511
- },
512
- },
513
- required: ['owner_repo', 'path'],
514
- },
515
- },
516
493
  {
517
494
  name: 'github_create_or_update_file',
518
495
  access: 'write',
@@ -717,7 +694,7 @@ const githubToolDefinitions = [
717
694
  {
718
695
  name: 'github_api_request',
719
696
  access: 'dynamic_http_method',
720
- description: 'Make an authenticated GitHub API request for advanced operations not covered by dedicated tools.',
697
+ description: 'Make an authenticated GitHub API request for advanced operations not covered by dedicated tools. Path must be the FULL API path starting with /repos/{owner}/{repo}/... — e.g. /repos/NeoLabs-Systems/NeoAgent/git/trees/main?recursive=1. Alternatively, supply owner_repo and a relative path like /git/trees/main and the prefix is prepended automatically.',
721
698
  parameters: {
722
699
  type: 'object',
723
700
  properties: {
@@ -728,7 +705,19 @@ const githubToolDefinitions = [
728
705
  },
729
706
  path: {
730
707
  type: 'string',
731
- description: 'API path or full URL.',
708
+ description: 'Full API path (e.g. /repos/owner/repo/git/trees/main) or a relative path like /git/trees/main when owner_repo is also provided.',
709
+ },
710
+ owner_repo: {
711
+ type: 'string',
712
+ description: 'Repository in "owner/repo" format. When provided together with a relative path, the /repos/{owner}/{repo} prefix is automatically prepended.',
713
+ },
714
+ endpoint: {
715
+ type: 'string',
716
+ description: 'Alias for path.',
717
+ },
718
+ url: {
719
+ type: 'string',
720
+ description: 'Full GitHub API URL (https://api.github.com/...).',
732
721
  },
733
722
  query: {
734
723
  type: 'object',
@@ -739,7 +728,7 @@ const githubToolDefinitions = [
739
728
  description: 'Optional JSON request body.',
740
729
  },
741
730
  },
742
- required: ['method', 'path'],
731
+ required: ['method'],
743
732
  },
744
733
  },
745
734
  ];
@@ -749,6 +738,14 @@ function parseCommaSeparatedList(value) {
749
738
  return String(value).split(',').map((s) => s.trim()).filter(Boolean);
750
739
  }
751
740
 
741
+ function resolveApiRequestPath(args = {}) {
742
+ for (const key of ['path', 'endpoint', 'url']) {
743
+ const value = args?.[key];
744
+ if (typeof value === 'string' && value.trim()) return value.trim();
745
+ }
746
+ return '';
747
+ }
748
+
752
749
  async function executeGithubTool(toolName, args, auth) {
753
750
  switch (toolName) {
754
751
  case 'github_get_auth_user': {
@@ -961,10 +958,15 @@ async function executeGithubTool(toolName, args, auth) {
961
958
  const { owner, repo } = parseOwnerRepo(args.owner_repo);
962
959
  const query = {};
963
960
  if (args.ref) query.ref = String(args.ref);
964
- return await githubApiRequest(auth, {
961
+ const result = await githubApiRequest(auth, {
965
962
  path: `/repos/${owner}/${repo}/contents/${String(args.path || '')}`,
966
963
  query,
967
964
  });
965
+ if (result && result.encoding === 'base64' && typeof result.content === 'string') {
966
+ result.content = Buffer.from(result.content.replace(/\n/g, ''), 'base64').toString('utf8');
967
+ result.encoding = 'utf8';
968
+ }
969
+ return result;
968
970
  }
969
971
 
970
972
  case 'github_create_or_update_file': {
@@ -1085,7 +1087,10 @@ async function executeGithubTool(toolName, args, auth) {
1085
1087
 
1086
1088
  case 'github_api_request': {
1087
1089
  let baseUrl = 'https://api.github.com';
1088
- let path = String(args.path || '');
1090
+ let path = resolveApiRequestPath(args);
1091
+ if (!path) {
1092
+ throw new Error('github_api_request requires path, endpoint, or url.');
1093
+ }
1089
1094
  let query = args.query || null;
1090
1095
  if (path.startsWith('http')) {
1091
1096
  const url = new URL(path);
@@ -1103,6 +1108,11 @@ async function executeGithubTool(toolName, args, auth) {
1103
1108
  ...parsedQuery,
1104
1109
  ...(args.query && typeof args.query === 'object' ? args.query : {}),
1105
1110
  };
1111
+ } else if (args.owner_repo && !path.startsWith('/repos/') && !path.startsWith('/user') && !path.startsWith('/orgs/') && !path.startsWith('/search/')) {
1112
+ // Convenience: prepend /repos/{owner}/{repo} for relative paths
1113
+ const { owner, repo } = parseOwnerRepo(args.owner_repo);
1114
+ const relativePath = path.startsWith('/') ? path : `/${path}`;
1115
+ path = `/repos/${owner}/${repo}${relativePath}`;
1106
1116
  }
1107
1117
  return await githubApiRequest(auth, {
1108
1118
  method: args.method || 'GET',
@@ -1121,4 +1131,4 @@ async function executeGithubTool(toolName, args, auth) {
1121
1131
  module.exports = {
1122
1132
  executeGithubTool,
1123
1133
  githubToolDefinitions,
1124
- };
1134
+ };
@@ -515,6 +515,13 @@ class MessagingManager extends EventEmitter {
515
515
  }
516
516
 
517
517
  const result = await platform.sendMessage(to, normalizedContent, sendOptions);
518
+ if (result?.success === false) {
519
+ const reason = result.error || result.reason || 'platform rejected the message';
520
+ const error = new Error(`Platform ${platformName} delivery failed: ${reason}`);
521
+ error.code = 'MESSAGING_DELIVERY_FAILED';
522
+ error.deliveryResult = result;
523
+ throw error;
524
+ }
518
525
 
519
526
  db.prepare('INSERT INTO messages (user_id, agent_id, run_id, role, content, platform, platform_chat_id, media_path, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
520
527
  .run(userId, agentId, runId, 'assistant', normalizedContent, platformName, to, mediaPath, metadata ? JSON.stringify(metadata) : null);
@@ -250,8 +250,8 @@ class VmBrowserProvider {
250
250
  async typeText(text, options = {}) { return this.#materialize(await this.client.request('POST', '/browser/type-text', { text, ...options })); }
251
251
  async pressKey(key, screenshot = true) { return this.#materialize(await this.client.request('POST', '/browser/press-key', { key, screenshot })); }
252
252
  async scroll(deltaX, deltaY, screenshot = true) { return this.#materialize(await this.client.request('POST', '/browser/scroll', { deltaX, deltaY, screenshot })); }
253
- extract(selector, attribute, all = false) { return this.client.request('POST', '/browser/extract', { selector, attribute, all }); }
254
- evaluate(script) { return this.client.request('POST', '/browser/execute', { code: script }); }
253
+ async extract(selector, attribute, all = false) { return this.client.request('POST', '/browser/extract', { selector, attribute, all }); }
254
+ async evaluate(script) { return this.client.request('POST', '/browser/execute', { code: script }); }
255
255
  async screenshot(options = {}) { return this.#materialize(await this.client.request('POST', '/browser/screenshot', options)); }
256
256
  async screenshotJpeg(quality = 80, options = {}) {
257
257
  const result = await this.client.request('POST', '/browser/screenshot-jpeg', { ...options, quality });
@@ -259,11 +259,11 @@ class VmBrowserProvider {
259
259
  if (!content) throw new Error('VM browser screenshot-jpeg returned no data.');
260
260
  return Buffer.from(content, 'base64');
261
261
  }
262
- launch(options = {}) { return this.client.request('POST', '/browser/launch', options); }
263
- closeBrowser() { return this.client.request('POST', '/browser/close'); }
264
- fill(selector, value) { return this.type(selector, value); }
265
- extractContent(options = {}) { return this.client.request('POST', '/browser/extract', options); }
266
- executeJS(code) { return this.evaluate(code); }
262
+ async launch(options = {}) { return this.client.request('POST', '/browser/launch', options); }
263
+ async closeBrowser() { return this.client.request('POST', '/browser/close'); }
264
+ async fill(selector, value) { return this.type(selector, value); }
265
+ async extractContent(options = {}) { return this.client.request('POST', '/browser/extract', options); }
266
+ async executeJS(code) { return this.evaluate(code); }
267
267
  async getPageInfo() {
268
268
  const status = await this.client.request('GET', '/browser/status');
269
269
  this.headless = status?.headless !== false;
@@ -1,11 +1,25 @@
1
1
  'use strict';
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
4
6
  const http = require('http');
5
7
  const net = require('net');
8
+ const { AGENT_DATA_DIR } = require('../../../runtime/paths');
9
+ const { sanitizeWorkspaceKey } = require('../workspace/manager');
6
10
 
7
11
  const CONTAINER_IMAGE = 'mcr.microsoft.com/playwright:v1.44.0-focal';
8
12
  const CONTAINER_LABEL = 'neoagent.managed=1';
13
+ // The per-user host workspace is bind-mounted here so the shell (execute_command)
14
+ // and the workspace file tools (read_file/write_file/list_directory/search_files)
15
+ // operate on the SAME files. The guest agent defaults its shell cwd to this path.
16
+ const GUEST_WORKSPACE = '/workspace';
17
+
18
+ // Host path of a user's workspace — must match WorkspaceManager's layout exactly
19
+ // (AGENT_DATA_DIR/workspaces/<sanitized key>) so both sides see one directory.
20
+ function hostWorkspaceDir(key) {
21
+ return path.join(AGENT_DATA_DIR, 'workspaces', sanitizeWorkspaceKey(key));
22
+ }
9
23
 
10
24
  // ─── Guest agent ─────────────────────────────────────────────────────────────
11
25
  // Injected into every container. Pure Node.js — only built-in modules + playwright
@@ -104,7 +118,7 @@ const server = http.createServer(async (req, res) => {
104
118
  if (req.method === 'POST' && url === '/exec') {
105
119
  const timeoutMs = Math.min(Number(b.timeout) || 15 * 60 * 1000, 20 * 60 * 1000);
106
120
  const child = spawn('sh', ['-c', b.command || 'true'], {
107
- cwd: b.cwd || '/tmp',
121
+ cwd: b.cwd || '/workspace',
108
122
  env: { ...process.env, ...b.env },
109
123
  });
110
124
  const pid = child.pid;
@@ -325,6 +339,10 @@ class DockerVMManager {
325
339
  const port = await findAvailablePort();
326
340
  console.log(`[DockerVM:${this.profile}] Starting container for user ${key} on port ${port}`);
327
341
 
342
+ // Bind-mount the user's host workspace so shell and file tools share one place.
343
+ const workspaceDir = hostWorkspaceDir(key);
344
+ try { fs.mkdirSync(workspaceDir, { recursive: true }); } catch { /* best effort */ }
345
+
328
346
  const containerId = docker([
329
347
  'run', '-d',
330
348
  '--memory', `${this.memoryMb}m`,
@@ -334,6 +352,8 @@ class DockerVMManager {
334
352
  ...(this.guestToken ? ['-e', `NEOAGENT_VM_GUEST_TOKEN=${this.guestToken}`] : []),
335
353
  '--shm-size=2g',
336
354
  '--security-opt', 'no-new-privileges',
355
+ '-v', `${workspaceDir}:${GUEST_WORKSPACE}`,
356
+ '-w', GUEST_WORKSPACE,
337
357
  '--label', CONTAINER_LABEL,
338
358
  '--label', `neoagent.profile=${this.profile}`,
339
359
  '--label', `neoagent.user=${key}`,
@@ -414,4 +414,5 @@ class WorkspaceManager {
414
414
 
415
415
  module.exports = {
416
416
  WorkspaceManager,
417
+ sanitizeWorkspaceKey,
417
418
  };