nexus-ai-pro 2.5.0 → 2.7.0

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 (131) hide show
  1. package/API_STABILITY.md +88 -0
  2. package/CHANGELOG.md +220 -1
  3. package/README.md +13 -4
  4. package/dist/agent/create-agent.d.ts +37 -2
  5. package/dist/agent/create-agent.js +77 -6
  6. package/dist/agent/glob.d.ts +24 -0
  7. package/dist/agent/glob.js +72 -0
  8. package/dist/agent/index.d.ts +3 -1
  9. package/dist/agent/index.js +1 -0
  10. package/dist/agent/interpreter.d.ts +125 -0
  11. package/dist/agent/interpreter.js +270 -0
  12. package/dist/agent/sandbox-container.d.ts +48 -0
  13. package/dist/agent/sandbox-container.js +291 -0
  14. package/dist/agent/sandbox.d.ts +50 -1
  15. package/dist/agent/sandbox.js +155 -2
  16. package/dist/agent/tool-search.d.ts +39 -0
  17. package/dist/agent/tool-search.js +113 -0
  18. package/dist/agent/tool.d.ts +21 -4
  19. package/dist/agent/tool.js +28 -4
  20. package/dist/context/window.js +4 -0
  21. package/dist/deep-agent/helpers.d.ts +57 -0
  22. package/dist/deep-agent/helpers.js +235 -0
  23. package/dist/deep-agent/index.d.ts +20 -1
  24. package/dist/deep-agent/index.js +69 -51
  25. package/dist/evaluate/generate.d.ts +171 -0
  26. package/dist/evaluate/generate.js +266 -0
  27. package/dist/evaluate/index.d.ts +1 -1
  28. package/dist/evaluate/index.js +1 -1
  29. package/dist/evaluate/online.d.ts +114 -0
  30. package/dist/evaluate/online.js +261 -0
  31. package/dist/evaluate/run.d.ts +28 -0
  32. package/dist/evaluate/run.js +56 -5
  33. package/dist/graph/event-stream.js +2 -0
  34. package/dist/graph/graph.d.ts +7 -1
  35. package/dist/graph/graph.js +16 -2
  36. package/dist/mcp/client.d.ts +9 -1
  37. package/dist/mcp/client.js +38 -8
  38. package/dist/mcp/server.js +42 -2
  39. package/dist/operations/runner.js +1 -1
  40. package/dist/protocols/a2a.d.ts +166 -1
  41. package/dist/protocols/a2a.js +499 -132
  42. package/dist/protocols/acp-editor.d.ts +43 -0
  43. package/dist/protocols/acp-editor.js +164 -0
  44. package/dist/protocols/acp.d.ts +76 -3
  45. package/dist/protocols/acp.js +180 -7
  46. package/dist/protocols/ag-ui.d.ts +23 -0
  47. package/dist/protocols/ag-ui.js +128 -8
  48. package/dist/providers/anthropic.d.ts +6 -0
  49. package/dist/providers/anthropic.js +36 -9
  50. package/dist/providers/content.d.ts +33 -1
  51. package/dist/providers/content.js +62 -0
  52. package/dist/providers/google.js +14 -5
  53. package/dist/providers/openai.d.ts +8 -4
  54. package/dist/providers/openai.js +75 -34
  55. package/dist/runtime/index.d.ts +1 -1
  56. package/dist/security/index.d.ts +1 -1
  57. package/dist/security/index.js +1 -1
  58. package/dist/security/injection-detector.d.ts +18 -0
  59. package/dist/security/injection-detector.js +39 -4
  60. package/dist/security/schema-validator.js +11 -2
  61. package/dist/tracing/instrument.js +14 -3
  62. package/dist/types/graph.d.ts +7 -0
  63. package/dist/types/messages.d.ts +69 -3
  64. package/dist/utils/tokenizer.js +4 -0
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/dist-cjs/agent/create-agent.d.ts +37 -2
  68. package/dist-cjs/agent/create-agent.js +76 -5
  69. package/dist-cjs/agent/glob.d.ts +24 -0
  70. package/dist-cjs/agent/glob.js +77 -0
  71. package/dist-cjs/agent/index.d.ts +3 -1
  72. package/dist-cjs/agent/index.js +5 -1
  73. package/dist-cjs/agent/interpreter.d.ts +125 -0
  74. package/dist-cjs/agent/interpreter.js +308 -0
  75. package/dist-cjs/agent/sandbox-container.d.ts +48 -0
  76. package/dist-cjs/agent/sandbox-container.js +327 -0
  77. package/dist-cjs/agent/sandbox.d.ts +50 -1
  78. package/dist-cjs/agent/sandbox.js +155 -2
  79. package/dist-cjs/agent/tool-search.d.ts +39 -0
  80. package/dist-cjs/agent/tool-search.js +118 -0
  81. package/dist-cjs/agent/tool.d.ts +21 -4
  82. package/dist-cjs/agent/tool.js +28 -4
  83. package/dist-cjs/context/window.js +4 -0
  84. package/dist-cjs/deep-agent/helpers.d.ts +57 -0
  85. package/dist-cjs/deep-agent/helpers.js +238 -0
  86. package/dist-cjs/deep-agent/index.d.ts +20 -1
  87. package/dist-cjs/deep-agent/index.js +68 -50
  88. package/dist-cjs/evaluate/generate.d.ts +171 -0
  89. package/dist-cjs/evaluate/generate.js +272 -0
  90. package/dist-cjs/evaluate/index.d.ts +1 -1
  91. package/dist-cjs/evaluate/index.js +2 -1
  92. package/dist-cjs/evaluate/online.d.ts +114 -0
  93. package/dist-cjs/evaluate/online.js +264 -0
  94. package/dist-cjs/evaluate/run.d.ts +28 -0
  95. package/dist-cjs/evaluate/run.js +58 -5
  96. package/dist-cjs/graph/event-stream.js +2 -0
  97. package/dist-cjs/graph/graph.d.ts +7 -1
  98. package/dist-cjs/graph/graph.js +17 -2
  99. package/dist-cjs/mcp/client.d.ts +9 -1
  100. package/dist-cjs/mcp/client.js +37 -7
  101. package/dist-cjs/mcp/server.js +42 -2
  102. package/dist-cjs/operations/runner.js +1 -1
  103. package/dist-cjs/protocols/a2a.d.ts +166 -1
  104. package/dist-cjs/protocols/a2a.js +501 -132
  105. package/dist-cjs/protocols/acp-editor.d.ts +43 -0
  106. package/dist-cjs/protocols/acp-editor.js +167 -0
  107. package/dist-cjs/protocols/acp.d.ts +76 -3
  108. package/dist-cjs/protocols/acp.js +183 -8
  109. package/dist-cjs/protocols/ag-ui.d.ts +23 -0
  110. package/dist-cjs/protocols/ag-ui.js +127 -7
  111. package/dist-cjs/providers/anthropic.d.ts +6 -0
  112. package/dist-cjs/providers/anthropic.js +35 -8
  113. package/dist-cjs/providers/content.d.ts +33 -1
  114. package/dist-cjs/providers/content.js +69 -0
  115. package/dist-cjs/providers/google.js +13 -4
  116. package/dist-cjs/providers/openai.d.ts +8 -4
  117. package/dist-cjs/providers/openai.js +74 -33
  118. package/dist-cjs/runtime/index.d.ts +1 -1
  119. package/dist-cjs/security/index.d.ts +1 -1
  120. package/dist-cjs/security/index.js +2 -1
  121. package/dist-cjs/security/injection-detector.d.ts +18 -0
  122. package/dist-cjs/security/injection-detector.js +40 -4
  123. package/dist-cjs/security/schema-validator.js +11 -2
  124. package/dist-cjs/tracing/instrument.js +14 -3
  125. package/dist-cjs/types/graph.d.ts +7 -0
  126. package/dist-cjs/types/messages.d.ts +69 -3
  127. package/dist-cjs/utils/tokenizer.js +4 -0
  128. package/dist-cjs/version.d.ts +1 -1
  129. package/dist-cjs/version.js +1 -1
  130. package/llms.txt +1 -1
  131. package/package.json +69 -2
@@ -0,0 +1,291 @@
1
+ import { GREP_FILE_BYTES, GREP_TOTAL_BYTES, globMatcher, matchingLines, WALK_LIMIT } from './glob.js';
2
+ import { SandboxPathError, } from './sandbox.js';
3
+ export function containerSandbox(options) {
4
+ const binary = options.binary ?? 'docker';
5
+ const workdir = `/${(options.workdir ?? '/workspace').replace(/^\/+|\/+$/g, '')}`;
6
+ const maxOutput = options.maxOutputBytes ?? 1_048_576;
7
+ const network = options.network ?? false;
8
+ const label = `nexus-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
9
+ let marks = 0;
10
+ const inside = (file) => {
11
+ const unified = file.replace(/\\/g, '/');
12
+ if (unified.includes('\0'))
13
+ throw new SandboxPathError(file);
14
+ const parts = [];
15
+ for (const segment of (unified.startsWith('/') ? unified : `${workdir}/${unified}`).split('/')) {
16
+ if (segment === '' || segment === '.')
17
+ continue;
18
+ if (segment !== '..')
19
+ parts.push(segment);
20
+ else if (parts.pop() === undefined)
21
+ throw new SandboxPathError(file);
22
+ }
23
+ const resolved = `/${parts.join('/')}`;
24
+ if (resolved !== workdir && !resolved.startsWith(`${workdir}/`))
25
+ throw new SandboxPathError(file);
26
+ return resolved;
27
+ };
28
+ const fromRoot = (path) => (path === workdir ? '.' : path.slice(workdir.length + 1));
29
+ const invoke = async (args, call = {}) => {
30
+ const { spawn } = await import('node:child_process');
31
+ return new Promise((resolve) => {
32
+ const child = spawn(binary, args, { windowsHide: true });
33
+ const out = [];
34
+ let kept = 0;
35
+ let stderr = '';
36
+ let timedOut = false;
37
+ let settled = false;
38
+ child.stdout.on('data', (chunk) => {
39
+ const room = (call.keep ?? Number.POSITIVE_INFINITY) - kept;
40
+ if (room <= 0)
41
+ return;
42
+ const part = chunk.byteLength > room ? chunk.subarray(0, room) : chunk;
43
+ out.push(part);
44
+ kept += part.byteLength;
45
+ });
46
+ child.stderr.on('data', (chunk) => {
47
+ if (stderr.length < maxOutput)
48
+ stderr = (stderr + new TextDecoder().decode(chunk)).slice(0, maxOutput);
49
+ });
50
+ const stop = () => {
51
+ call.onStop?.();
52
+ child.kill('SIGKILL');
53
+ };
54
+ const timer = call.timeoutMs === undefined
55
+ ? undefined
56
+ : setTimeout(() => {
57
+ timedOut = true;
58
+ stop();
59
+ }, call.timeoutMs);
60
+ call.signal?.addEventListener('abort', stop, { once: true });
61
+ const finish = (code) => {
62
+ if (settled)
63
+ return;
64
+ settled = true;
65
+ if (timer)
66
+ clearTimeout(timer);
67
+ call.signal?.removeEventListener('abort', stop);
68
+ resolve({ code: timedOut ? null : code, stdout: concat(out), stderr, timedOut });
69
+ };
70
+ child.on('error', (error) => {
71
+ stderr += error.message;
72
+ finish(null);
73
+ });
74
+ child.on('close', (code) => finish(code));
75
+ child.stdin.on('error', () => undefined);
76
+ child.stdin.end(call.stdin ?? '');
77
+ });
78
+ };
79
+ const failed = (what, result) => new Error(`${what} failed in the container: ${result.stderr.trim() || `exit code ${result.code}`}`);
80
+ let started;
81
+ const container = () => {
82
+ if (started)
83
+ return started;
84
+ const starting = (async () => {
85
+ const result = await invoke([
86
+ 'run',
87
+ '--detach',
88
+ '--rm',
89
+ '--init',
90
+ '--name',
91
+ label,
92
+ '--label',
93
+ 'nexus-ai-pro.sandbox=1',
94
+ '--workdir',
95
+ workdir,
96
+ ...(network === true ? [] : ['--network', network === false ? 'none' : network]),
97
+ '--memory',
98
+ options.memory ?? '512m',
99
+ '--memory-swap',
100
+ options.memory ?? '512m',
101
+ '--cpus',
102
+ String(options.cpus ?? 1),
103
+ '--pids-limit',
104
+ String(options.pids ?? 256),
105
+ '--cap-drop',
106
+ 'ALL',
107
+ '--security-opt',
108
+ 'no-new-privileges',
109
+ ...envArgs(options.env),
110
+ ...(options.runArgs ?? []),
111
+ '--entrypoint',
112
+ 'tail',
113
+ options.image,
114
+ '-f',
115
+ '/dev/null',
116
+ ]);
117
+ if (result.code !== 0)
118
+ throw failed(`Starting ${options.image}`, result);
119
+ return new TextDecoder().decode(result.stdout).trim();
120
+ })();
121
+ started = starting;
122
+ starting.catch(() => {
123
+ if (started === starting)
124
+ started = undefined;
125
+ });
126
+ return starting;
127
+ };
128
+ const shell = async (script, args, stdin, keep) => {
129
+ const id = await container();
130
+ return invoke(['exec', ...(stdin === undefined ? [] : ['--interactive']), id, 'sh', '-c', script, 'sh', ...args], {
131
+ ...(stdin === undefined ? {} : { stdin }),
132
+ ...(keep === undefined ? {} : { keep }),
133
+ });
134
+ };
135
+ const walk = async (directory, filter = '') => {
136
+ const start = inside(directory);
137
+ const result = await shell(`find "$1" \\( -name .git -o -name node_modules \\) -prune -o -type f ${filter} -print`, [start]);
138
+ if (result.code !== 0 && result.stdout.byteLength === 0)
139
+ throw failed('Listing files', result);
140
+ return new TextDecoder()
141
+ .decode(result.stdout)
142
+ .split('\n')
143
+ .filter(Boolean)
144
+ .slice(0, WALK_LIMIT)
145
+ .map((path) => ({ fromRoot: fromRoot(path), fromStart: path.slice(start.length).replace(/^\//, '') }))
146
+ .sort((a, b) => (a.fromRoot < b.fromRoot ? -1 : a.fromRoot > b.fromRoot ? 1 : 0));
147
+ };
148
+ const readBytes = async (file) => {
149
+ const result = await shell('cat "$1"', [inside(file)]);
150
+ if (result.code !== 0)
151
+ throw failed(`Reading ${file}`, result);
152
+ return result.stdout;
153
+ };
154
+ return {
155
+ name: `container (${binary} ${options.image})`,
156
+ isolation: { filesystem: true, network: network === false, processes: true },
157
+ async exec(command, execOptions = {}) {
158
+ const id = await container();
159
+ const startedAt = Date.now();
160
+ const mark = `${label}-${++marks}`;
161
+ const result = await invoke([
162
+ 'exec',
163
+ '--interactive',
164
+ '--workdir',
165
+ inside(execOptions.cwd ?? '.'),
166
+ ...envArgs({ ...options.env, ...execOptions.env, NEXUS_SANDBOX_EXEC: mark }),
167
+ id,
168
+ 'sh',
169
+ '-c',
170
+ command,
171
+ ], {
172
+ stdin: execOptions.stdin ?? '',
173
+ keep: maxOutput,
174
+ timeoutMs: execOptions.timeoutMs ?? options.timeoutMs ?? 30_000,
175
+ ...(execOptions.signal ? { signal: execOptions.signal } : {}),
176
+ onStop: () => {
177
+ void invoke(['exec', id, 'sh', '-c', KILL_MARKED, 'sh', mark]);
178
+ },
179
+ });
180
+ return {
181
+ exitCode: result.code,
182
+ stdout: new TextDecoder().decode(result.stdout),
183
+ stderr: result.stderr,
184
+ timedOut: result.timedOut,
185
+ durationMs: Date.now() - startedAt,
186
+ };
187
+ },
188
+ async readFile(file) {
189
+ return new TextDecoder().decode(await readBytes(file));
190
+ },
191
+ readBytes,
192
+ async writeFile(file, content) {
193
+ const result = await shell('mkdir -p "$(dirname "$1")" && cat > "$1"', [inside(file)], content);
194
+ if (result.code !== 0)
195
+ throw failed(`Writing ${file}`, result);
196
+ },
197
+ async listFiles(directory = '.') {
198
+ const result = await shell('ls -1A "$1"', [inside(directory)]);
199
+ if (result.code !== 0)
200
+ throw failed(`Listing ${directory}`, result);
201
+ return new TextDecoder().decode(result.stdout).split('\n').filter(Boolean).sort();
202
+ },
203
+ async removeFile(file) {
204
+ const target = inside(file);
205
+ if (target === workdir)
206
+ throw new SandboxPathError(file);
207
+ const result = await shell('rm -rf "$1"', [target]);
208
+ if (result.code !== 0)
209
+ throw failed(`Removing ${file}`, result);
210
+ },
211
+ async glob(pattern, globOptions = {}) {
212
+ const matches = globMatcher(pattern);
213
+ return (await walk(globOptions.cwd ?? '.'))
214
+ .filter((file) => matches(file.fromStart))
215
+ .slice(0, globOptions.limit ?? 1_000)
216
+ .map((file) => file.fromRoot);
217
+ },
218
+ async grep(pattern, grepOptions = {}) {
219
+ const expression = new RegExp(pattern, grepOptions.ignoreCase ? 'i' : '');
220
+ const only = grepOptions.glob ? globMatcher(grepOptions.glob) : undefined;
221
+ const limit = grepOptions.limit ?? 200;
222
+ const candidates = (await walk(grepOptions.cwd ?? '.', `-size -${GREP_FILE_BYTES / 1024 + 1}k`)).filter((file) => !only || only(file.fromStart));
223
+ const found = [];
224
+ let read = 0;
225
+ for (let index = 0; index < candidates.length && found.length < limit && read < GREP_TOTAL_BYTES; index += 200) {
226
+ const batch = candidates.slice(index, index + 200).map((file) => `./${file.fromRoot}`);
227
+ const result = await shell('cd "$1" && shift && tar -cf - "$@"', [workdir, ...batch], undefined, GREP_TOTAL_BYTES - read);
228
+ for (const entry of untar(result.stdout)) {
229
+ read += entry.data.byteLength;
230
+ const file = entry.name.replace(/^\.\//, '');
231
+ found.push(...matchingLines(entry.data, expression, limit - found.length).map((match) => ({ file, ...match })));
232
+ if (found.length >= limit)
233
+ break;
234
+ }
235
+ }
236
+ return found;
237
+ },
238
+ async dispose() {
239
+ const current = started;
240
+ started = undefined;
241
+ const id = await current?.catch(() => undefined);
242
+ if (id)
243
+ await invoke(['rm', '--force', id]);
244
+ },
245
+ };
246
+ }
247
+ const KILL_MARKED = 'for p in /proc/[0-9]*; do if tr "\\000" "\\n" < "$p/environ" 2>/dev/null | grep -qx "NEXUS_SANDBOX_EXEC=$1"; then kill -9 "$(basename "$p")" 2>/dev/null; fi; done; true';
248
+ function envArgs(env) {
249
+ return Object.entries(env ?? {}).flatMap(([key, value]) => ['--env', `${key}=${value}`]);
250
+ }
251
+ function concat(chunks) {
252
+ const bytes = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));
253
+ let offset = 0;
254
+ for (const chunk of chunks) {
255
+ bytes.set(chunk, offset);
256
+ offset += chunk.byteLength;
257
+ }
258
+ return bytes;
259
+ }
260
+ function untar(tar) {
261
+ const decoder = new TextDecoder();
262
+ const field = (from, length) => {
263
+ const bytes = tar.subarray(from, from + length);
264
+ const end = bytes.indexOf(0);
265
+ return decoder.decode(end < 0 ? bytes : bytes.subarray(0, end));
266
+ };
267
+ const files = [];
268
+ let offset = 0;
269
+ let longName;
270
+ while (offset + 512 <= tar.byteLength) {
271
+ if (tar.subarray(offset, offset + 512).every((byte) => byte === 0))
272
+ break;
273
+ const size = Number.parseInt(field(offset + 124, 12).trim() || '0', 8);
274
+ const type = String.fromCharCode(tar[offset + 156] ?? 0);
275
+ const prefix = field(offset + 345, 155);
276
+ const name = longName ?? (prefix ? `${prefix}/${field(offset, 100)}` : field(offset, 100));
277
+ const data = tar.subarray(offset + 512, offset + 512 + size);
278
+ longName = undefined;
279
+ if (type === 'L') {
280
+ longName = decoder.decode(data).replace(/\0+$/, '');
281
+ }
282
+ else if (type === 'x') {
283
+ longName = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(decoder.decode(data))?.[1];
284
+ }
285
+ else if (type === '0' || type === '\0') {
286
+ files.push({ name, data });
287
+ }
288
+ offset += 512 + Math.ceil(size / 512) * 512;
289
+ }
290
+ return files;
291
+ }
@@ -33,6 +33,33 @@ export interface SandboxExecResult {
33
33
  /** How long it ran, in milliseconds. */
34
34
  durationMs: number;
35
35
  }
36
+ /** Options for `Sandbox.glob()`. */
37
+ export interface SandboxGlobOptions {
38
+ /** The directory to search, inside the sandbox. Defaults to its root. */
39
+ cwd?: string;
40
+ /** The most paths returned. Defaults to 1,000. */
41
+ limit?: number;
42
+ }
43
+ /** Options for `Sandbox.grep()`. */
44
+ export interface SandboxGrepOptions {
45
+ /** The directory to search, inside the sandbox. Defaults to its root. */
46
+ cwd?: string;
47
+ /** Searches only the files whose paths, relative to `cwd`, match this glob. */
48
+ glob?: string;
49
+ /** Matches without regard to case. */
50
+ ignoreCase?: boolean;
51
+ /** The most matching lines returned. Defaults to 200. */
52
+ limit?: number;
53
+ }
54
+ /** One line `Sandbox.grep()` matched. */
55
+ export interface SandboxMatch {
56
+ /** The file, relative to the sandbox's root. */
57
+ file: string;
58
+ /** The line's number, counting from 1. */
59
+ line: number;
60
+ /** The line, cut to 300 characters. */
61
+ text: string;
62
+ }
36
63
  /** What a sandbox claims to isolate. The conformance suite checks the claims it can observe. */
37
64
  export interface SandboxIsolation {
38
65
  /** Commands cannot read or write the host's files. */
@@ -58,6 +85,19 @@ export interface Sandbox {
58
85
  listFiles?(directory?: string): Promise<string[]>;
59
86
  /** Removes a file or directory. */
60
87
  removeFile?(file: string): Promise<void>;
88
+ /**
89
+ * The files under a directory whose paths match a glob, such as `src/**\/*.ts`, sorted. The glob
90
+ * matches paths relative to `cwd`; the paths returned are relative to the root, as `readFile`
91
+ * takes them. Returns at most `limit`.
92
+ */
93
+ glob?(pattern: string, options?: SandboxGlobOptions): Promise<string[]>;
94
+ /**
95
+ * The lines matching a regular expression in the files under a directory, at most `limit`. Large
96
+ * files and binary files are skipped.
97
+ */
98
+ grep?(pattern: string, options?: SandboxGrepOptions): Promise<SandboxMatch[]>;
99
+ /** Reads a file's bytes, for an image, audio, or a document. */
100
+ readBytes?(file: string): Promise<Uint8Array>;
61
101
  /** Releases the sandbox. */
62
102
  dispose?(): Promise<void>;
63
103
  }
@@ -88,14 +128,23 @@ export interface ProcessSandboxOptions {
88
128
  * Its file methods refuse any path outside `root`, and commands get only `PATH` and the variables
89
129
  * given, never the host's environment. But a command it runs can read and write anything the user
90
130
  * can, and reach any host: `isolation` says so. Use a container or a VM in production.
131
+ *
132
+ * Its search skips `.git` and `node_modules`, walks at most 20,000 entries, and follows no links;
133
+ * `grep()` skips files over 1 MB and binary files, reads at most 64 MB, and searches the first
134
+ * 4,000 characters of each line.
91
135
  */
92
136
  export declare function processSandbox(options: ProcessSandboxOptions): Sandbox;
93
137
  /** Options for `sandboxTools()`. */
94
138
  export interface SandboxToolsOptions {
95
139
  /** Offer `run_command`. Defaults to true. */
96
140
  shell?: boolean;
97
- /** Offer `read_file`, `write_file`, and `list_files`. Defaults to true. */
141
+ /**
142
+ * Offer the file tools: `read_file`, `write_file`, and, where the sandbox can, `list_files`,
143
+ * `glob_files`, `grep_files`, and `read_media`. Defaults to true.
144
+ */
98
145
  files?: boolean;
146
+ /** The largest file `read_media` sends to the model, in bytes. Defaults to 10 MB. */
147
+ maxMediaBytes?: number;
99
148
  /**
100
149
  * The path a permission policy sees the sandbox's files under. Defaults to `/workspace`, so a
101
150
  * policy granting `/workspace/**` grants the sandbox.
@@ -1,3 +1,5 @@
1
+ import { encodeBase64 } from '../utils/base64.js';
2
+ import { GREP_FILE_BYTES, GREP_TOTAL_BYTES, globMatcher, MEDIA_TYPES, matchingLines, SKIPPED_DIRECTORIES, WALK_LIMIT, } from './glob.js';
1
3
  export class SandboxPathError extends Error {
2
4
  path;
3
5
  code = 'SANDBOX_PATH';
@@ -9,7 +11,7 @@ export class SandboxPathError extends Error {
9
11
  }
10
12
  export function processSandbox(options) {
11
13
  const ready = (async () => {
12
- const [{ spawn }, fs, { dirname, isAbsolute, relative, resolve }] = await Promise.all([
14
+ const [{ spawn }, fs, { dirname, isAbsolute, join, relative, resolve }] = await Promise.all([
13
15
  import('node:child_process'),
14
16
  import('node:fs/promises'),
15
17
  import('node:path'),
@@ -23,10 +25,36 @@ export function processSandbox(options) {
23
25
  throw new SandboxPathError(file);
24
26
  return resolved;
25
27
  };
26
- return { spawn, fs, dirname, root, inside };
28
+ return { spawn, fs, dirname, join, relative, root, inside };
27
29
  })();
28
30
  ready.catch(() => undefined);
29
31
  const maxOutput = options.maxOutputBytes ?? 1_048_576;
32
+ const walk = async (directory) => {
33
+ const { fs, join, relative, root, inside } = await ready;
34
+ const start = inside(directory);
35
+ const slash = (path) => path.replace(/\\/g, '/');
36
+ const files = [];
37
+ const queue = [start];
38
+ let walked = 0;
39
+ while (queue.length > 0 && walked < WALK_LIMIT) {
40
+ const current = queue.shift();
41
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
42
+ for (const entry of entries) {
43
+ walked += 1;
44
+ if (walked > WALK_LIMIT)
45
+ break;
46
+ const full = join(current, entry.name);
47
+ if (entry.isDirectory()) {
48
+ if (!SKIPPED_DIRECTORIES.has(entry.name))
49
+ queue.push(full);
50
+ }
51
+ else if (entry.isFile()) {
52
+ files.push({ full, fromRoot: slash(relative(root, full)), fromStart: slash(relative(start, full)) });
53
+ }
54
+ }
55
+ }
56
+ return files.sort((a, b) => (a.fromRoot < b.fromRoot ? -1 : a.fromRoot > b.fromRoot ? 1 : 0));
57
+ };
30
58
  return {
31
59
  name: 'process (development only)',
32
60
  isolation: { filesystem: false, network: false, processes: false },
@@ -108,6 +136,40 @@ export function processSandbox(options) {
108
136
  throw new SandboxPathError(file);
109
137
  await fs.rm(target, { recursive: true, force: true });
110
138
  },
139
+ async glob(pattern, globOptions = {}) {
140
+ const matches = globMatcher(pattern);
141
+ return (await walk(globOptions.cwd ?? '.'))
142
+ .filter((file) => matches(file.fromStart))
143
+ .slice(0, globOptions.limit ?? 1_000)
144
+ .map((file) => file.fromRoot);
145
+ },
146
+ async grep(pattern, grepOptions = {}) {
147
+ const { fs } = await ready;
148
+ const expression = new RegExp(pattern, grepOptions.ignoreCase ? 'i' : '');
149
+ const only = grepOptions.glob ? globMatcher(grepOptions.glob) : undefined;
150
+ const limit = grepOptions.limit ?? 200;
151
+ const found = [];
152
+ let scanned = 0;
153
+ for (const file of await walk(grepOptions.cwd ?? '.')) {
154
+ if (only && !only(file.fromStart))
155
+ continue;
156
+ const size = (await fs.stat(file.full).catch(() => undefined))?.size;
157
+ if (size === undefined || size > GREP_FILE_BYTES)
158
+ continue;
159
+ scanned += size;
160
+ if (scanned > GREP_TOTAL_BYTES)
161
+ break;
162
+ const lines = matchingLines(await fs.readFile(file.full), expression, limit - found.length);
163
+ found.push(...lines.map((match) => ({ file: file.fromRoot, ...match })));
164
+ if (found.length >= limit)
165
+ break;
166
+ }
167
+ return found;
168
+ },
169
+ async readBytes(file) {
170
+ const { fs, inside } = await ready;
171
+ return fs.readFile(inside(file));
172
+ },
111
173
  };
112
174
  }
113
175
  function killTree(spawn, pid) {
@@ -182,9 +244,100 @@ export function sandboxTools(sandbox, options = {}) {
182
244
  execute: async (args) => list(local(args.path)),
183
245
  });
184
246
  }
247
+ if (sandbox.glob) {
248
+ const glob = sandbox.glob.bind(sandbox);
249
+ tools.push({
250
+ name: 'glob_files',
251
+ description: `Finds files by a glob, such as **/*.ts or src/*.{js,json}, and lists their paths relative to ${mount}, at most ${GLOB_LIMIT}.`,
252
+ parameters: {
253
+ type: 'object',
254
+ properties: {
255
+ pattern: { type: 'string', description: 'The glob the paths must match' },
256
+ path: { type: 'string', description: 'The directory to search. Defaults to the whole workspace' },
257
+ },
258
+ required: ['pattern'],
259
+ },
260
+ capabilities: (args) => [`filesystem:read:${mounted(args.path)}`],
261
+ execute: async (args) => {
262
+ const files = await glob(String(args.pattern ?? '**'), { cwd: local(args.path), limit: GLOB_LIMIT + 1 });
263
+ if (files.length === 0)
264
+ return 'No files match.';
265
+ const shown = files.slice(0, GLOB_LIMIT).join('\n');
266
+ return files.length > GLOB_LIMIT ? `${shown}\n[more files match: narrow the glob]` : shown;
267
+ },
268
+ });
269
+ }
270
+ if (sandbox.grep) {
271
+ const grep = sandbox.grep.bind(sandbox);
272
+ tools.push({
273
+ name: 'grep_files',
274
+ description: `Searches files for lines matching a regular expression and lists each as path:line: text, at most ${GREP_LIMIT}. Large and binary files are skipped.`,
275
+ parameters: {
276
+ type: 'object',
277
+ properties: {
278
+ pattern: { type: 'string', description: 'The regular expression to find' },
279
+ path: { type: 'string', description: 'The directory to search. Defaults to the whole workspace' },
280
+ glob: { type: 'string', description: 'Searches only files matching this glob, such as **/*.ts' },
281
+ ignore_case: { type: 'boolean', description: 'Matches without regard to case' },
282
+ },
283
+ required: ['pattern'],
284
+ },
285
+ capabilities: (args) => [`filesystem:read:${mounted(args.path)}`],
286
+ execute: async (args) => {
287
+ const found = await grep(String(args.pattern ?? ''), {
288
+ cwd: local(args.path),
289
+ ...(typeof args.glob === 'string' && args.glob ? { glob: args.glob } : {}),
290
+ ignoreCase: args.ignore_case === true,
291
+ limit: GREP_LIMIT + 1,
292
+ });
293
+ if (found.length === 0)
294
+ return 'No lines match.';
295
+ const shown = found
296
+ .slice(0, GREP_LIMIT)
297
+ .map((match) => `${match.file}:${match.line}: ${match.text}`)
298
+ .join('\n');
299
+ return found.length > GREP_LIMIT ? `${shown}\n[more lines match: narrow the search]` : shown;
300
+ },
301
+ });
302
+ }
303
+ if (sandbox.readBytes) {
304
+ const readBytes = sandbox.readBytes.bind(sandbox);
305
+ const maxBytes = options.maxMediaBytes ?? 10 * 1_048_576;
306
+ tools.push({
307
+ name: 'read_media',
308
+ description: `Shows you an image (PNG, JPEG, GIF, WebP), audio (WAV, MP3, FLAC, OGG, AAC), or a PDF in the sandbox as content you see or hear, not as text. Paths are relative to ${mount}.`,
309
+ parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
310
+ capabilities: (args) => [`filesystem:read:${mounted(args.path)}`],
311
+ execute: async (args) => {
312
+ const file = local(args.path);
313
+ const mimeType = MEDIA_TYPES[file.split('.').pop()?.toLowerCase() ?? ''];
314
+ if (!mimeType)
315
+ throw new Error(`${file} is not an image, audio, or PDF file; read text with read_file`);
316
+ const bytes = await readBytes(file);
317
+ if (bytes.byteLength > maxBytes) {
318
+ throw new Error(`${file} is ${bytes.byteLength} bytes, over the ${maxBytes}-byte limit for media`);
319
+ }
320
+ return mediaOutput(file, mimeType, bytes);
321
+ },
322
+ });
323
+ }
185
324
  }
186
325
  return tools;
187
326
  }
327
+ const GLOB_LIMIT = 500;
328
+ const GREP_LIMIT = 200;
329
+ function mediaOutput(file, mimeType, bytes) {
330
+ const data = encodeBase64(bytes);
331
+ const part = mimeType.startsWith('image/')
332
+ ? { type: 'image', source: { base64: data, mimeType } }
333
+ : mimeType.startsWith('audio/')
334
+ ? { type: 'audio', source: { base64: data, format: mimeType.slice('audio/'.length) } }
335
+ : { type: 'document', source: { base64: data, mimeType }, filename: file.split('/').pop() ?? file };
336
+ return {
337
+ type: 'tool_output',
338
+ content: [{ type: 'text', text: `${file} (${mimeType}, ${bytes.byteLength} bytes)` }, part],
339
+ };
340
+ }
188
341
  export async function runSandboxConformance(create, options = {}) {
189
342
  const node = options.node ?? 'node';
190
343
  const js = (code) => `${node} -e "${code}"`;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Tool search: an agent with hundreds of tools marks most of them `deferred`, and the model finds the
3
+ * few a task needs with `search_tools` instead of reading every definition on every call.
4
+ *
5
+ * The search ranks tools by BM25 over their names, descriptions, and argument names and descriptions,
6
+ * and answers with the tools it found as `tool_reference` parts. A provider with tool references,
7
+ * such as Anthropic, expands each reference into the tool's definition where it stands, so the tool
8
+ * list at the front of the prompt never changes; every other provider is sent a found tool from then
9
+ * on. Either way the model calls a found tool on its next turn.
10
+ */
11
+ import type { ToolDefinition } from '../types/messages.js';
12
+ /** The name of the search tool an agent with deferred tools offers. */
13
+ export declare const TOOL_SEARCH_NAME = "search_tools";
14
+ /** Options for `toolSearchTool()`. */
15
+ export interface ToolSearchOptions {
16
+ /** How many tools a search returns unless the model asks for another number. Defaults to 5. */
17
+ limit?: number;
18
+ /** The most a model may ask for. Defaults to 20. */
19
+ maxLimit?: number;
20
+ }
21
+ /** One tool a search found, with its score. */
22
+ export interface ToolSearchHit {
23
+ /** The tool. */
24
+ tool: ToolDefinition;
25
+ /** Its BM25 score for the query; higher is closer. */
26
+ score: number;
27
+ }
28
+ /**
29
+ * Ranks tools against a query by BM25 over their names, descriptions, and arguments. A tool's name
30
+ * counts twice, so a query that names a tool finds it first. Returns the tools that match at all,
31
+ * best first, at most `limit`.
32
+ */
33
+ export declare function searchTools(tools: readonly ToolDefinition[], query: string, limit?: number): ToolSearchHit[];
34
+ /**
35
+ * The tool an agent offers when it has deferred tools: `search_tools`, which finds deferred tools by
36
+ * what a task needs and answers with them as tool references. `createAgent()` adds it on its own; an
37
+ * application's own loop can offer it the same way.
38
+ */
39
+ export declare function toolSearchTool(tools: readonly ToolDefinition[], options?: ToolSearchOptions): ToolDefinition;