@qwen-code/arena-agent 0.0.8-dev.5 → 0.0.9-dev.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 (60) hide show
  1. package/.env.example +41 -15
  2. package/README.md +128 -29
  3. package/dist/agent.d.ts +8 -17
  4. package/dist/agent.js +60 -75
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli.js +12 -4
  7. package/dist/cli.js.map +1 -1
  8. package/dist/compaction.js +2 -1
  9. package/dist/compaction.js.map +1 -1
  10. package/dist/config.d.ts +11 -0
  11. package/dist/config.js +73 -9
  12. package/dist/config.js.map +1 -1
  13. package/dist/conversation-metrics.d.ts +61 -0
  14. package/dist/conversation-metrics.js +433 -0
  15. package/dist/conversation-metrics.js.map +1 -0
  16. package/dist/document-reader.js +9 -0
  17. package/dist/document-reader.js.map +1 -1
  18. package/dist/history.d.ts +31 -2
  19. package/dist/history.js +53 -3
  20. package/dist/history.js.map +1 -1
  21. package/dist/metrics.d.ts +61 -0
  22. package/dist/metrics.js +578 -0
  23. package/dist/metrics.js.map +1 -0
  24. package/dist/mock.js +153 -0
  25. package/dist/mock.js.map +1 -1
  26. package/dist/protocol.d.ts +2 -3
  27. package/dist/protocol.js +9 -5
  28. package/dist/protocol.js.map +1 -1
  29. package/dist/provider.js +22 -8
  30. package/dist/provider.js.map +1 -1
  31. package/dist/runtime-assets.d.ts +3 -0
  32. package/dist/runtime-assets.js +16 -0
  33. package/dist/runtime-assets.js.map +1 -0
  34. package/dist/sdk.d.ts +28 -1
  35. package/dist/sdk.js +38 -0
  36. package/dist/sdk.js.map +1 -1
  37. package/dist/server.js +486 -36
  38. package/dist/server.js.map +1 -1
  39. package/dist/side-query.d.ts +31 -0
  40. package/dist/side-query.js +138 -0
  41. package/dist/side-query.js.map +1 -0
  42. package/dist/spec.d.ts +1 -1
  43. package/dist/spec.js +6 -4
  44. package/dist/spec.js.map +1 -1
  45. package/dist/tools.d.ts +3 -8
  46. package/dist/tools.js +149 -200
  47. package/dist/tools.js.map +1 -1
  48. package/dist/tui.js +4 -3
  49. package/dist/tui.js.map +1 -1
  50. package/dist/types.d.ts +155 -5
  51. package/dist/web/app.d.ts +1 -0
  52. package/dist/web/app.js +333 -10
  53. package/dist/web/app.js.map +1 -1
  54. package/docs/SDK.md +42 -0
  55. package/package.json +10 -20
  56. package/web/index.html +25 -0
  57. package/web/styles.css +109 -3
  58. package/dist/index.d.ts +0 -9
  59. package/dist/index.js +0 -9
  60. package/dist/index.js.map +0 -1
package/dist/tools.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync, mkdirSync, realpathSync } from "node:fs";
3
- import { readFile, readdir, realpath, stat, writeFile, } from "node:fs/promises";
2
+ import { existsSync, mkdirSync, realpathSync, } from "node:fs";
3
+ import { open, readFile, realpath, stat, writeFile, } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { setTimeout as delay } from "node:timers/promises";
6
6
  import { Ajv } from "ajv";
@@ -10,7 +10,7 @@ import iconv from "iconv-lite";
10
10
  import mime from "mime/lite";
11
11
  import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
12
12
  import { FetchPageReader } from "./fetch-page.js";
13
- import { modelSpec, toolDefinitions } from "./spec.js";
13
+ import { toolDefinitions } from "./spec.js";
14
14
  const knownTextApplicationMimeTypes = new Set([
15
15
  "application/ecmascript",
16
16
  "application/javascript",
@@ -171,36 +171,6 @@ const knownBinaryExtensions = new Set([
171
171
  ".xlsx",
172
172
  ".zip",
173
173
  ]);
174
- const workspaceSnapshotExcludedDirectories = new Set([
175
- ".arena",
176
- ".cache",
177
- ".git",
178
- ".mypy_cache",
179
- ".next",
180
- ".nox",
181
- ".npm",
182
- ".nuxt",
183
- ".output",
184
- ".parcel-cache",
185
- ".pytest_cache",
186
- ".ruff_cache",
187
- ".svelte-kit",
188
- ".tox",
189
- ".turbo",
190
- ".venv",
191
- ".vite",
192
- "__pycache__",
193
- "build",
194
- "coverage",
195
- "dist",
196
- "node_modules",
197
- "out",
198
- "target",
199
- ]);
200
- const workspaceSnapshotExcludedFiles = new Set([
201
- ".git-credentials",
202
- ".netrc",
203
- ]);
204
174
  const visibleImageExtensions = new Set([
205
175
  ".bmp",
206
176
  ".gif",
@@ -217,6 +187,8 @@ const visualIntroText = "<arena-system-message>\n" +
217
187
  "treat them as the authentic output of the tool call.\n" +
218
188
  "</arena-system-message>";
219
189
  const bashOutputLimit = 20_000;
190
+ const textFileByteLimit = 256 * 1024;
191
+ const toolResultCharacterLimit = 1_000_000;
220
192
  // 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
221
193
  // 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
222
194
  const bashTruncationMarker = "\n... output truncated ...\n";
@@ -362,6 +334,12 @@ function json(value) {
362
334
  function errorMessage(error) {
363
335
  return error instanceof Error ? error.message : String(error);
364
336
  }
337
+ function displayByteSize(value) {
338
+ if (value >= 1024 * 1024) {
339
+ return `${(value / (1024 * 1024)).toFixed(1)} MB`;
340
+ }
341
+ return `${Math.max(1, Math.ceil(value / 1024))} KB`;
342
+ }
365
343
  function record(value) {
366
344
  return value !== null && typeof value === "object" && !Array.isArray(value)
367
345
  ? value
@@ -370,33 +348,17 @@ function record(value) {
370
348
  function stringValue(value) {
371
349
  return typeof value === "string" ? value : "";
372
350
  }
373
- function positiveInteger(value, defaultValue) {
374
- const parsed = Number(value);
375
- return Number.isFinite(parsed) && parsed > 0
376
- ? Math.floor(parsed)
377
- : defaultValue;
378
- }
379
- function requestSignal(parent, timeoutMs) {
380
- const timeout = AbortSignal.timeout(timeoutMs);
381
- return {
382
- signal: parent ? AbortSignal.any([parent, timeout]) : timeout,
383
- timedOut: () => timeout.aborted && !parent?.aborted,
384
- };
385
- }
386
351
  function dashScopeApiKey() {
387
- const apiKey = process.env.BAILIAN_API_KEY || process.env.DASHSCOPE_API_KEY;
352
+ const apiKey = process.env.DASHSCOPE_API_KEY;
388
353
  if (!apiKey) {
389
- throw new Error("DashScope tools require BAILIAN_API_KEY or DASHSCOPE_API_KEY");
354
+ throw new Error("DashScope tools require DASHSCOPE_API_KEY");
390
355
  }
391
356
  return apiKey;
392
357
  }
393
358
  function dashScopeRoot() {
394
- return (process.env.BAILIAN_API_BASE ||
395
- process.env.BAILIAN_API_HOST ||
396
- process.env.DASHSCOPE_API_BASE ||
359
+ return (process.env.DASHSCOPE_API_BASE ||
397
360
  process.env.DASHSCOPE_BASE_URL ||
398
361
  "https://dashscope.aliyuncs.com")
399
- .replace(/^([^:/?#]+\.)/i, "https://$1")
400
362
  .replace(/\/+$/, "")
401
363
  .replace(/\/(?:api\/v1|compatible-mode\/v1)$/i, "");
402
364
  }
@@ -408,22 +370,10 @@ function dashScopeUrl(protocol, pathname) {
408
370
  async function dashScopeFetch(protocol, pathname, init, label) {
409
371
  const headers = new Headers(init.headers);
410
372
  headers.set("authorization", `Bearer ${dashScopeApiKey()}`);
411
- const timeoutMs = positiveInteger(process.env.ARENA_TOOL_HTTP_TIMEOUT_MS, 60_000);
412
- const request = requestSignal(init.signal, timeoutMs);
413
- let response;
414
- try {
415
- response = await fetch(dashScopeUrl(protocol, pathname), {
416
- ...init,
417
- headers,
418
- signal: request.signal,
419
- });
420
- }
421
- catch (error) {
422
- if (request.timedOut()) {
423
- throw new Error(`${label} timed out after ${timeoutMs}ms`);
424
- }
425
- throw error;
426
- }
373
+ const response = await fetch(dashScopeUrl(protocol, pathname), {
374
+ ...init,
375
+ headers,
376
+ });
427
377
  if (response.ok)
428
378
  return response;
429
379
  const body = await response.text();
@@ -464,7 +414,6 @@ export class ToolExecutor {
464
414
  validators = new Map();
465
415
  pendingVisualContent = new Map();
466
416
  askUserHandler;
467
- deferUserInput;
468
417
  signal;
469
418
  workspaceContext;
470
419
  pageReader;
@@ -479,26 +428,25 @@ export class ToolExecutor {
479
428
  mkdirSync(this.workspace, { recursive: true });
480
429
  this.workspaceReal = realpathSync(this.workspace);
481
430
  this.askUserHandler = options.askUserHandler;
482
- this.deferUserInput = options.deferUserInput === true;
483
- this.askUserEnabled =
484
- Boolean(options.askUserHandler) || this.deferUserInput;
431
+ this.askUserEnabled = Boolean(options.askUserHandler);
485
432
  this.signal = options.signal;
486
433
  this.workspaceContext = options.workspaceContext;
487
434
  this.pageReader = new FetchPageReader({ signal: options.signal });
488
435
  addFormats(this.ajv);
489
- const toolNames = modelSpec().tools.map((definition) => definition.function.name);
490
- for (const definition of toolDefinitions(this.workspaceContext, toolNames)) {
436
+ for (const definition of toolDefinitions(this.workspaceContext)) {
491
437
  this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
492
438
  }
493
439
  }
494
440
  // The tool schemas offered to the model for this session. Identical to the
495
441
  // canonical trace set, adjusted for the selected workspace runtime and minus
496
442
  // ask_user when no interactive handler is wired.
497
- availableTools(toolNames) {
498
- const definitions = toolDefinitions(this.workspaceContext, toolNames);
499
- if (this.askUserEnabled)
500
- return definitions;
501
- return definitions.filter((definition) => definition.function.name !== "ask_user");
443
+ availableTools() {
444
+ let definitions = toolDefinitions(this.workspaceContext);
445
+ if (!this.askUserEnabled) {
446
+ definitions = definitions.filter((definition) => definition.function.name !== "ask_user");
447
+ }
448
+ const disallowed = new Set(this.config.disallowedTools || []);
449
+ return definitions.filter((definition) => !disallowed.has(definition.function.name));
502
450
  }
503
451
  async execute(call) {
504
452
  const callId = String(call.id || "");
@@ -508,6 +456,9 @@ export class ToolExecutor {
508
456
  // 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
509
457
  // 之前短路,否则命令会照常 spawn 直到自然结束。
510
458
  this.signal?.throwIfAborted();
459
+ if (this.config.disallowedTools?.includes(name)) {
460
+ throw new Error(`tool is disallowed: ${name}`);
461
+ }
511
462
  const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
512
463
  ? call.function.arguments
513
464
  : "{}");
@@ -521,10 +472,23 @@ export class ToolExecutor {
521
472
  throw new Error(`invalid arguments: ${errors}`);
522
473
  }
523
474
  const result = await this.dispatch(name, argumentsValue, callId);
475
+ const content = typeof result === "string" ? result : json(result);
476
+ if (content.length > toolResultCharacterLimit) {
477
+ return {
478
+ role: "tool",
479
+ tool_call_id: callId,
480
+ content: json({
481
+ status: "error",
482
+ message: `Tool result from ${name} (${content.length} characters) exceeds the ` +
483
+ `maximum allowed size (${toolResultCharacterLimit} characters). ` +
484
+ "Retry with narrower arguments or inspect the data in smaller portions.",
485
+ }),
486
+ };
487
+ }
524
488
  return {
525
489
  role: "tool",
526
490
  tool_call_id: callId,
527
- content: typeof result === "string" ? result : json(result),
491
+ content,
528
492
  };
529
493
  }
530
494
  catch (error) {
@@ -555,52 +519,6 @@ export class ToolExecutor {
555
519
  content: [{ text: visualIntroText }, ...images],
556
520
  };
557
521
  }
558
- async workspaceSnapshot() {
559
- const files = [];
560
- let capturedBytes = 0;
561
- const perFileLimit = Math.max(1024 * 1024, Number(process.env.ARENA_DB_FILE_LIMIT_BYTES || 8 * 1024 * 1024));
562
- const totalLimit = Math.max(perFileLimit, Number(process.env.ARENA_DB_WORKSPACE_LIMIT_BYTES || 32 * 1024 * 1024));
563
- const visit = async (directory) => {
564
- if (files.length >= 2_000)
565
- return;
566
- const entries = await readdir(directory, { withFileTypes: true });
567
- for (const entry of entries) {
568
- if (files.length >= 2_000)
569
- break;
570
- if (entry.isSymbolicLink())
571
- continue;
572
- const target = path.join(directory, entry.name);
573
- if (entry.isDirectory()) {
574
- if (!workspaceSnapshotExcludedDirectories.has(entry.name)) {
575
- await visit(target);
576
- }
577
- continue;
578
- }
579
- if (!entry.isFile() ||
580
- workspaceSnapshotExcludedFiles.has(entry.name)) {
581
- continue;
582
- }
583
- const metadata = await stat(target);
584
- const relative = this.relativePath(target);
585
- if (metadata.size > perFileLimit ||
586
- capturedBytes + metadata.size > totalLimit) {
587
- throw new Error(`workspace database snapshot limit exceeded at ${relative}`);
588
- }
589
- const data = await readFile(target);
590
- capturedBytes += data.length;
591
- const kind = workspaceFileKind(target, data.subarray(0, 4096));
592
- const mimeType = mime.getType(target) || "application/octet-stream";
593
- files.push({
594
- path: relative,
595
- content: kind === "text"
596
- ? decodeText(data)
597
- : `data:${mimeType};base64,${data.toString("base64")}`,
598
- });
599
- }
600
- };
601
- await visit(this.workspaceReal);
602
- return files;
603
- }
604
522
  async dispatch(name, values, toolCallId) {
605
523
  switch (name) {
606
524
  case "bash":
@@ -822,13 +740,6 @@ export class ToolExecutor {
822
740
  if (this.askUserHandler) {
823
741
  return this.askUserHandler(toolCallId, questions);
824
742
  }
825
- if (this.deferUserInput) {
826
- return {
827
- status: "needs_input",
828
- questions,
829
- message: "The current request is waiting for interactive input.",
830
- };
831
- }
832
743
  // ask_user is not offered in non-interactive sessions; if the model still
833
744
  // emits it (e.g. resumed history or a hallucinated call), report an error
834
745
  // so the turn continues instead of stalling on needs_input.
@@ -859,9 +770,23 @@ export class ToolExecutor {
859
770
  }
860
771
  async readWorkspaceFile(rawPath, toolCallId) {
861
772
  const target = await this.confinedPath(rawPath);
862
- let data;
773
+ let fileSize;
774
+ let sample;
863
775
  try {
864
- data = await readFile(target);
776
+ const metadata = await stat(target);
777
+ if (!metadata.isFile())
778
+ throw new Error("not a regular file");
779
+ fileSize = metadata.size;
780
+ sample = Buffer.alloc(Math.min(fileSize, 4096));
781
+ if (sample.length) {
782
+ const handle = await open(target, "r");
783
+ try {
784
+ await handle.read(sample, 0, sample.length, 0);
785
+ }
786
+ finally {
787
+ await handle.close();
788
+ }
789
+ }
865
790
  }
866
791
  catch {
867
792
  // 逐字对齐生产错误文案,路径按调用方原样回显。
@@ -871,7 +796,29 @@ export class ToolExecutor {
871
796
  };
872
797
  }
873
798
  const extension = path.extname(target).toLowerCase();
874
- const kind = workspaceFileKind(target, data.subarray(0, 4096));
799
+ const kind = workspaceFileKind(target, sample);
800
+ if (kind === "text" && fileSize > textFileByteLimit) {
801
+ return {
802
+ status: "error",
803
+ message: `File content (${displayByteSize(fileSize)}) exceeds the maximum allowed size ` +
804
+ `(${displayByteSize(textFileByteLimit)}). Use bash with sed/head/tail to read ` +
805
+ "specific portions of the file, or grep/rg to search for specific content " +
806
+ "instead of reading the whole file.",
807
+ };
808
+ }
809
+ if (kind === "image" && fileSize > 20 * 1024 * 1024) {
810
+ throw new Error(`image exceeds 20 MB: ${rawPath}`);
811
+ }
812
+ let data;
813
+ try {
814
+ data = await readFile(target);
815
+ }
816
+ catch {
817
+ return {
818
+ status: "error",
819
+ message: `Failed to read workspace file: ${rawPath}`,
820
+ };
821
+ }
875
822
  if (isStructuredDocument(target)) {
876
823
  // 文档提取无参考样本(known-unknown),保留本地扩展键。
877
824
  const result = {
@@ -909,15 +856,12 @@ export class ToolExecutor {
909
856
  mimeTypes[extension] ||
910
857
  "application/octet-stream";
911
858
  if (kind === "image") {
912
- if (data.length > 20 * 1024 * 1024) {
913
- throw new Error(`image exceeds 20 MB: ${rawPath}`);
914
- }
915
859
  this.pendingVisualContent.set(toolCallId, [
916
860
  {
917
861
  image: `data:${mimeType};base64,${data.toString("base64")}`,
918
862
  },
919
863
  ]);
920
- return [{ text: imageResultPlaceholder }];
864
+ return [{ type: "text", text: imageResultPlaceholder }];
921
865
  }
922
866
  return {
923
867
  status: "success",
@@ -981,13 +925,13 @@ export class ToolExecutor {
981
925
  };
982
926
  }
983
927
  async imageSearch(query, count) {
984
- const model = process.env.ARENA_IMAGE_SEARCH_MODEL || "qwen3.7-flash";
928
+ const model = process.env.ARENA_IMAGE_SEARCH_MODEL || "qwen3.7-plus";
985
929
  const payload = await dashScopeJson("compatible", "/responses", {
986
930
  method: "POST",
987
931
  headers: { "content-type": "application/json" },
988
932
  body: JSON.stringify({
989
933
  model,
990
- input: `Search the web for images matching this query and return image search results: ${query}`,
934
+ input: query,
991
935
  tools: [{ type: "web_search_image" }],
992
936
  }),
993
937
  signal: this.signal,
@@ -1015,53 +959,40 @@ export class ToolExecutor {
1015
959
  .toLowerCase()
1016
960
  .replace(/[^a-z0-9]+/g, "-")
1017
961
  .slice(0, 40);
1018
- const downloaded = await Promise.all(results.map(async (value) => {
962
+ for (const value of results) {
963
+ if (saved.length >= count)
964
+ break;
1019
965
  const item = record(value);
1020
966
  const sourceUrl = stringValue(item.url) ||
1021
967
  stringValue(item.original_url) ||
1022
968
  stringValue(item.image_url) ||
1023
969
  stringValue(item.thumbnail_url);
1024
970
  if (!sourceUrl)
1025
- return undefined;
971
+ continue;
1026
972
  try {
1027
- const download = requestSignal(this.signal, positiveInteger(process.env.ARENA_IMAGE_DOWNLOAD_TIMEOUT_MS, 15_000));
1028
- const imageResponse = await fetch(sourceUrl, {
1029
- signal: download.signal,
1030
- });
973
+ const imageResponse = await fetch(sourceUrl, { signal: this.signal });
1031
974
  if (!imageResponse.ok)
1032
- return undefined;
975
+ continue;
1033
976
  const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
1034
977
  if (!contentType.toLowerCase().startsWith("image/"))
1035
- return undefined;
978
+ continue;
1036
979
  const normalizedType = contentType.split(";")[0]?.trim().toLowerCase();
1037
980
  const extension = Object.entries(mimeTypes).find(([, mime]) => mime === normalizedType)?.[0] || ".jpg";
1038
- return {
981
+ const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${extension}`);
982
+ mkdirSync(path.dirname(target), { recursive: true });
983
+ await writeFile(target, await responseBytes(imageResponse));
984
+ saved.push({
1039
985
  title: stringValue(item.title),
986
+ file_path: this.relativePath(target),
1040
987
  source_url: sourceUrl,
1041
988
  thumbnail_url: stringValue(item.thumbnail_url) || sourceUrl,
1042
- extension,
1043
- bytes: await responseBytes(imageResponse),
1044
- };
989
+ });
1045
990
  }
1046
991
  catch (error) {
1047
992
  if (this.signal?.aborted)
1048
993
  throw error;
1049
994
  // A single inaccessible search result should not fail the whole search.
1050
- return undefined;
1051
995
  }
1052
- }));
1053
- for (const image of downloaded) {
1054
- if (!image || saved.length >= count)
1055
- continue;
1056
- const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${image.extension}`);
1057
- mkdirSync(path.dirname(target), { recursive: true });
1058
- await writeFile(target, image.bytes);
1059
- saved.push({
1060
- title: image.title,
1061
- file_path: this.relativePath(target),
1062
- source_url: image.source_url,
1063
- thumbnail_url: image.thumbnail_url,
1064
- });
1065
996
  }
1066
997
  return { status: "success", results: saved };
1067
998
  }
@@ -1077,7 +1008,8 @@ export class ToolExecutor {
1077
1008
  throw new Error("file_path must end in .jpg, .jpeg, or .png");
1078
1009
  }
1079
1010
  mkdirSync(path.dirname(target), { recursive: true });
1080
- const model = process.env.ARENA_IMAGE_MODEL || "wan2.7-image-pro";
1011
+ const model = process.env.ARENA_IMAGE_MODEL || "qwen-image-2.0-pro";
1012
+ const isQwenImage2 = model.startsWith("qwen-image-2.0");
1081
1013
  const content = [];
1082
1014
  for (const imagePath of images || []) {
1083
1015
  const source = await this.confinedPath(imagePath);
@@ -1096,18 +1028,28 @@ export class ToolExecutor {
1096
1028
  }
1097
1029
  content.push({ text: prompt });
1098
1030
  const parameters = {
1099
- size: process.env.ARENA_IMAGE_SIZE || "2K",
1100
- n: 1,
1031
+ size: process.env.ARENA_IMAGE_SIZE ||
1032
+ (isQwenImage2 ? "2048*2048" : "2K"),
1101
1033
  watermark: false,
1102
1034
  };
1103
- if (!images?.length)
1104
- parameters.thinking_mode = true;
1105
- const submitted = await dashScopeJson("api", "/services/aigc/image-generation/generation", {
1035
+ if (isQwenImage2) {
1036
+ parameters.prompt_extend = true;
1037
+ }
1038
+ else {
1039
+ parameters.n = 1;
1040
+ if (!images?.length)
1041
+ parameters.thinking_mode = true;
1042
+ }
1043
+ const headers = {
1044
+ "content-type": "application/json",
1045
+ };
1046
+ if (!isQwenImage2)
1047
+ headers["x-dashscope-async"] = "enable";
1048
+ const submitted = await dashScopeJson("api", isQwenImage2
1049
+ ? "/services/aigc/multimodal-generation/generation"
1050
+ : "/services/aigc/image-generation/generation", {
1106
1051
  method: "POST",
1107
- headers: {
1108
- "content-type": "application/json",
1109
- "x-dashscope-async": "enable",
1110
- },
1052
+ headers,
1111
1053
  body: JSON.stringify({
1112
1054
  model,
1113
1055
  input: {
@@ -1117,31 +1059,38 @@ export class ToolExecutor {
1117
1059
  }),
1118
1060
  signal: this.signal,
1119
1061
  }, "generate_image");
1120
- const taskId = stringValue(record(submitted.output).task_id);
1121
- if (!taskId) {
1122
- throw new Error(`generate_image returned no task_id: ${responseMessage(submitted)}`);
1123
- }
1124
- const timeoutMs = Number(process.env.DASHSCOPE_TASK_TIMEOUT_MS || 5 * 60 * 1000);
1125
- const pollIntervalMs = Number(process.env.DASHSCOPE_TASK_POLL_INTERVAL_MS || 1000);
1126
- const deadline = Date.now() + timeoutMs;
1127
- let completed;
1128
- while (Date.now() <= deadline) {
1129
- const task = await dashScopeJson("api", `/tasks/${encodeURIComponent(taskId)}`, { method: "GET", signal: this.signal }, "generate_image status");
1130
- const output = record(task.output);
1131
- const taskStatus = stringValue(output.task_status).toUpperCase();
1132
- if (taskStatus === "SUCCEEDED") {
1133
- completed = task;
1134
- break;
1062
+ let completed = isQwenImage2
1063
+ ? submitted
1064
+ : undefined;
1065
+ if (!isQwenImage2) {
1066
+ const taskId = stringValue(record(submitted.output).task_id);
1067
+ if (!taskId) {
1068
+ throw new Error(`generate_image returned no task_id: ${responseMessage(submitted)}`);
1069
+ }
1070
+ const timeoutMs = Number(process.env.DASHSCOPE_TASK_TIMEOUT_MS || 5 * 60 * 1000);
1071
+ const pollIntervalMs = Number(process.env.DASHSCOPE_TASK_POLL_INTERVAL_MS || 1000);
1072
+ const deadline = Date.now() + timeoutMs;
1073
+ while (Date.now() <= deadline) {
1074
+ const task = await dashScopeJson("api", `/tasks/${encodeURIComponent(taskId)}`, { method: "GET", signal: this.signal }, "generate_image status");
1075
+ const output = record(task.output);
1076
+ const taskStatus = stringValue(output.task_status).toUpperCase();
1077
+ if (taskStatus === "SUCCEEDED") {
1078
+ completed = task;
1079
+ break;
1080
+ }
1081
+ if (["FAILED", "CANCELED", "UNKNOWN"].includes(taskStatus)) {
1082
+ throw new Error(`generate_image task ${taskStatus}: ${responseMessage(task)}`);
1083
+ }
1084
+ if (Date.now() + pollIntervalMs > deadline)
1085
+ break;
1086
+ await delay(pollIntervalMs, undefined, { signal: this.signal });
1135
1087
  }
1136
- if (["FAILED", "CANCELED", "UNKNOWN"].includes(taskStatus)) {
1137
- throw new Error(`generate_image task ${taskStatus}: ${responseMessage(task)}`);
1088
+ if (!completed) {
1089
+ throw new Error(`generate_image timed out waiting for task ${taskId}`);
1138
1090
  }
1139
- if (Date.now() + pollIntervalMs > deadline)
1140
- break;
1141
- await delay(pollIntervalMs, undefined, { signal: this.signal });
1142
1091
  }
1143
1092
  if (!completed) {
1144
- throw new Error(`generate_image timed out waiting for task ${taskId}`);
1093
+ throw new Error("generate_image returned no response");
1145
1094
  }
1146
1095
  const output = record(completed.output);
1147
1096
  const choices = Array.isArray(output.choices) ? output.choices : [];
@@ -1156,7 +1105,7 @@ export class ToolExecutor {
1156
1105
  const imageUrl = stringValue(generated?.image) ||
1157
1106
  stringValue(record(legacyResults[0]).url);
1158
1107
  if (!imageUrl) {
1159
- throw new Error("generate_image task succeeded without an image URL");
1108
+ throw new Error("generate_image response did not include an image URL");
1160
1109
  }
1161
1110
  const imageResponse = await fetch(imageUrl, { signal: this.signal });
1162
1111
  if (!imageResponse.ok) {