@qwen-code/arena-agent 0.0.1

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 (64) hide show
  1. package/.env.example +52 -0
  2. package/README.md +436 -0
  3. package/dist/agent.d.ts +33 -0
  4. package/dist/agent.js +366 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/attachments.d.ts +29 -0
  7. package/dist/attachments.js +222 -0
  8. package/dist/attachments.js.map +1 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +256 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/compaction.d.ts +15 -0
  13. package/dist/compaction.js +176 -0
  14. package/dist/compaction.js.map +1 -0
  15. package/dist/config.d.ts +23 -0
  16. package/dist/config.js +61 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/document-reader.d.ts +18 -0
  19. package/dist/document-reader.js +423 -0
  20. package/dist/document-reader.js.map +1 -0
  21. package/dist/fetch-page.d.ts +25 -0
  22. package/dist/fetch-page.js +428 -0
  23. package/dist/fetch-page.js.map +1 -0
  24. package/dist/history.d.ts +5 -0
  25. package/dist/history.js +31 -0
  26. package/dist/history.js.map +1 -0
  27. package/dist/mock.d.ts +4 -0
  28. package/dist/mock.js +151 -0
  29. package/dist/mock.js.map +1 -0
  30. package/dist/protocol.d.ts +23 -0
  31. package/dist/protocol.js +84 -0
  32. package/dist/protocol.js.map +1 -0
  33. package/dist/provider.d.ts +31 -0
  34. package/dist/provider.js +308 -0
  35. package/dist/provider.js.map +1 -0
  36. package/dist/sdk.d.ts +181 -0
  37. package/dist/sdk.js +674 -0
  38. package/dist/sdk.js.map +1 -0
  39. package/dist/server.d.ts +15 -0
  40. package/dist/server.js +1433 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/spec.d.ts +11 -0
  43. package/dist/spec.js +198 -0
  44. package/dist/spec.js.map +1 -0
  45. package/dist/tools.d.ts +38 -0
  46. package/dist/tools.js +992 -0
  47. package/dist/tools.js.map +1 -0
  48. package/dist/tui.d.ts +10 -0
  49. package/dist/tui.js +350 -0
  50. package/dist/tui.js.map +1 -0
  51. package/dist/types.d.ts +146 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/web/app.d.ts +1 -0
  55. package/dist/web/app.js +1082 -0
  56. package/dist/web/app.js.map +1 -0
  57. package/docs/SDK.md +521 -0
  58. package/package.json +59 -0
  59. package/spec/message.json +150 -0
  60. package/spec/tools.json +418 -0
  61. package/web/arena-logo.svg +11 -0
  62. package/web/fonts.css +67 -0
  63. package/web/index.html +285 -0
  64. package/web/styles.css +1798 -0
package/dist/tools.js ADDED
@@ -0,0 +1,992 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, realpathSync, } from "node:fs";
3
+ import { readFile, realpath, stat, writeFile, } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { Ajv } from "ajv";
7
+ import addFormats from "ajv-formats";
8
+ import { detect as detectEncoding } from "chardet";
9
+ import iconv from "iconv-lite";
10
+ import mime from "mime/lite";
11
+ import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
12
+ import { FetchPageReader } from "./fetch-page.js";
13
+ import { toolDefinitions } from "./spec.js";
14
+ const knownTextApplicationMimeTypes = new Set([
15
+ "application/ecmascript",
16
+ "application/javascript",
17
+ "application/json",
18
+ "application/node",
19
+ "application/toml",
20
+ "application/xml",
21
+ ]);
22
+ const knownTextExtensions = new Set([
23
+ ".adoc",
24
+ ".astro",
25
+ ".bash",
26
+ ".bib",
27
+ ".c",
28
+ ".cc",
29
+ ".cfg",
30
+ ".clj",
31
+ ".cmake",
32
+ ".conf",
33
+ ".cpp",
34
+ ".cs",
35
+ ".css",
36
+ ".cts",
37
+ ".cxx",
38
+ ".dart",
39
+ ".dockerfile",
40
+ ".ex",
41
+ ".exs",
42
+ ".fish",
43
+ ".fs",
44
+ ".go",
45
+ ".gql",
46
+ ".gradle",
47
+ ".graphql",
48
+ ".groovy",
49
+ ".h",
50
+ ".hcl",
51
+ ".hpp",
52
+ ".hs",
53
+ ".html",
54
+ ".ini",
55
+ ".java",
56
+ ".js",
57
+ ".json",
58
+ ".jsx",
59
+ ".kt",
60
+ ".kts",
61
+ ".lua",
62
+ ".m",
63
+ ".md",
64
+ ".mjs",
65
+ ".mk",
66
+ ".mm",
67
+ ".mts",
68
+ ".nim",
69
+ ".org",
70
+ ".php",
71
+ ".pl",
72
+ ".pm",
73
+ ".properties",
74
+ ".proto",
75
+ ".ps1",
76
+ ".py",
77
+ ".pyi",
78
+ ".r",
79
+ ".rb",
80
+ ".rs",
81
+ ".rst",
82
+ ".scala",
83
+ ".sh",
84
+ ".sol",
85
+ ".sql",
86
+ ".svelte",
87
+ ".svg",
88
+ ".swift",
89
+ ".tex",
90
+ ".tf",
91
+ ".tfvars",
92
+ ".toml",
93
+ ".ts",
94
+ ".tsx",
95
+ ".txt",
96
+ ".vb",
97
+ ".vue",
98
+ ".xml",
99
+ ".yaml",
100
+ ".yml",
101
+ ".zig",
102
+ ".zsh",
103
+ ]);
104
+ const knownTextBasenames = new Set([
105
+ ".bashrc",
106
+ ".dockerignore",
107
+ ".editorconfig",
108
+ ".env",
109
+ ".gitattributes",
110
+ ".gitignore",
111
+ ".npmignore",
112
+ ".profile",
113
+ ".zshrc",
114
+ "AUTHORS",
115
+ "BUILD",
116
+ "CHANGELOG",
117
+ "CMakeLists.txt",
118
+ "COPYING",
119
+ "Cargo.lock",
120
+ "Containerfile",
121
+ "Dockerfile",
122
+ "Gemfile",
123
+ "GNUmakefile",
124
+ "Jenkinsfile",
125
+ "LICENSE",
126
+ "Makefile",
127
+ "NOTICE",
128
+ "Pipfile",
129
+ "Pipfile.lock",
130
+ "Procfile",
131
+ "README",
132
+ "Rakefile",
133
+ "Vagrantfile",
134
+ "WORKSPACE",
135
+ "go.mod",
136
+ "go.sum",
137
+ "go.work",
138
+ "package-lock.json",
139
+ "pnpm-lock.yaml",
140
+ "poetry.lock",
141
+ "requirements.txt",
142
+ "yarn.lock",
143
+ ]);
144
+ const knownBinaryExtensions = new Set([
145
+ ".7z",
146
+ ".a",
147
+ ".bin",
148
+ ".bz2",
149
+ ".class",
150
+ ".dll",
151
+ ".doc",
152
+ ".docx",
153
+ ".dylib",
154
+ ".exe",
155
+ ".flac",
156
+ ".gz",
157
+ ".jar",
158
+ ".o",
159
+ ".obj",
160
+ ".odt",
161
+ ".ods",
162
+ ".odp",
163
+ ".pdf",
164
+ ".ppt",
165
+ ".pptx",
166
+ ".rar",
167
+ ".so",
168
+ ".tar",
169
+ ".wasm",
170
+ ".xls",
171
+ ".xlsx",
172
+ ".zip",
173
+ ]);
174
+ const visibleImageExtensions = new Set([
175
+ ".bmp",
176
+ ".gif",
177
+ ".jpeg",
178
+ ".jpg",
179
+ ".png",
180
+ ".webp",
181
+ ]);
182
+ const imagePlaceholder = "[Image content is provided in the next user message.]";
183
+ const mimeTypes = {
184
+ ".aac": "audio/aac",
185
+ ".bmp": "image/bmp",
186
+ ".gif": "image/gif",
187
+ ".jpeg": "image/jpeg",
188
+ ".jpg": "image/jpeg",
189
+ ".m4a": "audio/mp4",
190
+ ".mp3": "audio/mpeg",
191
+ ".mp4": "video/mp4",
192
+ ".ogg": "audio/ogg",
193
+ ".pdf": "application/pdf",
194
+ ".png": "image/png",
195
+ ".svg": "image/svg+xml",
196
+ ".wav": "audio/wav",
197
+ ".webm": "video/webm",
198
+ ".webp": "image/webp",
199
+ };
200
+ function detectBom(data) {
201
+ if (data.length >= 4 &&
202
+ data[0] === 0xff &&
203
+ data[1] === 0xfe &&
204
+ data[2] === 0x00 &&
205
+ data[3] === 0x00) {
206
+ return { encoding: "utf32le", length: 4 };
207
+ }
208
+ if (data.length >= 4 &&
209
+ data[0] === 0x00 &&
210
+ data[1] === 0x00 &&
211
+ data[2] === 0xfe &&
212
+ data[3] === 0xff) {
213
+ return { encoding: "utf32be", length: 4 };
214
+ }
215
+ if (data.length >= 3 &&
216
+ data[0] === 0xef &&
217
+ data[1] === 0xbb &&
218
+ data[2] === 0xbf) {
219
+ return { encoding: "utf8", length: 3 };
220
+ }
221
+ if (data.length >= 2 && data[0] === 0xff && data[1] === 0xfe) {
222
+ return { encoding: "utf16le", length: 2 };
223
+ }
224
+ if (data.length >= 2 && data[0] === 0xfe && data[1] === 0xff) {
225
+ return { encoding: "utf16be", length: 2 };
226
+ }
227
+ return undefined;
228
+ }
229
+ function decodeUtf32(data, littleEndian) {
230
+ const characters = [];
231
+ const usableLength = data.length - (data.length % 4);
232
+ for (let index = 0; index < usableLength; index += 4) {
233
+ const codePoint = littleEndian
234
+ ? data.readUInt32LE(index)
235
+ : data.readUInt32BE(index);
236
+ characters.push(codePoint <= 0x10ffff &&
237
+ !(codePoint >= 0xd800 && codePoint <= 0xdfff)
238
+ ? String.fromCodePoint(codePoint)
239
+ : "\ufffd");
240
+ }
241
+ return characters.join("");
242
+ }
243
+ function decodeText(data) {
244
+ const bom = detectBom(data);
245
+ if (bom) {
246
+ const content = data.subarray(bom.length);
247
+ switch (bom.encoding) {
248
+ case "utf8":
249
+ return content.toString("utf8");
250
+ case "utf16le":
251
+ return content.toString("utf16le");
252
+ case "utf16be": {
253
+ const swapped = Buffer.from(content.subarray(0, content.length - (content.length % 2)));
254
+ return swapped.swap16().toString("utf16le");
255
+ }
256
+ case "utf32le":
257
+ return decodeUtf32(content, true);
258
+ case "utf32be":
259
+ return decodeUtf32(content, false);
260
+ }
261
+ }
262
+ try {
263
+ return new TextDecoder("utf-8", { fatal: true }).decode(data);
264
+ }
265
+ catch {
266
+ const encoding = detectEncoding(data);
267
+ if (encoding && iconv.encodingExists(encoding)) {
268
+ return iconv.decode(data, encoding);
269
+ }
270
+ return data.toString("utf8");
271
+ }
272
+ }
273
+ function isTextMimeType(value) {
274
+ return (value.startsWith("text/") ||
275
+ value.endsWith("+json") ||
276
+ value.endsWith("+xml") ||
277
+ knownTextApplicationMimeTypes.has(value));
278
+ }
279
+ function sampleLooksBinary(data) {
280
+ if (data.length === 0 || detectBom(data))
281
+ return false;
282
+ let nonPrintable = 0;
283
+ for (const byte of data) {
284
+ if (byte === 0)
285
+ return true;
286
+ if (byte < 9 || (byte > 13 && byte < 32))
287
+ nonPrintable += 1;
288
+ }
289
+ return nonPrintable / data.length > 0.3;
290
+ }
291
+ function workspaceFileKind(filePath, sample) {
292
+ const extension = path.extname(filePath).toLowerCase();
293
+ if (extension === ".svg")
294
+ return "text";
295
+ if (visibleImageExtensions.has(extension))
296
+ return "image";
297
+ const mimeType = mime.getType(filePath);
298
+ if (mimeType?.startsWith("image/"))
299
+ return "image";
300
+ if (mimeType && isTextMimeType(mimeType))
301
+ return "text";
302
+ if (knownBinaryExtensions.has(extension))
303
+ return "binary";
304
+ if (knownTextExtensions.has(extension) ||
305
+ knownTextBasenames.has(path.basename(filePath))) {
306
+ return "text";
307
+ }
308
+ return sampleLooksBinary(sample) ? "binary" : "text";
309
+ }
310
+ function json(value) {
311
+ return JSON.stringify(value);
312
+ }
313
+ function errorMessage(error) {
314
+ return error instanceof Error ? error.message : String(error);
315
+ }
316
+ function record(value) {
317
+ return value !== null && typeof value === "object" && !Array.isArray(value)
318
+ ? value
319
+ : {};
320
+ }
321
+ function stringValue(value) {
322
+ return typeof value === "string" ? value : "";
323
+ }
324
+ function dashScopeApiKey() {
325
+ const apiKey = process.env.DASHSCOPE_API_KEY;
326
+ if (!apiKey) {
327
+ throw new Error("DashScope tools require DASHSCOPE_API_KEY");
328
+ }
329
+ return apiKey;
330
+ }
331
+ function dashScopeRoot() {
332
+ return (process.env.DASHSCOPE_API_BASE ||
333
+ process.env.DASHSCOPE_BASE_URL ||
334
+ "https://dashscope.aliyuncs.com")
335
+ .replace(/\/+$/, "")
336
+ .replace(/\/(?:api\/v1|compatible-mode\/v1)$/i, "");
337
+ }
338
+ function dashScopeUrl(protocol, pathname) {
339
+ const prefix = protocol === "api" ? "/api/v1" : "/compatible-mode/v1";
340
+ const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
341
+ return `${dashScopeRoot()}${prefix}${normalized}`;
342
+ }
343
+ async function dashScopeFetch(protocol, pathname, init, label) {
344
+ const headers = new Headers(init.headers);
345
+ headers.set("authorization", `Bearer ${dashScopeApiKey()}`);
346
+ const response = await fetch(dashScopeUrl(protocol, pathname), {
347
+ ...init,
348
+ headers,
349
+ });
350
+ if (response.ok)
351
+ return response;
352
+ const body = await response.text();
353
+ let detail = body;
354
+ try {
355
+ const payload = record(JSON.parse(body));
356
+ detail =
357
+ stringValue(payload.message) ||
358
+ stringValue(record(payload.error).message) ||
359
+ body;
360
+ }
361
+ catch {
362
+ // Keep the response text as the diagnostic.
363
+ }
364
+ throw new Error(`${label} failed (${response.status} ${response.statusText}): ${detail.slice(0, 2000)}`);
365
+ }
366
+ async function dashScopeJson(protocol, pathname, init, label) {
367
+ const response = await dashScopeFetch(protocol, pathname, init, label);
368
+ return record(await response.json());
369
+ }
370
+ function responseMessage(payload) {
371
+ return (stringValue(payload.message) ||
372
+ stringValue(record(payload.output).message) ||
373
+ stringValue(payload.code) ||
374
+ "unknown provider error");
375
+ }
376
+ function escapeRegExp(value) {
377
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
378
+ }
379
+ async function responseBytes(response, limit = 4 * 1024 * 1024) {
380
+ const bytes = Buffer.from(await response.arrayBuffer());
381
+ if (bytes.length > limit) {
382
+ throw new Error(`download exceeds ${limit} bytes`);
383
+ }
384
+ return bytes;
385
+ }
386
+ export class ToolExecutor {
387
+ config;
388
+ workspace;
389
+ workspaceReal;
390
+ validators = new Map();
391
+ pendingVisualContent = new Map();
392
+ askUserHandler;
393
+ signal;
394
+ workspaceContext;
395
+ pageReader;
396
+ ajv = new Ajv({ allErrors: true, strict: false });
397
+ // ask_user needs an interactive front-end (TUI/WebUI) to collect answers.
398
+ // Headless `-p` supplies no handler, so the tool is withheld from the model
399
+ // entirely rather than offered and then dead-ending on needs_input.
400
+ askUserEnabled;
401
+ constructor(config, options = {}) {
402
+ this.config = config;
403
+ this.workspace = path.resolve(config.workspace);
404
+ mkdirSync(this.workspace, { recursive: true });
405
+ this.workspaceReal = realpathSync(this.workspace);
406
+ this.askUserHandler = options.askUserHandler;
407
+ this.askUserEnabled = Boolean(options.askUserHandler);
408
+ this.signal = options.signal;
409
+ this.workspaceContext = options.workspaceContext;
410
+ this.pageReader = new FetchPageReader({ signal: options.signal });
411
+ addFormats(this.ajv);
412
+ for (const definition of toolDefinitions(this.workspaceContext)) {
413
+ this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
414
+ }
415
+ }
416
+ // The tool schemas offered to the model for this session. Identical to the
417
+ // canonical trace set, adjusted for the selected workspace runtime and minus
418
+ // ask_user when no interactive handler is wired.
419
+ availableTools() {
420
+ const definitions = toolDefinitions(this.workspaceContext);
421
+ if (this.askUserEnabled)
422
+ return definitions;
423
+ return definitions.filter((definition) => definition.function.name !== "ask_user");
424
+ }
425
+ async execute(call) {
426
+ const callId = String(call.id || "");
427
+ const name = String(call.function?.name || "");
428
+ this.pendingVisualContent.delete(callId);
429
+ try {
430
+ const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
431
+ ? call.function.arguments
432
+ : "{}");
433
+ const validate = this.validators.get(name);
434
+ if (!validate)
435
+ throw new Error(`unknown tool: ${name}`);
436
+ if (!validate(argumentsValue)) {
437
+ const errors = (validate.errors || [])
438
+ .map((error) => `${error.instancePath || "/"} ${error.message}`)
439
+ .join("; ");
440
+ throw new Error(`invalid arguments: ${errors}`);
441
+ }
442
+ const result = await this.dispatch(name, argumentsValue, callId);
443
+ return {
444
+ role: "tool",
445
+ tool_call_id: callId,
446
+ content: typeof result === "string" ? result : json(result),
447
+ };
448
+ }
449
+ catch (error) {
450
+ return {
451
+ role: "tool",
452
+ tool_call_id: callId,
453
+ content: json({
454
+ status: "error",
455
+ tool: name,
456
+ error: errorMessage(error),
457
+ }),
458
+ };
459
+ }
460
+ }
461
+ takeVisualMessage(toolCallIds) {
462
+ const content = [];
463
+ for (const toolCallId of toolCallIds) {
464
+ const parts = this.pendingVisualContent.get(toolCallId);
465
+ this.pendingVisualContent.delete(toolCallId);
466
+ if (parts)
467
+ content.push(...parts);
468
+ }
469
+ return content.length ? { role: "user", content } : undefined;
470
+ }
471
+ async dispatch(name, values, toolCallId) {
472
+ switch (name) {
473
+ case "bash":
474
+ return this.bash(String(values.command), String(values.cwd ||
475
+ (this.workspaceContext
476
+ ? path.resolve(this.workspaceContext.cwd)
477
+ : "/home/user")), Number(values.timeout || 30));
478
+ case "ask_user":
479
+ return this.askUser(values.questions, toolCallId);
480
+ case "edit_file":
481
+ return this.editFile(String(values.path), String(values.old_text), String(values.new_text));
482
+ case "read_file":
483
+ return this.readWorkspaceFile(String(values.path), toolCallId);
484
+ case "fetch_page":
485
+ return this.pageReader.read(String(values.url), Number(values.chunkIndex || 0));
486
+ case "web_search":
487
+ return this.webSearch(String(values.query), String(values.depth));
488
+ case "write_file":
489
+ return this.writeWorkspaceFile(String(values.path), String(values.content));
490
+ case "image_search":
491
+ return this.imageSearch(String(values.query), Number(values.count), toolCallId);
492
+ case "present_file":
493
+ return this.presentFile(String(values.path));
494
+ case "generate_image":
495
+ return this.generateImage(String(values.file_path), String(values.prompt), values.images);
496
+ case "generate_speech":
497
+ return this.generateSpeech(String(values.file_path), String(values.text), String(values.language), values.voice_identity);
498
+ default:
499
+ throw new Error(`unknown tool: ${name}`);
500
+ }
501
+ }
502
+ async confinedPath(raw) {
503
+ if (!raw)
504
+ throw new Error("path must not be empty");
505
+ let candidate;
506
+ if (!this.workspaceContext && (raw === "/home/user" || raw === "~")) {
507
+ candidate = this.workspace;
508
+ }
509
+ else if (!this.workspaceContext && raw.startsWith("/home/user/")) {
510
+ candidate = path.join(this.workspace, raw.slice("/home/user/".length));
511
+ }
512
+ else if (!this.workspaceContext && raw.startsWith("~/")) {
513
+ candidate = path.join(this.workspace, raw.slice(2));
514
+ }
515
+ else {
516
+ const defaultCwd = this.workspaceContext
517
+ ? path.resolve(this.workspaceContext.cwd)
518
+ : this.workspace;
519
+ candidate = path.isAbsolute(raw) ? raw : path.join(defaultCwd, raw);
520
+ }
521
+ const absolute = path.resolve(candidate);
522
+ const lexicalRelative = path.relative(this.workspace, absolute);
523
+ if (lexicalRelative.startsWith("..") ||
524
+ path.isAbsolute(lexicalRelative)) {
525
+ throw new Error(`path is outside the workspace: ${raw}`);
526
+ }
527
+ let ancestor = absolute;
528
+ while (!existsSync(ancestor)) {
529
+ const parent = path.dirname(ancestor);
530
+ if (parent === ancestor)
531
+ break;
532
+ ancestor = parent;
533
+ }
534
+ const ancestorReal = await realpath(ancestor);
535
+ const resolved = path.resolve(ancestorReal, path.relative(ancestor, absolute));
536
+ const realRelative = path.relative(this.workspaceReal, resolved);
537
+ if (realRelative.startsWith("..") || path.isAbsolute(realRelative)) {
538
+ throw new Error(`path is outside the workspace: ${raw}`);
539
+ }
540
+ return resolved;
541
+ }
542
+ relativePath(target) {
543
+ return path.relative(this.workspaceReal, target).split(path.sep).join("/");
544
+ }
545
+ async bash(command, cwd, timeoutSeconds) {
546
+ const workdir = await this.confinedPath(cwd);
547
+ if (!(await stat(workdir)).isDirectory()) {
548
+ throw new Error(`cwd is not a directory: ${cwd}`);
549
+ }
550
+ return new Promise((resolve, reject) => {
551
+ const child = spawn("/bin/bash", ["-lc", command], {
552
+ cwd: workdir,
553
+ stdio: ["ignore", "pipe", "pipe"],
554
+ });
555
+ let stdout = "";
556
+ let stderr = "";
557
+ let timedOut = false;
558
+ const timeout = setTimeout(() => {
559
+ timedOut = true;
560
+ child.kill("SIGKILL");
561
+ }, timeoutSeconds * 1000);
562
+ const abort = () => child.kill("SIGTERM");
563
+ this.signal?.addEventListener("abort", abort, { once: true });
564
+ child.stdout.on("data", (chunk) => {
565
+ stdout += chunk.toString("utf8");
566
+ });
567
+ child.stderr.on("data", (chunk) => {
568
+ stderr += chunk.toString("utf8");
569
+ });
570
+ child.once("error", reject);
571
+ child.once("close", (code) => {
572
+ clearTimeout(timeout);
573
+ this.signal?.removeEventListener("abort", abort);
574
+ if (this.signal?.aborted) {
575
+ reject(this.signal.reason || new Error("operation cancelled"));
576
+ return;
577
+ }
578
+ if (timedOut) {
579
+ resolve({
580
+ status: "timeout",
581
+ exit_code: null,
582
+ stdout: "",
583
+ stderr: `command exceeded ${timeoutSeconds}s`,
584
+ });
585
+ return;
586
+ }
587
+ resolve({
588
+ status: code === 0 ? "success" : "error",
589
+ exit_code: code,
590
+ stdout,
591
+ stderr,
592
+ });
593
+ });
594
+ });
595
+ }
596
+ async askUser(questions, toolCallId) {
597
+ if (this.askUserHandler) {
598
+ return this.askUserHandler(toolCallId, questions);
599
+ }
600
+ // ask_user is not offered in non-interactive sessions; if the model still
601
+ // emits it (e.g. resumed history or a hallucinated call), report an error
602
+ // so the turn continues instead of stalling on needs_input.
603
+ return {
604
+ status: "error",
605
+ tool: "ask_user",
606
+ error: "ask_user is unavailable in this non-interactive session. " +
607
+ "Proceed with reasonable defaults and state any assumptions you make.",
608
+ };
609
+ }
610
+ async editFile(rawPath, oldText, newText) {
611
+ const target = await this.confinedPath(rawPath);
612
+ const source = await readFile(target, "utf8");
613
+ let updated;
614
+ if (source.includes(oldText)) {
615
+ updated = source.replace(oldText, newText);
616
+ }
617
+ else {
618
+ const tokens = oldText.split(/\s+/).filter(Boolean);
619
+ const pattern = new RegExp(tokens.map(escapeRegExp).join("\\s+"));
620
+ if (!pattern.test(source)) {
621
+ return {
622
+ status: "error",
623
+ message: "Context not found. Read the file to verify the text exists.",
624
+ };
625
+ }
626
+ updated = source.replace(pattern, newText);
627
+ }
628
+ await writeFile(target, updated, "utf8");
629
+ return { status: "success", message: `Edited ${rawPath}.` };
630
+ }
631
+ async readWorkspaceFile(rawPath, toolCallId) {
632
+ const target = await this.confinedPath(rawPath);
633
+ const data = await readFile(target);
634
+ const extension = path.extname(target).toLowerCase();
635
+ const kind = workspaceFileKind(target, data.subarray(0, 4096));
636
+ const relativePath = this.relativePath(target);
637
+ const result = {
638
+ status: "success",
639
+ path: relativePath,
640
+ };
641
+ if (isStructuredDocument(target)) {
642
+ const extracted = await extractStructuredDocument(target, data, this.signal);
643
+ result.content = extracted.content;
644
+ result.mime_type =
645
+ mime.getType(target) ||
646
+ mimeTypes[extension] ||
647
+ "application/octet-stream";
648
+ result.size = data.length;
649
+ result.document_type = extracted.documentType;
650
+ if (extracted.totalPages !== undefined) {
651
+ result.total_pages = extracted.totalPages;
652
+ result.pages_parsed = extracted.pagesParsed;
653
+ }
654
+ if (extracted.truncated)
655
+ result.truncated = true;
656
+ if (extracted.warning)
657
+ result.warning = extracted.warning;
658
+ }
659
+ else if (kind === "text") {
660
+ result.content = decodeText(data);
661
+ }
662
+ else {
663
+ const mimeType = mime.getType(target) ||
664
+ mimeTypes[extension] ||
665
+ "application/octet-stream";
666
+ result.mime_type = mimeType;
667
+ result.size = data.length;
668
+ if (kind === "image") {
669
+ if (data.length > 20 * 1024 * 1024) {
670
+ throw new Error(`image exceeds 20 MB: ${rawPath}`);
671
+ }
672
+ result.content = imagePlaceholder;
673
+ this.pendingVisualContent.set(toolCallId, [
674
+ {
675
+ text: "<arena-system-message>" +
676
+ `The image below is the genuine content returned by read_file for ${JSON.stringify(relativePath)}.` +
677
+ "</arena-system-message>",
678
+ },
679
+ {
680
+ image: `data:${mimeType};base64,${data.toString("base64")}`,
681
+ },
682
+ ]);
683
+ }
684
+ }
685
+ return result;
686
+ }
687
+ async writeWorkspaceFile(rawPath, content) {
688
+ const target = await this.confinedPath(rawPath);
689
+ mkdirSync(path.dirname(target), { recursive: true });
690
+ await writeFile(target, content, "utf8");
691
+ return { status: "success" };
692
+ }
693
+ async webSearch(query, depth) {
694
+ const model = process.env.ARENA_WEB_SEARCH_MODEL || "qwen-plus";
695
+ const searchStrategy = depth === "1" ? "turbo" : "max";
696
+ const payload = await dashScopeJson("api", "/services/aigc/text-generation/generation", {
697
+ method: "POST",
698
+ headers: { "content-type": "application/json" },
699
+ body: JSON.stringify({
700
+ model,
701
+ input: {
702
+ messages: [{ role: "user", content: query }],
703
+ },
704
+ parameters: {
705
+ enable_search: true,
706
+ result_format: "message",
707
+ search_options: {
708
+ forced_search: true,
709
+ enable_source: true,
710
+ enable_citation: true,
711
+ citation_format: "[<number>]",
712
+ search_strategy: searchStrategy,
713
+ },
714
+ },
715
+ }),
716
+ signal: this.signal,
717
+ }, "web_search");
718
+ const output = record(payload.output);
719
+ const searchInfo = record(output.search_info);
720
+ const searchResults = Array.isArray(searchInfo.search_results)
721
+ ? searchInfo.search_results
722
+ : [];
723
+ const choices = Array.isArray(output.choices) ? output.choices : [];
724
+ const answer = stringValue(record(record(choices[0]).message).content);
725
+ const limit = { "1": 3, "2": 5, "3": 8 }[depth] || 5;
726
+ return {
727
+ status: "success",
728
+ query,
729
+ answer,
730
+ results: searchResults.slice(0, limit).map((value, index) => {
731
+ const item = record(value);
732
+ return {
733
+ id: Number(item.index) || index + 1,
734
+ title: stringValue(item.title),
735
+ url: stringValue(item.url),
736
+ content: stringValue(item.content) ||
737
+ stringValue(item.snippet) ||
738
+ stringValue(item.summary),
739
+ site_name: stringValue(item.site_name),
740
+ };
741
+ }),
742
+ };
743
+ }
744
+ async imageSearch(query, count, toolCallId) {
745
+ const model = process.env.ARENA_IMAGE_SEARCH_MODEL || "qwen3.7-plus";
746
+ const payload = await dashScopeJson("compatible", "/responses", {
747
+ method: "POST",
748
+ headers: { "content-type": "application/json" },
749
+ body: JSON.stringify({
750
+ model,
751
+ input: query,
752
+ tools: [{ type: "web_search_image" }],
753
+ }),
754
+ signal: this.signal,
755
+ }, "image_search");
756
+ const output = Array.isArray(payload.output) ? payload.output : [];
757
+ const toolCall = output
758
+ .map(record)
759
+ .find((item) => item.type === "web_search_image_call");
760
+ let results = [];
761
+ if (toolCall?.output) {
762
+ try {
763
+ const parsed = typeof toolCall.output === "string"
764
+ ? JSON.parse(toolCall.output)
765
+ : toolCall.output;
766
+ if (Array.isArray(parsed))
767
+ results = parsed;
768
+ }
769
+ catch {
770
+ throw new Error("image_search returned malformed image results");
771
+ }
772
+ }
773
+ const images = [];
774
+ const searchId = toolCallId
775
+ .replace(/[^A-Za-z0-9_-]+/g, "-")
776
+ .replace(/^-+|-+$/g, "")
777
+ .slice(0, 96) || "search";
778
+ for (const value of results) {
779
+ if (images.length >= count)
780
+ break;
781
+ const item = record(value);
782
+ const sourceUrl = stringValue(item.url) ||
783
+ stringValue(item.original_url) ||
784
+ stringValue(item.image_url) ||
785
+ stringValue(item.thumbnail_url);
786
+ if (!sourceUrl)
787
+ continue;
788
+ try {
789
+ const imageResponse = await fetch(sourceUrl, { signal: this.signal });
790
+ if (!imageResponse.ok)
791
+ continue;
792
+ const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
793
+ if (!contentType.toLowerCase().startsWith("image/"))
794
+ continue;
795
+ const normalizedType = contentType.split(";")[0]?.trim().toLowerCase();
796
+ const extension = Object.entries(mimeTypes).find(([, mime]) => mime === normalizedType)?.[0] || ".jpg";
797
+ const target = await this.confinedPath(`assets/image-search/${searchId}/result-${images.length + 1}${extension}`);
798
+ mkdirSync(path.dirname(target), { recursive: true });
799
+ await writeFile(target, await responseBytes(imageResponse));
800
+ images.push({
801
+ path: this.relativePath(target),
802
+ source_url: sourceUrl,
803
+ title: stringValue(item.title),
804
+ });
805
+ }
806
+ catch (error) {
807
+ if (this.signal?.aborted)
808
+ throw error;
809
+ // A single inaccessible search result should not fail the whole search.
810
+ }
811
+ }
812
+ return { status: "success", query, images };
813
+ }
814
+ async presentFile(rawPath) {
815
+ const target = await this.confinedPath(rawPath);
816
+ if (!(await stat(target)).isFile())
817
+ throw new Error(rawPath);
818
+ return { path: rawPath, status: "success" };
819
+ }
820
+ async generateImage(rawPath, prompt, images) {
821
+ const target = await this.confinedPath(rawPath);
822
+ if (![".jpg", ".jpeg", ".png"].includes(path.extname(target).toLowerCase())) {
823
+ throw new Error("file_path must end in .jpg, .jpeg, or .png");
824
+ }
825
+ mkdirSync(path.dirname(target), { recursive: true });
826
+ const model = process.env.ARENA_IMAGE_MODEL || "wan2.7-image-pro";
827
+ const content = [];
828
+ for (const imagePath of images || []) {
829
+ const source = await this.confinedPath(imagePath);
830
+ const extension = path.extname(source).toLowerCase();
831
+ const mimeType = mimeTypes[extension];
832
+ if (!mimeType?.startsWith("image/")) {
833
+ throw new Error(`unsupported reference image format: ${imagePath}`);
834
+ }
835
+ const bytes = await readFile(source);
836
+ if (bytes.length > 20 * 1024 * 1024) {
837
+ throw new Error(`reference image exceeds 20 MB: ${imagePath}`);
838
+ }
839
+ content.push({
840
+ image: `data:${mimeType};base64,${bytes.toString("base64")}`,
841
+ });
842
+ }
843
+ content.push({ text: prompt });
844
+ const parameters = {
845
+ size: process.env.ARENA_IMAGE_SIZE || "2K",
846
+ n: 1,
847
+ watermark: false,
848
+ };
849
+ if (!images?.length)
850
+ parameters.thinking_mode = true;
851
+ const submitted = await dashScopeJson("api", "/services/aigc/image-generation/generation", {
852
+ method: "POST",
853
+ headers: {
854
+ "content-type": "application/json",
855
+ "x-dashscope-async": "enable",
856
+ },
857
+ body: JSON.stringify({
858
+ model,
859
+ input: {
860
+ messages: [{ role: "user", content }],
861
+ },
862
+ parameters,
863
+ }),
864
+ signal: this.signal,
865
+ }, "generate_image");
866
+ const taskId = stringValue(record(submitted.output).task_id);
867
+ if (!taskId) {
868
+ throw new Error(`generate_image returned no task_id: ${responseMessage(submitted)}`);
869
+ }
870
+ const timeoutMs = Number(process.env.DASHSCOPE_TASK_TIMEOUT_MS || 5 * 60 * 1000);
871
+ const pollIntervalMs = Number(process.env.DASHSCOPE_TASK_POLL_INTERVAL_MS || 1000);
872
+ const deadline = Date.now() + timeoutMs;
873
+ let completed;
874
+ while (Date.now() <= deadline) {
875
+ const task = await dashScopeJson("api", `/tasks/${encodeURIComponent(taskId)}`, { method: "GET", signal: this.signal }, "generate_image status");
876
+ const output = record(task.output);
877
+ const taskStatus = stringValue(output.task_status).toUpperCase();
878
+ if (taskStatus === "SUCCEEDED") {
879
+ completed = task;
880
+ break;
881
+ }
882
+ if (["FAILED", "CANCELED", "UNKNOWN"].includes(taskStatus)) {
883
+ throw new Error(`generate_image task ${taskStatus}: ${responseMessage(task)}`);
884
+ }
885
+ if (Date.now() + pollIntervalMs > deadline)
886
+ break;
887
+ await delay(pollIntervalMs, undefined, { signal: this.signal });
888
+ }
889
+ if (!completed) {
890
+ throw new Error(`generate_image timed out waiting for task ${taskId}`);
891
+ }
892
+ const output = record(completed.output);
893
+ const choices = Array.isArray(output.choices) ? output.choices : [];
894
+ const message = record(record(choices[0]).message);
895
+ const generatedContent = Array.isArray(message.content)
896
+ ? message.content
897
+ : [];
898
+ const generated = generatedContent
899
+ .map(record)
900
+ .find((item) => typeof item.image === "string");
901
+ const legacyResults = Array.isArray(output.results) ? output.results : [];
902
+ const imageUrl = stringValue(generated?.image) ||
903
+ stringValue(record(legacyResults[0]).url);
904
+ if (!imageUrl) {
905
+ throw new Error("generate_image task succeeded without an image URL");
906
+ }
907
+ const imageResponse = await fetch(imageUrl, { signal: this.signal });
908
+ if (!imageResponse.ok) {
909
+ throw new Error(`image download failed (${imageResponse.status})`);
910
+ }
911
+ await writeFile(target, await responseBytes(imageResponse, 25 * 1024 * 1024));
912
+ return {
913
+ status: "success",
914
+ file_path: this.relativePath(target),
915
+ task_id: taskId,
916
+ };
917
+ }
918
+ async generateSpeech(rawPath, text, language, voiceIdentity = {}) {
919
+ const target = await this.confinedPath(rawPath);
920
+ const extension = path.extname(target).toLowerCase();
921
+ const format = {
922
+ ".mp3": "mp3",
923
+ ".wav": "wav",
924
+ ".opus": "opus",
925
+ }[extension];
926
+ if (!format) {
927
+ throw new Error("DashScope speech output supports .mp3, .wav, or .opus");
928
+ }
929
+ mkdirSync(path.dirname(target), { recursive: true });
930
+ const model = process.env.ARENA_SPEECH_MODEL || "qwen-audio-3.0-tts-flash";
931
+ const feminine = voiceIdentity.gender !== "masculine";
932
+ const primaryLanguage = language.toLowerCase().split(/[-_]/)[0] || "";
933
+ const voices = primaryLanguage === "en"
934
+ ? feminine
935
+ ? ["loongeva_v3.6", "longanhuan_v3.6"]
936
+ : ["loongjohn", "longjielidou_v3.6"]
937
+ : feminine
938
+ ? ["longanhuan_v3.6"]
939
+ : ["longjielidou_v3.6"];
940
+ const requestedIndex = Math.max(0, Math.trunc(Number(voiceIdentity.index || 0)));
941
+ const voice = process.env.DASHSCOPE_SPEECH_VOICE ||
942
+ voices[requestedIndex % voices.length] ||
943
+ voices[0];
944
+ const supportedHints = new Set([
945
+ "ar",
946
+ "de",
947
+ "en",
948
+ "es",
949
+ "fil",
950
+ "fr",
951
+ "id",
952
+ "it",
953
+ "ja",
954
+ "ko",
955
+ "ms",
956
+ "pt",
957
+ "ru",
958
+ "th",
959
+ "vi",
960
+ "zh",
961
+ ]);
962
+ const input = {
963
+ text,
964
+ voice,
965
+ format,
966
+ sample_rate: 24000,
967
+ };
968
+ if (supportedHints.has(primaryLanguage)) {
969
+ input.language_hints = [primaryLanguage];
970
+ }
971
+ const payload = await dashScopeJson("api", "/services/audio/tts/SpeechSynthesizer", {
972
+ method: "POST",
973
+ headers: { "content-type": "application/json" },
974
+ body: JSON.stringify({ model, input }),
975
+ signal: this.signal,
976
+ }, "generate_speech");
977
+ const audioUrl = stringValue(record(record(payload.output).audio).url);
978
+ if (!audioUrl) {
979
+ throw new Error(`generate_speech returned no audio URL: ${responseMessage(payload)}`);
980
+ }
981
+ const audioResponse = await fetch(audioUrl, { signal: this.signal });
982
+ if (!audioResponse.ok) {
983
+ throw new Error(`audio download failed (${audioResponse.status})`);
984
+ }
985
+ await writeFile(target, await responseBytes(audioResponse, 25 * 1024 * 1024));
986
+ return {
987
+ status: "success",
988
+ file_path: this.relativePath(target),
989
+ };
990
+ }
991
+ }
992
+ //# sourceMappingURL=tools.js.map