@qwen-code/arena-agent 0.0.10-dev.0 → 0.0.10-dev.10
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.
- package/.env.example +57 -14
- package/CHANGELOG.md +228 -0
- package/README.md +240 -57
- package/dist/agent.d.ts +30 -2
- package/dist/agent.js +351 -104
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.js +23 -22
- package/dist/anthropic.js.map +1 -1
- package/dist/ask-user.js +2 -0
- package/dist/ask-user.js.map +1 -1
- package/dist/attachments.js +4 -3
- package/dist/attachments.js.map +1 -1
- package/dist/brave-search.js +1 -3
- package/dist/brave-search.js.map +1 -1
- package/dist/canonical-data.js +2 -2
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +197 -34
- package/dist/cli.js.map +1 -1
- package/dist/code-tools.d.ts +8 -2
- package/dist/code-tools.js +709 -117
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction.d.ts +49 -3
- package/dist/compaction.js +383 -170
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +17 -2
- package/dist/config.js +47 -5
- package/dist/config.js.map +1 -1
- package/dist/fetch-page.js +9 -33
- package/dist/fetch-page.js.map +1 -1
- package/dist/file-edit.d.ts +8 -0
- package/dist/file-edit.js +63 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/history.d.ts +35 -1
- package/dist/history.js +400 -16
- package/dist/history.js.map +1 -1
- package/dist/legacy-compaction.d.ts +20 -0
- package/dist/legacy-compaction.js +209 -0
- package/dist/legacy-compaction.js.map +1 -0
- package/dist/midturn-checkpoint-hook.d.ts +31 -0
- package/dist/midturn-checkpoint-hook.js +38 -0
- package/dist/midturn-checkpoint-hook.js.map +1 -0
- package/dist/mock.js +5 -0
- package/dist/mock.js.map +1 -1
- package/dist/mode.js +4 -2
- package/dist/mode.js.map +1 -1
- package/dist/model-router-media.d.ts +1 -0
- package/dist/model-router-media.js +71 -1
- package/dist/model-router-media.js.map +1 -1
- package/dist/openai-responses.js +2 -0
- package/dist/openai-responses.js.map +1 -1
- package/dist/process-manager.js +18 -11
- package/dist/process-manager.js.map +1 -1
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +6 -2
- package/dist/prompts.js.map +1 -1
- package/dist/protocol.d.ts +5 -1
- package/dist/protocol.js +24 -2
- package/dist/protocol.js.map +1 -1
- package/dist/provider.d.ts +12 -5
- package/dist/provider.js +60 -52
- package/dist/provider.js.map +1 -1
- package/dist/repetitive-output.d.ts +12 -0
- package/dist/repetitive-output.js +80 -0
- package/dist/repetitive-output.js.map +1 -0
- package/dist/replay-responses.d.ts +17 -0
- package/dist/replay-responses.js +100 -0
- package/dist/replay-responses.js.map +1 -0
- package/dist/routify-web-fetch.js +20 -7
- package/dist/routify-web-fetch.js.map +1 -1
- package/dist/sandbox-file-write.d.ts +10 -0
- package/dist/sandbox-file-write.js +58 -0
- package/dist/sandbox-file-write.js.map +1 -0
- package/dist/sdk.js +9 -1
- package/dist/sdk.js.map +1 -1
- package/dist/server.js +127 -44
- package/dist/server.js.map +1 -1
- package/dist/session-memory-prompts.d.ts +5 -0
- package/dist/session-memory-prompts.js +7 -0
- package/dist/session-memory-prompts.js.map +1 -0
- package/dist/snapshot-client.d.ts +10 -0
- package/dist/snapshot-client.js +39 -0
- package/dist/snapshot-client.js.map +1 -0
- package/dist/spec.js +18 -10
- package/dist/spec.js.map +1 -1
- package/dist/speech-audio.d.ts +9 -0
- package/dist/speech-audio.js +130 -0
- package/dist/speech-audio.js.map +1 -0
- package/dist/speech-model.d.ts +3 -0
- package/dist/speech-model.js +8 -0
- package/dist/speech-model.js.map +1 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +744 -121
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +20 -12
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +28 -3
- package/dist/voice-registry.d.ts +2 -0
- package/dist/voice-registry.js +10 -2
- package/dist/voice-registry.js.map +1 -1
- package/dist/web/app.js +17 -3
- package/dist/web/app.js.map +1 -1
- package/dist/web-fetch-output.d.ts +9 -0
- package/dist/web-fetch-output.js +38 -0
- package/dist/web-fetch-output.js.map +1 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.js +11 -6
- package/dist/workspace.js.map +1 -1
- package/docs/BINARY_DISTRIBUTION.md +27 -16
- package/docs/MODES_AND_RELEASES.md +28 -5
- package/docs/SDK.md +10 -4
- package/modes/code-arena/capture-provenance.json +57 -0
- package/modes/code-arena/full-stack/system-prompt.txt +1 -1
- package/modes/code-arena/full-stack/template/eslint.config.mjs +3 -4
- package/modes/code-arena/full-stack/template/postcss.config.mjs +2 -2
- package/modes/code-arena/full-stack/tools.json +186 -186
- package/modes/code-arena/html/system-prompt.txt +1 -1
- package/modes/code-arena/html/tools.json +123 -123
- package/modes/code-arena/react/system-prompt.txt +4 -3
- package/modes/code-arena/react/template/package.json +4 -4
- package/modes/code-arena/react/template/src/App.tsx +1 -1
- package/modes/code-arena/react/template/src/main.tsx +1 -1
- package/modes/code-arena/react/template/tsconfig.json +6 -0
- package/modes/code-arena/react/template/vite.config.ts +1 -0
- package/modes/code-arena/react/tools.json +178 -184
- package/package.json +15 -4
- package/scripts/README.md +62 -0
- package/scripts/check-changelog.mjs +66 -0
- package/vendor/audio/darwin-arm64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/darwin-arm64/LAME.LICENSE +481 -0
- package/vendor/audio/darwin-arm64/Opus.LICENSE +44 -0
- package/vendor/audio/darwin-arm64/ffmpeg.gz +0 -0
- package/vendor/audio/darwin-arm64/manifest.json +39 -0
- package/vendor/audio/darwin-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/darwin-x64/LAME.LICENSE +481 -0
- package/vendor/audio/darwin-x64/Opus.LICENSE +44 -0
- package/vendor/audio/darwin-x64/ffmpeg.gz +0 -0
- package/vendor/audio/darwin-x64/manifest.json +39 -0
- package/vendor/audio/linux-arm64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/linux-arm64/LAME.LICENSE +481 -0
- package/vendor/audio/linux-arm64/Opus.LICENSE +44 -0
- package/vendor/audio/linux-arm64/ffmpeg.gz +0 -0
- package/vendor/audio/linux-arm64/manifest.json +39 -0
- package/vendor/audio/linux-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/linux-x64/LAME.LICENSE +481 -0
- package/vendor/audio/linux-x64/Opus.LICENSE +44 -0
- package/vendor/audio/linux-x64/ffmpeg.gz +0 -0
- package/vendor/audio/linux-x64/manifest.json +39 -0
- package/vendor/audio/sources/build-audio-encoder.sh +76 -0
- package/vendor/audio/sources/ffmpeg-7.0.2.tar.xz +0 -0
- package/vendor/audio/sources/lame-3.100.tar.gz +0 -0
- package/vendor/audio/sources/opus-1.5.2.tar.gz +0 -0
- package/vendor/audio/sources/pkgconf-2.3.0.tar.xz +0 -0
- package/vendor/audio/sources/sources.lock.json +30 -0
- package/vendor/audio/win32-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/win32-x64/LAME.LICENSE +481 -0
- package/vendor/audio/win32-x64/Opus.LICENSE +44 -0
- package/vendor/audio/win32-x64/ffmpeg.gz +0 -0
- package/vendor/audio/win32-x64/manifest.json +39 -0
package/dist/code-tools.js
CHANGED
|
@@ -12,10 +12,14 @@ import iconv from "iconv-lite";
|
|
|
12
12
|
import mime from "mime/lite";
|
|
13
13
|
import { executeBraveWebSearch } from "./brave-search.js";
|
|
14
14
|
import { FetchPageReader } from "./fetch-page.js";
|
|
15
|
-
import {
|
|
15
|
+
import { replaceFirstText, replaceUniqueText } from "./file-edit.js";
|
|
16
|
+
import { detectedImageMimeType, generatedImageBytesForExtension } from "./image.js";
|
|
17
|
+
import { supportsImageInput } from "./model-capabilities.js";
|
|
16
18
|
import { generateImageWithModelRouter, } from "./model-router-media.js";
|
|
17
19
|
import { executeRoutifyWebFetch, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
20
|
+
import { replayResponseFor } from "./replay-responses.js";
|
|
18
21
|
import { toolDefinitions } from "./spec.js";
|
|
22
|
+
import { boundedWebFetchResult, truncateCodeArenaText } from "./web-fetch-output.js";
|
|
19
23
|
const knownTextApplicationMimeTypes = new Set([
|
|
20
24
|
"application/ecmascript",
|
|
21
25
|
"application/javascript",
|
|
@@ -184,6 +188,42 @@ const visibleImageExtensions = new Set([
|
|
|
184
188
|
".png",
|
|
185
189
|
".webp",
|
|
186
190
|
]);
|
|
191
|
+
const readableImageExtensions = new Set([".jpeg", ".jpg", ".png"]);
|
|
192
|
+
const maximumReadableImageBytes = 10 * 1024 * 1024;
|
|
193
|
+
function readableImageMimeType(data) {
|
|
194
|
+
if (data.length >= 8 &&
|
|
195
|
+
data.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
|
|
196
|
+
return "image/png";
|
|
197
|
+
if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
|
|
198
|
+
return "image/jpeg";
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
function hasReadableImageStructure(data, mediaType) {
|
|
202
|
+
if (detectedImageMimeType(data) !== mediaType)
|
|
203
|
+
return false;
|
|
204
|
+
if (mediaType === "image/jpeg") {
|
|
205
|
+
return data.length >= 4 && data[data.length - 2] === 0xff && data[data.length - 1] === 0xd9;
|
|
206
|
+
}
|
|
207
|
+
if (data.readUInt32BE(16) === 0 || data.readUInt32BE(20) === 0)
|
|
208
|
+
return false;
|
|
209
|
+
let offset = 8;
|
|
210
|
+
let hasImageData = false;
|
|
211
|
+
while (offset + 12 <= data.length) {
|
|
212
|
+
const length = data.readUInt32BE(offset);
|
|
213
|
+
const end = offset + 12 + length;
|
|
214
|
+
if (end > data.length)
|
|
215
|
+
return false;
|
|
216
|
+
const type = data.toString("ascii", offset + 4, offset + 8);
|
|
217
|
+
if (offset === 8 && (type !== "IHDR" || length !== 13))
|
|
218
|
+
return false;
|
|
219
|
+
if (type === "IDAT")
|
|
220
|
+
hasImageData = true;
|
|
221
|
+
if (type === "IEND")
|
|
222
|
+
return length === 0 && hasImageData;
|
|
223
|
+
offset = end;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
187
227
|
// All 39 successful, unmutated list_files captures use this exact order,
|
|
188
228
|
// independently of whether the requested path is the project root or src/.
|
|
189
229
|
// It is the seed manifest order, not an alphabetical/filesystem order.
|
|
@@ -229,6 +269,7 @@ const visualIntroText = "<code-harness-system-message>\n" +
|
|
|
229
269
|
"</code-harness-system-message>";
|
|
230
270
|
const bashOutputLimit = 20_000;
|
|
231
271
|
const lintOutputLimit = 10_000;
|
|
272
|
+
const grepLineCharacterLimit = 2_000;
|
|
232
273
|
// 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
|
|
233
274
|
// 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
|
|
234
275
|
const bashTruncationMarker = "\n... output truncated ...\n";
|
|
@@ -379,6 +420,14 @@ function workspaceFileKind(filePath, sample) {
|
|
|
379
420
|
function json(value) {
|
|
380
421
|
return JSON.stringify(value);
|
|
381
422
|
}
|
|
423
|
+
function capGrepLine(value) {
|
|
424
|
+
// Production captures cap every returned grep line independently. This is
|
|
425
|
+
// intentionally a prefix without a marker, including for the matching line.
|
|
426
|
+
return value.slice(0, grepLineCharacterLimit);
|
|
427
|
+
}
|
|
428
|
+
function capGrepLines(values) {
|
|
429
|
+
return values.map(capGrepLine);
|
|
430
|
+
}
|
|
382
431
|
function errorMessage(error) {
|
|
383
432
|
return error instanceof Error ? error.message : String(error);
|
|
384
433
|
}
|
|
@@ -392,6 +441,17 @@ function stringValue(value) {
|
|
|
392
441
|
}
|
|
393
442
|
function contentTypeForFile(filePath) {
|
|
394
443
|
const extension = path.extname(filePath).toLowerCase();
|
|
444
|
+
const capturedTypes = {
|
|
445
|
+
".py": "text/x-python",
|
|
446
|
+
".sh": "text/x-shellscript",
|
|
447
|
+
".sql": "application/sql",
|
|
448
|
+
".yaml": "application/x-yaml",
|
|
449
|
+
".yml": "application/x-yaml",
|
|
450
|
+
".mjs": "text/plain",
|
|
451
|
+
".c": "text/plain",
|
|
452
|
+
};
|
|
453
|
+
if (capturedTypes[extension])
|
|
454
|
+
return capturedTypes[extension];
|
|
395
455
|
if ([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"].includes(extension)) {
|
|
396
456
|
return "text/javascript";
|
|
397
457
|
}
|
|
@@ -450,9 +510,14 @@ function matchesGlob(filePath, pattern) {
|
|
|
450
510
|
matcher.test(path.posix.basename(normalized))));
|
|
451
511
|
});
|
|
452
512
|
}
|
|
453
|
-
function packageNameIsSafe(value) {
|
|
513
|
+
function packageNameIsSafe(value, mode) {
|
|
454
514
|
if (!value || value.startsWith("-") || /\s/.test(value))
|
|
455
515
|
return false;
|
|
516
|
+
if (mode === "react") {
|
|
517
|
+
// React captures reject version ranges before invoking npm, while accepting
|
|
518
|
+
// exact versions and tags such as latest. Keep other modes' validator intact.
|
|
519
|
+
return /^(?:@[\w.-]+\/)?[\w.-]+(?:@[\w.+-]+)?$/.test(value);
|
|
520
|
+
}
|
|
456
521
|
return /^(?:@[\w.-]+\/)?[\w.-]+(?:@(?:[\w*^<>=|.+-]+))?$/.test(value);
|
|
457
522
|
}
|
|
458
523
|
function valueAtPath(value, instancePath) {
|
|
@@ -524,6 +589,17 @@ function validationIssues(toolName, value, errors, definitions) {
|
|
|
524
589
|
continue;
|
|
525
590
|
}
|
|
526
591
|
const actual = missing ? undefined : valueAtPath(value, error.instancePath);
|
|
592
|
+
if (error.keyword === "enum") {
|
|
593
|
+
const options = record(error.params).allowedValues;
|
|
594
|
+
issues.push({
|
|
595
|
+
received: actual,
|
|
596
|
+
code: "invalid_enum_value",
|
|
597
|
+
options,
|
|
598
|
+
path: pathParts,
|
|
599
|
+
message: `Invalid enum value. Expected ${options.map((option) => `'${option}'`).join(" | ")}, received '${actual}'`,
|
|
600
|
+
});
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
527
603
|
const expected = error.keyword === "type"
|
|
528
604
|
? String(record(error.params).type || "string")
|
|
529
605
|
: requiredPropertyType(toolName, key, definitions);
|
|
@@ -638,6 +714,57 @@ function interleaveMedia(left, right) {
|
|
|
638
714
|
function normalizeMediaQuery(value) {
|
|
639
715
|
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
640
716
|
}
|
|
717
|
+
// Captured sessions call apply_patch even though no recorded request ever
|
|
718
|
+
// advertised it: six react calls (s05/s17/s33) and 260+ html calls executed
|
|
719
|
+
// successfully against the untouched 13-tool/7-tool contracts, while one h19
|
|
720
|
+
// call was rejected by a turn whose runtime lacked it. The canonical schemas
|
|
721
|
+
// stay untouched; the executor accepts the call the way the recording
|
|
722
|
+
// environment's runtime did.
|
|
723
|
+
const APPLY_PATCH_PARAMETERS = {
|
|
724
|
+
type: "object",
|
|
725
|
+
properties: {
|
|
726
|
+
input: { type: "string" },
|
|
727
|
+
},
|
|
728
|
+
required: ["input"],
|
|
729
|
+
additionalProperties: false,
|
|
730
|
+
};
|
|
731
|
+
const APPLY_PATCH_DESCRIPTION = "Apply a patch to files in the project. The patch uses the codex patch format: " +
|
|
732
|
+
"'*** Begin Patch' ... '*** End Patch', with '*** Add File: <path>' (content as " +
|
|
733
|
+
"'+'-prefixed lines), '*** Update File: <path>' ('@@'-separated hunks with ' ' " +
|
|
734
|
+
"context, '-' removed and '+' added lines), and '*** Delete File: <path>'.";
|
|
735
|
+
// CODE_HARNESS_APPLY_PATCH_TOOL opts into advertising the tool to the model.
|
|
736
|
+
// It stays out of the tool list by default while the executor still accepts
|
|
737
|
+
// the call, matching the recorded runtime.
|
|
738
|
+
function applyPatchToolAdvertised() {
|
|
739
|
+
const value = process.env.CODE_HARNESS_APPLY_PATCH_TOOL;
|
|
740
|
+
return (value !== undefined && !["0", "false", "no"].includes(value.toLowerCase()));
|
|
741
|
+
}
|
|
742
|
+
function locateHunkContext(lines, findLines, fromIndex) {
|
|
743
|
+
const limit = lines.length - findLines.length;
|
|
744
|
+
for (let start = Math.max(0, fromIndex); start <= limit; start += 1) {
|
|
745
|
+
let matched = true;
|
|
746
|
+
for (let offset = 0; offset < findLines.length; offset += 1) {
|
|
747
|
+
if (lines[start + offset] !== findLines[offset]) {
|
|
748
|
+
matched = false;
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (matched)
|
|
753
|
+
return start;
|
|
754
|
+
}
|
|
755
|
+
for (let start = Math.max(0, fromIndex); start <= limit; start += 1) {
|
|
756
|
+
let matched = true;
|
|
757
|
+
for (let offset = 0; offset < findLines.length; offset += 1) {
|
|
758
|
+
if (lines[start + offset].trim() !== findLines[offset].trim()) {
|
|
759
|
+
matched = false;
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (matched)
|
|
764
|
+
return start;
|
|
765
|
+
}
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
641
768
|
export class CodeToolExecutor {
|
|
642
769
|
config;
|
|
643
770
|
workspace;
|
|
@@ -671,15 +798,38 @@ export class CodeToolExecutor {
|
|
|
671
798
|
this.workspaceContext = options.workspaceContext;
|
|
672
799
|
this.pageReader = new FetchPageReader({ signal: options.signal });
|
|
673
800
|
const disallowed = new Set(config.disallowedTools || []);
|
|
674
|
-
|
|
801
|
+
const canonical = toolDefinitions(this.workspaceContext, config.mode).filter((definition) => !disallowed.has(definition.function.name));
|
|
802
|
+
const advertiseApplyPatch = applyPatchToolAdvertised() && !disallowed.has("apply_patch");
|
|
803
|
+
this.definitions = advertiseApplyPatch
|
|
804
|
+
? [
|
|
805
|
+
...canonical,
|
|
806
|
+
{
|
|
807
|
+
type: "function",
|
|
808
|
+
function: {
|
|
809
|
+
name: "apply_patch",
|
|
810
|
+
description: APPLY_PATCH_DESCRIPTION,
|
|
811
|
+
parameters: APPLY_PATCH_PARAMETERS,
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
]
|
|
815
|
+
: canonical;
|
|
675
816
|
this.seedFileRank = capturedSeedFileRank(config.mode);
|
|
676
817
|
addFormats(this.ajv);
|
|
677
818
|
for (const definition of this.definitions) {
|
|
678
819
|
this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
|
|
679
820
|
}
|
|
821
|
+
// Hidden mode: advertise nothing, but keep the call executable.
|
|
822
|
+
if (!advertiseApplyPatch && !disallowed.has("apply_patch")) {
|
|
823
|
+
this.validators.set("apply_patch", this.ajv.compile(APPLY_PATCH_PARAMETERS));
|
|
824
|
+
}
|
|
825
|
+
// Captured React/HTML histories call this hidden tool. No public schema was
|
|
826
|
+
// captured: accept its object payload without adding model-visible tools.
|
|
827
|
+
if (config.mode !== "full-stack" && !disallowed.has("update_plan")) {
|
|
828
|
+
this.validators.set("update_plan", this.ajv.compile({ type: "object" }));
|
|
829
|
+
}
|
|
680
830
|
}
|
|
681
|
-
// The
|
|
682
|
-
//
|
|
831
|
+
// The tools offered to the model for this session, with captured schemas
|
|
832
|
+
// and descriptions adjusted for current capabilities and workspace runtime.
|
|
683
833
|
availableTools() {
|
|
684
834
|
return structuredClone(this.definitions);
|
|
685
835
|
}
|
|
@@ -687,6 +837,128 @@ export class CodeToolExecutor {
|
|
|
687
837
|
const names = this.definitions.map((tool) => tool.function.name);
|
|
688
838
|
return `Model tried to call unavailable tool '${name}'. Available tools: ${names.join(", ")}.`;
|
|
689
839
|
}
|
|
840
|
+
rawArguments(call) {
|
|
841
|
+
return typeof call.function?.arguments === "string"
|
|
842
|
+
? call.function.arguments
|
|
843
|
+
: "{}";
|
|
844
|
+
}
|
|
845
|
+
normalizeArguments(name, parsedArguments) {
|
|
846
|
+
// Keep the parsed value available to checkpoint hooks while applying the
|
|
847
|
+
// same captured runtime defaults to the independent dispatch value.
|
|
848
|
+
const argumentsValue = (this.config.mode === "react" && typeof parsedArguments === "string"
|
|
849
|
+
? {}
|
|
850
|
+
: structuredClone(parsedArguments));
|
|
851
|
+
// The captured provider advertises these fields as required, but its
|
|
852
|
+
// runtime still applies the documented defaults when a model omits
|
|
853
|
+
// them. Preserve the byte-identical outgoing schema while matching the
|
|
854
|
+
// observed execution behavior.
|
|
855
|
+
if (name === "read_file") {
|
|
856
|
+
argumentsValue.offset ??= 1;
|
|
857
|
+
argumentsValue.limit ??= 2000;
|
|
858
|
+
}
|
|
859
|
+
else if (name === "list_files" || name === "glob_files") {
|
|
860
|
+
argumentsValue.path ??= "";
|
|
861
|
+
}
|
|
862
|
+
else if (name === "bash" &&
|
|
863
|
+
argumentsValue && typeof argumentsValue === "object" &&
|
|
864
|
+
!Array.isArray(argumentsValue)) {
|
|
865
|
+
if (argumentsValue.workdir === undefined)
|
|
866
|
+
argumentsValue.workdir = "";
|
|
867
|
+
if (argumentsValue.description === undefined)
|
|
868
|
+
argumentsValue.description = "";
|
|
869
|
+
}
|
|
870
|
+
else if (name === "grep_files") {
|
|
871
|
+
argumentsValue.path ??= "";
|
|
872
|
+
argumentsValue.glob ??= "";
|
|
873
|
+
argumentsValue.output_mode ??= "content";
|
|
874
|
+
argumentsValue["-i"] ??= false;
|
|
875
|
+
argumentsValue["-B"] ??= 0;
|
|
876
|
+
argumentsValue["-A"] ??= 0;
|
|
877
|
+
argumentsValue["-C"] ??= 0;
|
|
878
|
+
argumentsValue.context ??= 0;
|
|
879
|
+
}
|
|
880
|
+
return argumentsValue;
|
|
881
|
+
}
|
|
882
|
+
validationMessage(name, value, validate) {
|
|
883
|
+
if (validate(value))
|
|
884
|
+
return undefined;
|
|
885
|
+
const issues = validationIssues(name, value, validate.errors || [], this.definitions);
|
|
886
|
+
return (`Invalid input for tool ${name}: Type validation failed: ` +
|
|
887
|
+
`Value: ${JSON.stringify(value)}.\n` +
|
|
888
|
+
`Error message: ${JSON.stringify(issues, null, 2)}`);
|
|
889
|
+
}
|
|
890
|
+
inspect(call) {
|
|
891
|
+
const id = String(call.id || "");
|
|
892
|
+
const name = String(call.function?.name || "");
|
|
893
|
+
const rawArguments = this.rawArguments(call);
|
|
894
|
+
const validate = this.validators.get(name);
|
|
895
|
+
if (!validate) {
|
|
896
|
+
let parsedArguments = undefined;
|
|
897
|
+
let parsed = false;
|
|
898
|
+
try {
|
|
899
|
+
parsedArguments = JSON.parse(rawArguments);
|
|
900
|
+
parsed = true;
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
// Tool-name validation takes precedence. Parsing is best-effort here
|
|
904
|
+
// only so a valid argument payload remains available to the hook.
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
id,
|
|
908
|
+
name,
|
|
909
|
+
rawArguments,
|
|
910
|
+
...(parsed ? { arguments: parsedArguments } : {}),
|
|
911
|
+
validation: "unknown_tool",
|
|
912
|
+
validationMessage: this.unavailableToolMessage(name),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
let parsedArguments;
|
|
916
|
+
try {
|
|
917
|
+
parsedArguments = JSON.parse(rawArguments);
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
return {
|
|
921
|
+
id,
|
|
922
|
+
name,
|
|
923
|
+
rawArguments,
|
|
924
|
+
validation: "invalid_json",
|
|
925
|
+
validationMessage: `Invalid input for tool ${name}: JSON parsing failed: Text: ` +
|
|
926
|
+
`${rawArguments}.\nError message: ${errorMessage(error)}`,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
let argumentsValue;
|
|
930
|
+
try {
|
|
931
|
+
argumentsValue = this.normalizeArguments(name, parsedArguments);
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
return {
|
|
935
|
+
id,
|
|
936
|
+
name,
|
|
937
|
+
rawArguments,
|
|
938
|
+
arguments: parsedArguments,
|
|
939
|
+
validation: "invalid_arguments",
|
|
940
|
+
validationMessage: errorMessage(error),
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const validationMessage = this.validationMessage(name, argumentsValue, validate);
|
|
944
|
+
if (validationMessage !== undefined) {
|
|
945
|
+
return {
|
|
946
|
+
id,
|
|
947
|
+
name,
|
|
948
|
+
rawArguments,
|
|
949
|
+
arguments: parsedArguments,
|
|
950
|
+
validation: "invalid_arguments",
|
|
951
|
+
validationMessage,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
id,
|
|
956
|
+
name,
|
|
957
|
+
rawArguments,
|
|
958
|
+
arguments: parsedArguments,
|
|
959
|
+
validation: "valid",
|
|
960
|
+
};
|
|
961
|
+
}
|
|
690
962
|
async execute(call, _context) {
|
|
691
963
|
const callId = String(call.id || "");
|
|
692
964
|
const name = String(call.function?.name || "");
|
|
@@ -695,27 +967,8 @@ export class CodeToolExecutor {
|
|
|
695
967
|
// 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
|
|
696
968
|
// 之前短路,否则命令会照常 spawn 直到自然结束。
|
|
697
969
|
this.signal?.throwIfAborted();
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
: "{}");
|
|
701
|
-
// The captured provider advertises these fields as required, but its
|
|
702
|
-
// runtime still applies the documented defaults when a model omits
|
|
703
|
-
// them. Preserve the byte-identical outgoing schema while matching the
|
|
704
|
-
// observed execution behavior.
|
|
705
|
-
if (name === "read_file") {
|
|
706
|
-
argumentsValue.offset ??= 1;
|
|
707
|
-
argumentsValue.limit ??= 2000;
|
|
708
|
-
}
|
|
709
|
-
else if (name === "grep_files") {
|
|
710
|
-
argumentsValue.path ??= "";
|
|
711
|
-
argumentsValue.glob ??= "";
|
|
712
|
-
argumentsValue.output_mode ??= "content";
|
|
713
|
-
argumentsValue["-i"] ??= false;
|
|
714
|
-
argumentsValue["-B"] ??= 0;
|
|
715
|
-
argumentsValue["-A"] ??= 0;
|
|
716
|
-
argumentsValue["-C"] ??= 0;
|
|
717
|
-
argumentsValue.context ??= 0;
|
|
718
|
-
}
|
|
970
|
+
const parsedArguments = JSON.parse(this.rawArguments(call));
|
|
971
|
+
const argumentsValue = this.normalizeArguments(name, parsedArguments);
|
|
719
972
|
const validate = this.validators.get(name);
|
|
720
973
|
if (!validate) {
|
|
721
974
|
return {
|
|
@@ -724,14 +977,12 @@ export class CodeToolExecutor {
|
|
|
724
977
|
content: this.unavailableToolMessage(name),
|
|
725
978
|
};
|
|
726
979
|
}
|
|
727
|
-
|
|
728
|
-
|
|
980
|
+
const validationMessage = this.validationMessage(name, argumentsValue, validate);
|
|
981
|
+
if (validationMessage !== undefined) {
|
|
729
982
|
return {
|
|
730
983
|
role: "tool",
|
|
731
984
|
tool_call_id: callId,
|
|
732
|
-
content:
|
|
733
|
-
`Value: ${JSON.stringify(argumentsValue)}.\n` +
|
|
734
|
-
`Error message: ${JSON.stringify(issues, null, 2)}`,
|
|
985
|
+
content: validationMessage,
|
|
735
986
|
};
|
|
736
987
|
}
|
|
737
988
|
const result = await this.dispatch(name, argumentsValue, callId);
|
|
@@ -770,13 +1021,24 @@ export class CodeToolExecutor {
|
|
|
770
1021
|
};
|
|
771
1022
|
}
|
|
772
1023
|
async dispatch(name, values, _toolCallId) {
|
|
1024
|
+
// Replay mode (CODE_HARNESS_REPLAY_RESPONSES set): serve recorded outputs
|
|
1025
|
+
// verbatim for nondeterministic tools (web_search/web_fetch/fetch_media/…)
|
|
1026
|
+
// so replayed sessions stay offline and deterministic. Unlisted calls fall
|
|
1027
|
+
// through to real execution.
|
|
1028
|
+
const replayed = replayResponseFor(name, values);
|
|
1029
|
+
if (replayed)
|
|
1030
|
+
return replayed.output;
|
|
773
1031
|
switch (name) {
|
|
774
1032
|
case "read_file":
|
|
775
|
-
return this.readCodeFile(String(values.path), Number(values.offset), Number(values.limit));
|
|
1033
|
+
return this.readCodeFile(String(values.path), Number(values.offset), Number(values.limit), _toolCallId);
|
|
776
1034
|
case "create_file":
|
|
777
1035
|
return this.createCodeFile(String(values.path), String(values.content));
|
|
778
1036
|
case "edit_file":
|
|
779
1037
|
return this.editCodeFile(String(values.path), String(values.context), String(values.replacement));
|
|
1038
|
+
case "apply_patch":
|
|
1039
|
+
return this.applyCodePatch(String(values.input));
|
|
1040
|
+
case "update_plan":
|
|
1041
|
+
return { status: "success" };
|
|
780
1042
|
case "delete_file":
|
|
781
1043
|
return this.deleteCodeFile(String(values.path));
|
|
782
1044
|
case "list_files":
|
|
@@ -790,7 +1052,7 @@ export class CodeToolExecutor {
|
|
|
790
1052
|
case "build_project":
|
|
791
1053
|
return this.buildProject();
|
|
792
1054
|
case "bash":
|
|
793
|
-
return this.
|
|
1055
|
+
return this.codeBash(String(values.command), String(values.workdir));
|
|
794
1056
|
case "build_and_start":
|
|
795
1057
|
return this.buildAndStart();
|
|
796
1058
|
case "web_search":
|
|
@@ -806,8 +1068,9 @@ export class CodeToolExecutor {
|
|
|
806
1068
|
}
|
|
807
1069
|
}
|
|
808
1070
|
lexicalPath(raw) {
|
|
809
|
-
if (!raw)
|
|
810
|
-
throw new Error("
|
|
1071
|
+
if (!raw || path.isAbsolute(raw) || raw.includes("..")) {
|
|
1072
|
+
throw new Error("Path cannot be empty, start with '/', or contain '..'");
|
|
1073
|
+
}
|
|
811
1074
|
let candidate;
|
|
812
1075
|
if (!this.workspaceContext && (raw === "/home/user" || raw === "~")) {
|
|
813
1076
|
candidate = this.workspace;
|
|
@@ -852,8 +1115,7 @@ export class CodeToolExecutor {
|
|
|
852
1115
|
return path.relative(this.workspaceReal, target).split(path.sep).join("/");
|
|
853
1116
|
}
|
|
854
1117
|
rememberListedFile(filePath) {
|
|
855
|
-
if (this.seedFileRank.has(filePath) ||
|
|
856
|
-
this.listedFileOrder.has(filePath)) {
|
|
1118
|
+
if (this.seedFileRank.has(filePath) || this.listedFileOrder.has(filePath)) {
|
|
857
1119
|
return;
|
|
858
1120
|
}
|
|
859
1121
|
this.listedFileOrder.set(filePath, this.nextListedFileOrder);
|
|
@@ -888,7 +1150,7 @@ export class CodeToolExecutor {
|
|
|
888
1150
|
return -1;
|
|
889
1151
|
if (rightSeedRank !== undefined)
|
|
890
1152
|
return 1;
|
|
891
|
-
return
|
|
1153
|
+
return this.listedFileOrder.get(left) - this.listedFileOrder.get(right);
|
|
892
1154
|
});
|
|
893
1155
|
}
|
|
894
1156
|
async existsWithExactCase(rawPath) {
|
|
@@ -944,7 +1206,7 @@ export class CodeToolExecutor {
|
|
|
944
1206
|
await visit(searchRoot);
|
|
945
1207
|
return files;
|
|
946
1208
|
}
|
|
947
|
-
async readCodeFile(rawPath, offset, limit) {
|
|
1209
|
+
async readCodeFile(rawPath, offset, limit, toolCallId) {
|
|
948
1210
|
const target = await this.confinedPath(rawPath);
|
|
949
1211
|
let data;
|
|
950
1212
|
try {
|
|
@@ -958,29 +1220,80 @@ export class CodeToolExecutor {
|
|
|
958
1220
|
message: `File "${rawPath}" does not exist`,
|
|
959
1221
|
};
|
|
960
1222
|
}
|
|
1223
|
+
if (readableImageExtensions.has(path.extname(target).toLowerCase())) {
|
|
1224
|
+
const mediaType = readableImageMimeType(data);
|
|
1225
|
+
if (!mediaType) {
|
|
1226
|
+
return {
|
|
1227
|
+
status: "error",
|
|
1228
|
+
message: `"${rawPath}" is not a valid image — the file contents are not PNG or JPEG. Re-download or regenerate the file, then read it again.`,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
if (!supportsImageInput(this.config)) {
|
|
1232
|
+
return {
|
|
1233
|
+
status: "error",
|
|
1234
|
+
message: `This model cannot view images. Reference "${rawPath}" by its path instead.`,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
if (data.length > maximumReadableImageBytes) {
|
|
1238
|
+
return {
|
|
1239
|
+
status: "error",
|
|
1240
|
+
message: `Image "${rawPath}" is too large to view (${Math.round(data.length / 1024)} KB, max 10240 KB).`,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
if (!hasReadableImageStructure(data, mediaType)) {
|
|
1244
|
+
return {
|
|
1245
|
+
status: "error",
|
|
1246
|
+
message: `"${rawPath}" is not a valid image — the file contents are not PNG or JPEG. Re-download or regenerate the file, then read it again.`,
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
const relativePath = this.relativePath(target);
|
|
1250
|
+
this.pendingVisualContent.set(toolCallId, [
|
|
1251
|
+
{ image: `data:${mediaType};base64,${data.toString("base64")}` },
|
|
1252
|
+
]);
|
|
1253
|
+
return {
|
|
1254
|
+
file: {
|
|
1255
|
+
content: `[Image file ${relativePath} (${Math.round(data.length / 1024)} KB, ${mediaType})]`,
|
|
1256
|
+
contentType: "text/plain",
|
|
1257
|
+
path: relativePath,
|
|
1258
|
+
},
|
|
1259
|
+
image: { mediaType, sizeBytes: data.length },
|
|
1260
|
+
status: "success",
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
961
1263
|
if (workspaceFileKind(target, data.subarray(0, 4096)) !== "text") {
|
|
962
1264
|
return {
|
|
963
1265
|
status: "error",
|
|
964
|
-
message: `
|
|
1266
|
+
message: `Cannot read binary file "${rawPath}". Only text files and PNG/JPEG images are supported.`,
|
|
965
1267
|
};
|
|
966
1268
|
}
|
|
967
1269
|
const lines = decodeText(data).split("\n");
|
|
968
1270
|
const start = Math.max(1, Math.trunc(offset));
|
|
1271
|
+
if (start > lines.length) {
|
|
1272
|
+
return {
|
|
1273
|
+
status: "success",
|
|
1274
|
+
file: {
|
|
1275
|
+
path: this.relativePath(target),
|
|
1276
|
+
content: `Offset ${start} is out of range for this file (${lines.length} lines)`,
|
|
1277
|
+
contentType: contentTypeForFile(target),
|
|
1278
|
+
},
|
|
1279
|
+
totalLines: lines.length,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
969
1282
|
const end = Math.min(lines.length, start + Math.max(1, Math.trunc(limit)) - 1);
|
|
970
|
-
const numbered =
|
|
971
|
-
|
|
972
|
-
:
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
.join("\n");
|
|
976
|
-
const footer = start === 1 && end === lines.length
|
|
1283
|
+
const numbered = lines
|
|
1284
|
+
.slice(start - 1, end)
|
|
1285
|
+
.map((line, index) => `${start + index}: ${line}`)
|
|
1286
|
+
.join("\n");
|
|
1287
|
+
const footer = end === lines.length
|
|
977
1288
|
? `(End of file - ${lines.length} lines)`
|
|
978
1289
|
: `(Showing lines ${start}-${end} of ${lines.length}. Use offset=${end + 1} to continue.)`;
|
|
979
1290
|
return {
|
|
980
1291
|
status: "success",
|
|
981
1292
|
file: {
|
|
982
1293
|
path: this.relativePath(target),
|
|
983
|
-
content:
|
|
1294
|
+
content: this.config.mode === "react"
|
|
1295
|
+
? truncateCodeArenaText(`${numbered}\n\n${footer}`)
|
|
1296
|
+
: `${numbered}\n\n${footer}`,
|
|
984
1297
|
contentType: contentTypeForFile(target),
|
|
985
1298
|
},
|
|
986
1299
|
totalLines: lines.length,
|
|
@@ -1063,49 +1376,272 @@ export class CodeToolExecutor {
|
|
|
1063
1376
|
message: `File "${rawPath}" does not exist`,
|
|
1064
1377
|
};
|
|
1065
1378
|
}
|
|
1066
|
-
|
|
1379
|
+
// React replaces the first exact occurrence, including when an indented
|
|
1380
|
+
// context also occurs on a later, more deeply indented line. Keep the
|
|
1381
|
+
// existing uniqueness check for fuzzy fallback and the other modes.
|
|
1382
|
+
const replace = this.config.mode === "react" && context.length > 0 && source.includes(context)
|
|
1383
|
+
? replaceFirstText : replaceUniqueText;
|
|
1384
|
+
const edited = replace(source, context, replacement, {
|
|
1385
|
+
whitespaceInsensitiveMultiline: true,
|
|
1386
|
+
whitespaceInsensitiveSingleLine: this.config.mode === "react",
|
|
1387
|
+
preserveNewlines: this.config.mode === "react",
|
|
1388
|
+
});
|
|
1389
|
+
if (edited === undefined) {
|
|
1390
|
+
// Captured sessions re-issue an edit after compaction or a retry. The
|
|
1391
|
+
// provider reports success without touching the file when the context
|
|
1392
|
+
// is already gone and the replacement is in place (f39 re-issues four
|
|
1393
|
+
// dashboard edits this way).
|
|
1394
|
+
// That retry behavior comes from Full Stack captures. React still
|
|
1395
|
+
// reports Context not found even when the replacement is present.
|
|
1396
|
+
if (this.config.mode !== "react" && replacement && source.includes(replacement)) {
|
|
1397
|
+
return this.mutationResult("Edited", rawPath);
|
|
1398
|
+
}
|
|
1067
1399
|
return {
|
|
1068
1400
|
status: "error",
|
|
1069
1401
|
message: "Context not found. Read the file to verify the text exists.",
|
|
1070
1402
|
};
|
|
1071
1403
|
}
|
|
1072
|
-
await writeFile(target,
|
|
1404
|
+
await writeFile(target, edited, "utf8");
|
|
1073
1405
|
return this.mutationResult("Edited", rawPath);
|
|
1074
1406
|
}
|
|
1407
|
+
// Some recorded sessions exposed a codex-style apply_patch tool (*** Begin
|
|
1408
|
+
// Patch / Add|Update|Delete File / @@ hunks) that the provider executed
|
|
1409
|
+
// without ever advertising it. Its observable contract: "Patch applied[.
|
|
1410
|
+
// Type errors found.]" on success, "Invalid Context <anchor>:\n<context>"
|
|
1411
|
+
// when a hunk context cannot be located — the anchor is the line where the
|
|
1412
|
+
// previous hunk's match ended and the failed search started (h17 reports
|
|
1413
|
+
// 666 after a four-line context hunk matching lines 663-666).
|
|
1414
|
+
async applyCodePatch(patchText) {
|
|
1415
|
+
const kindByMarker = {
|
|
1416
|
+
Add: "add",
|
|
1417
|
+
Update: "update",
|
|
1418
|
+
Delete: "delete",
|
|
1419
|
+
};
|
|
1420
|
+
const operations = [];
|
|
1421
|
+
let current;
|
|
1422
|
+
for (const line of patchText.split("\n")) {
|
|
1423
|
+
const marker = /^\*\*\* (Add|Update|Delete) File: (.+)$/.exec(line);
|
|
1424
|
+
if (marker) {
|
|
1425
|
+
current = {
|
|
1426
|
+
kind: kindByMarker[marker[1]],
|
|
1427
|
+
path: marker[2].trim(),
|
|
1428
|
+
lines: [],
|
|
1429
|
+
};
|
|
1430
|
+
operations.push(current);
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (line.startsWith("*** ")) {
|
|
1434
|
+
current = undefined;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
current?.lines.push(line);
|
|
1438
|
+
}
|
|
1439
|
+
if (this.config.mode === "react") {
|
|
1440
|
+
// React parses duplicate operation paths before applying any writes.
|
|
1441
|
+
// In particular, Delete + Add of the same file is an Add duplicate.
|
|
1442
|
+
const seen = new Set();
|
|
1443
|
+
for (const operation of operations) {
|
|
1444
|
+
const target = await this.confinedPath(operation.path);
|
|
1445
|
+
if (seen.has(target)) {
|
|
1446
|
+
const label = operation.kind[0].toUpperCase() + operation.kind.slice(1);
|
|
1447
|
+
return { status: "error", message: `${label} File Error: Duplicate Path: ${operation.path}` };
|
|
1448
|
+
}
|
|
1449
|
+
seen.add(target);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
for (const operation of operations) {
|
|
1453
|
+
const target = await this.confinedPath(operation.path);
|
|
1454
|
+
if (operation.kind === "add") {
|
|
1455
|
+
if (await this.existsWithExactCase(operation.path)) {
|
|
1456
|
+
return {
|
|
1457
|
+
status: "error",
|
|
1458
|
+
message: `Add File Error: Duplicate Path: ${operation.path}`,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
// The recorded provider writes exactly the "+" lines joined by
|
|
1462
|
+
// newlines without appending a trailing one (h17's 27-line
|
|
1463
|
+
// manifest.json reads back as totalLines 27, not 28).
|
|
1464
|
+
const content = operation.lines
|
|
1465
|
+
.filter((line) => line.startsWith("+"))
|
|
1466
|
+
.map((line) => line.slice(1))
|
|
1467
|
+
.join("\n");
|
|
1468
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
1469
|
+
await writeFile(target, content, "utf8");
|
|
1470
|
+
this.rememberListedFile(this.relativePath(target));
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
if (operation.kind === "delete") {
|
|
1474
|
+
if (!(await this.existsWithExactCase(operation.path))) {
|
|
1475
|
+
return {
|
|
1476
|
+
status: "error",
|
|
1477
|
+
message: `Delete File Error: File Not Found: ${operation.path}`,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
await rm(target, { force: true });
|
|
1481
|
+
this.forgetListedPath(this.relativePath(target));
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
let source;
|
|
1485
|
+
try {
|
|
1486
|
+
if (!(await this.existsWithExactCase(operation.path)))
|
|
1487
|
+
throw new Error("missing");
|
|
1488
|
+
source = await readFile(target, "utf8");
|
|
1489
|
+
}
|
|
1490
|
+
catch {
|
|
1491
|
+
return {
|
|
1492
|
+
status: "error",
|
|
1493
|
+
message: `Update File Error: File Not Found: ${operation.path}`,
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
// Hunks are separated by @@ lines; within a hunk, " " context and "-"
|
|
1497
|
+
// removed lines form the find pattern, " " context and "+" added lines
|
|
1498
|
+
// form the replacement. Hunks stream over the original file: each
|
|
1499
|
+
// context is searched forward from where the previous hunk ended, and a
|
|
1500
|
+
// miss reports the search anchor over the find text the way the
|
|
1501
|
+
// recorded provider does (h17: "Invalid Context 666:"). Whitespace
|
|
1502
|
+
// drifted contexts match by trimmed lines.
|
|
1503
|
+
const hunks = [];
|
|
1504
|
+
let hunk = [];
|
|
1505
|
+
for (const line of operation.lines) {
|
|
1506
|
+
if (line.startsWith("@@")) {
|
|
1507
|
+
if (hunk.length)
|
|
1508
|
+
hunks.push(hunk);
|
|
1509
|
+
hunk = [];
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
hunk.push(line);
|
|
1513
|
+
}
|
|
1514
|
+
if (hunk.length)
|
|
1515
|
+
hunks.push(hunk);
|
|
1516
|
+
const originalLines = source.split("\n");
|
|
1517
|
+
const output = [];
|
|
1518
|
+
let position = 0;
|
|
1519
|
+
for (const hunkLines of hunks) {
|
|
1520
|
+
const findLines = [];
|
|
1521
|
+
const replaceLines = [];
|
|
1522
|
+
for (const line of hunkLines) {
|
|
1523
|
+
if (line.startsWith("-")) {
|
|
1524
|
+
findLines.push(line.slice(1));
|
|
1525
|
+
}
|
|
1526
|
+
else if (line.startsWith("+")) {
|
|
1527
|
+
replaceLines.push(line.slice(1));
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
findLines.push(line.slice(1));
|
|
1531
|
+
replaceLines.push(line.slice(1));
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (!findLines.length)
|
|
1535
|
+
continue;
|
|
1536
|
+
const start = locateHunkContext(originalLines, findLines, position);
|
|
1537
|
+
if (start === undefined) {
|
|
1538
|
+
return {
|
|
1539
|
+
status: "error",
|
|
1540
|
+
message: `Invalid Context ${position}:\n${findLines.join("\n")}`,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
output.push(...originalLines.slice(position, start));
|
|
1544
|
+
output.push(...replaceLines);
|
|
1545
|
+
position = start + findLines.length;
|
|
1546
|
+
}
|
|
1547
|
+
output.push(...originalLines.slice(position));
|
|
1548
|
+
await writeFile(target, output.join("\n"), "utf8");
|
|
1549
|
+
}
|
|
1550
|
+
const lintError = await this.typeErrors();
|
|
1551
|
+
if (lintError) {
|
|
1552
|
+
return {
|
|
1553
|
+
status: "success",
|
|
1554
|
+
message: "Patch applied. Type errors found.",
|
|
1555
|
+
lint_error: lintError,
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
return { status: "success", message: "Patch applied." };
|
|
1559
|
+
}
|
|
1075
1560
|
async deleteCodeFile(rawPath) {
|
|
1076
1561
|
const target = await this.confinedPath(rawPath);
|
|
1077
1562
|
await rm(target, { force: true, recursive: true });
|
|
1078
1563
|
this.forgetListedPath(this.relativePath(target));
|
|
1079
1564
|
return { status: "success" };
|
|
1080
1565
|
}
|
|
1566
|
+
discoverableFile(filePath) {
|
|
1567
|
+
// Full Stack captures expose the lock to shell/read_file, but omit it
|
|
1568
|
+
// from both project discovery tools, even before any package installation.
|
|
1569
|
+
return this.config.mode !== "full-stack" || this.relativePath(filePath) !== "package-lock.json";
|
|
1570
|
+
}
|
|
1081
1571
|
async listCodeFiles(rawPath) {
|
|
1572
|
+
if (this.config.mode === "react" && rawPath === "/")
|
|
1573
|
+
rawPath = ".";
|
|
1574
|
+
if (this.config.mode === "react" && (path.isAbsolute(rawPath) || rawPath.includes(".."))) {
|
|
1575
|
+
return { status: "error", message: "Path must be relative to project root (no absolute paths or '..' traversal)" };
|
|
1576
|
+
}
|
|
1082
1577
|
const target = await this.confinedPath(rawPath || ".");
|
|
1083
1578
|
if (!(await this.existsWithExactCase(rawPath || ".")) ||
|
|
1084
1579
|
!existsSync(target)) {
|
|
1085
1580
|
return { status: "success", files: [] };
|
|
1086
1581
|
}
|
|
1087
|
-
const files = this.orderListedFiles(await this.walkFiles(target));
|
|
1582
|
+
const files = this.orderListedFiles((await this.walkFiles(target)).filter(file => this.discoverableFile(file)));
|
|
1088
1583
|
return {
|
|
1089
1584
|
status: "success",
|
|
1090
|
-
files: files.
|
|
1585
|
+
files: (this.config.mode === "react" ? files.slice(0, 500) : files)
|
|
1586
|
+
.map((filePath) => ({ path: filePath })),
|
|
1091
1587
|
};
|
|
1092
1588
|
}
|
|
1093
1589
|
async grepCodeFiles(values) {
|
|
1590
|
+
const pattern = String(values.pattern);
|
|
1591
|
+
if (pattern.length > 200) {
|
|
1592
|
+
return {
|
|
1593
|
+
status: "error",
|
|
1594
|
+
message: "Pattern too long (max 200 characters)",
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1094
1597
|
const target = await this.confinedPath(String(values.path || "."));
|
|
1095
|
-
const targetInfo = await stat(target);
|
|
1096
|
-
const files = await this.walkFiles(target);
|
|
1097
1598
|
const glob = String(values.glob || "");
|
|
1599
|
+
const mode = String(values.output_mode || "content");
|
|
1098
1600
|
const flags = Boolean(values["-i"]) ? "i" : "";
|
|
1099
1601
|
let expression;
|
|
1100
1602
|
try {
|
|
1101
|
-
expression = new RegExp(
|
|
1603
|
+
expression = new RegExp(pattern, flags);
|
|
1102
1604
|
}
|
|
1103
1605
|
catch (error) {
|
|
1104
1606
|
throw new Error(`Invalid regular expression: ${errorMessage(error)}`);
|
|
1105
1607
|
}
|
|
1106
|
-
|
|
1608
|
+
let targetInfo;
|
|
1609
|
+
try {
|
|
1610
|
+
targetInfo = await stat(target);
|
|
1611
|
+
}
|
|
1612
|
+
catch (error) {
|
|
1613
|
+
if (error.code !== "ENOENT")
|
|
1614
|
+
throw error;
|
|
1615
|
+
if (mode === "files_with_matches") {
|
|
1616
|
+
return { status: "success", mode, files: [], truncated: false };
|
|
1617
|
+
}
|
|
1618
|
+
if (mode === "count") {
|
|
1619
|
+
return {
|
|
1620
|
+
status: "success",
|
|
1621
|
+
mode,
|
|
1622
|
+
counts: [],
|
|
1623
|
+
totalMatches: 0,
|
|
1624
|
+
truncated: false,
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
return {
|
|
1628
|
+
status: "success",
|
|
1629
|
+
mode: "content",
|
|
1630
|
+
matches: [],
|
|
1631
|
+
truncated: false,
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
const files = await this.walkFiles(target);
|
|
1635
|
+
// Full Stack captures expose a different context-grouping adapter from
|
|
1636
|
+
// HTML/React: -C/context are ignored, the trailing group spans B+A lines,
|
|
1637
|
+
// and a later match reuses the prior trailing group as contextBefore.
|
|
1638
|
+
const groupedContext = this.config.mode === "full-stack";
|
|
1639
|
+
const symmetric = groupedContext
|
|
1640
|
+
? 0
|
|
1641
|
+
: Math.max(Number(values.context || 0), Number(values["-C"] || 0));
|
|
1107
1642
|
const before = symmetric || Number(values["-B"] || 0);
|
|
1108
1643
|
const after = symmetric || Number(values["-A"] || 0);
|
|
1644
|
+
const contextSpan = groupedContext ? before + after : after;
|
|
1109
1645
|
const matches = [];
|
|
1110
1646
|
const matchedFiles = [];
|
|
1111
1647
|
const counts = [];
|
|
@@ -1115,6 +1651,11 @@ export class CodeToolExecutor {
|
|
|
1115
1651
|
const relativeToSearch = targetInfo.isFile()
|
|
1116
1652
|
? path.basename(filePath)
|
|
1117
1653
|
: path.relative(target, filePath).split(path.sep).join("/");
|
|
1654
|
+
if (relativeToSearch
|
|
1655
|
+
.split("/")
|
|
1656
|
+
.some((component) => component.startsWith("."))) {
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1118
1659
|
if (glob && !matchesGlob(relativeToSearch, glob))
|
|
1119
1660
|
continue;
|
|
1120
1661
|
let content;
|
|
@@ -1129,23 +1670,69 @@ export class CodeToolExecutor {
|
|
|
1129
1670
|
continue;
|
|
1130
1671
|
}
|
|
1131
1672
|
const lines = content.split("\n");
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1673
|
+
const searchLineCount = this.config.mode === "react" && lines.at(-1) === ""
|
|
1674
|
+
? lines.length - 1 : lines.length;
|
|
1675
|
+
const matchingLines = [];
|
|
1676
|
+
for (let index = 0; index < searchLineCount; index += 1) {
|
|
1677
|
+
if (expression.test(lines[index]))
|
|
1678
|
+
matchingLines.push(index);
|
|
1679
|
+
}
|
|
1680
|
+
const fileCount = matchingLines.length;
|
|
1681
|
+
let previousAfter = [];
|
|
1682
|
+
for (let position = 0; position < matchingLines.length; position += 1) {
|
|
1683
|
+
const index = matchingLines[position];
|
|
1137
1684
|
totalMatches += 1;
|
|
1138
1685
|
if (matches.length < maximumSearchResults) {
|
|
1139
1686
|
const match = {
|
|
1140
1687
|
path: relative,
|
|
1141
1688
|
lineNumber: index + 1,
|
|
1142
|
-
lineContent: lines[index],
|
|
1689
|
+
lineContent: capGrepLine(lines[index]),
|
|
1143
1690
|
};
|
|
1144
|
-
if (
|
|
1145
|
-
|
|
1691
|
+
if (groupedContext) {
|
|
1692
|
+
const contextBefore = position === 0
|
|
1693
|
+
? lines.slice(Math.max(0, index - before), index)
|
|
1694
|
+
: previousAfter;
|
|
1695
|
+
if (contextBefore.length > 0) {
|
|
1696
|
+
match.contextBefore = capGrepLines(contextBefore);
|
|
1697
|
+
}
|
|
1698
|
+
const nextMatch = matchingLines[position + 1] ?? lines.length;
|
|
1699
|
+
previousAfter = lines.slice(index + 1, Math.min(index + 1 + contextSpan, nextMatch));
|
|
1700
|
+
if (nextMatch === lines.length &&
|
|
1701
|
+
previousAfter.at(-1) === "" &&
|
|
1702
|
+
index + 1 + contextSpan >= lines.length) {
|
|
1703
|
+
previousAfter = previousAfter.slice(0, -1);
|
|
1704
|
+
}
|
|
1705
|
+
if (previousAfter.length > 0) {
|
|
1706
|
+
match.contextAfter = capGrepLines(previousAfter);
|
|
1707
|
+
}
|
|
1146
1708
|
}
|
|
1147
|
-
if (
|
|
1148
|
-
|
|
1709
|
+
else if (this.config.mode === "react") {
|
|
1710
|
+
// The captured adapter attaches context lines to the preceding
|
|
1711
|
+
// match, even across ripgrep group separators. Only a file's first
|
|
1712
|
+
// match receives contextBefore. Do not duplicate overlapping groups.
|
|
1713
|
+
if (position === 0 && before > 0) {
|
|
1714
|
+
const contextBefore = lines.slice(Math.max(0, index - before), index);
|
|
1715
|
+
if (contextBefore.length) {
|
|
1716
|
+
match.contextBefore = capGrepLines(contextBefore);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
const nextMatch = matchingLines[position + 1];
|
|
1720
|
+
const end = Math.min(index + 1 + after, nextMatch ?? searchLineCount);
|
|
1721
|
+
const contextAfter = lines.slice(index + 1, end);
|
|
1722
|
+
if (nextMatch !== undefined && before > 0) {
|
|
1723
|
+
contextAfter.push(...lines.slice(Math.max(end, index + 1, nextMatch - before), nextMatch));
|
|
1724
|
+
}
|
|
1725
|
+
if (contextAfter.length) {
|
|
1726
|
+
match.contextAfter = capGrepLines(contextAfter);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
if (before > 0) {
|
|
1731
|
+
match.contextBefore = capGrepLines(lines.slice(Math.max(0, index - before), index));
|
|
1732
|
+
}
|
|
1733
|
+
if (after > 0) {
|
|
1734
|
+
match.contextAfter = capGrepLines(lines.slice(index + 1, index + 1 + after));
|
|
1735
|
+
}
|
|
1149
1736
|
}
|
|
1150
1737
|
matches.push(match);
|
|
1151
1738
|
}
|
|
@@ -1158,7 +1745,6 @@ export class CodeToolExecutor {
|
|
|
1158
1745
|
}
|
|
1159
1746
|
}
|
|
1160
1747
|
}
|
|
1161
|
-
const mode = String(values.output_mode || "content");
|
|
1162
1748
|
if (mode === "files_with_matches") {
|
|
1163
1749
|
return {
|
|
1164
1750
|
status: "success",
|
|
@@ -1185,9 +1771,18 @@ export class CodeToolExecutor {
|
|
|
1185
1771
|
}
|
|
1186
1772
|
async globCodeFiles(pattern, rawPath) {
|
|
1187
1773
|
const target = await this.confinedPath(rawPath || ".");
|
|
1188
|
-
|
|
1774
|
+
let files;
|
|
1775
|
+
try {
|
|
1776
|
+
files = await this.walkFiles(target);
|
|
1777
|
+
}
|
|
1778
|
+
catch (error) {
|
|
1779
|
+
if (error.code !== "ENOENT")
|
|
1780
|
+
throw error;
|
|
1781
|
+
return { status: "success", paths: [], truncated: false };
|
|
1782
|
+
}
|
|
1189
1783
|
const allPaths = files
|
|
1190
|
-
.filter(
|
|
1784
|
+
.filter(file => this.discoverableFile(file))
|
|
1785
|
+
.filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern) || (this.config.mode === "react" && matchesGlob(this.relativePath(filePath), pattern)))
|
|
1191
1786
|
.map((filePath) => this.relativePath(filePath));
|
|
1192
1787
|
return {
|
|
1193
1788
|
status: "success",
|
|
@@ -1260,11 +1855,11 @@ export class CodeToolExecutor {
|
|
|
1260
1855
|
});
|
|
1261
1856
|
}
|
|
1262
1857
|
async installNpmPackages(packages) {
|
|
1263
|
-
const invalid = packages.find((packageName) => !packageNameIsSafe(packageName));
|
|
1858
|
+
const invalid = packages.find((packageName) => !packageNameIsSafe(packageName, this.config.mode));
|
|
1264
1859
|
if (invalid) {
|
|
1265
1860
|
return {
|
|
1266
1861
|
status: "error",
|
|
1267
|
-
message: `Invalid package name: ${invalid}`,
|
|
1862
|
+
message: `Invalid package name: ${this.config.mode === "react" ? JSON.stringify(invalid) : invalid}`,
|
|
1268
1863
|
};
|
|
1269
1864
|
}
|
|
1270
1865
|
const args = ["install", "-S"];
|
|
@@ -1302,24 +1897,18 @@ export class CodeToolExecutor {
|
|
|
1302
1897
|
stderr: this.virtualizeCodeOutput(result.stderr),
|
|
1303
1898
|
};
|
|
1304
1899
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const workspaceRoots = [...new Set([this.workspace, this.workspaceReal])]
|
|
1310
|
-
.filter((root) => root !== path.parse(root).root)
|
|
1311
|
-
.sort((left, right) => right.length - left.length);
|
|
1312
|
-
for (const root of workspaceRoots) {
|
|
1313
|
-
virtualized = virtualized.replaceAll(root, "/home/user");
|
|
1900
|
+
async codeBash(command, workdir) {
|
|
1901
|
+
const cwd = workdir.trim() || ".";
|
|
1902
|
+
if (path.isAbsolute(cwd) || cwd.includes("..")) {
|
|
1903
|
+
throw new Error(`Invalid workdir: ${cwd}. Commands run from the project root by default. Only use workdir for relative subdirectories, never absolute paths.`);
|
|
1314
1904
|
}
|
|
1315
|
-
return virtualized;
|
|
1316
|
-
}
|
|
1317
|
-
async fullStackBash(command, workdir) {
|
|
1318
1905
|
const configuredTimeout = Number(process.env.CODE_HARNESS_BASH_TIMEOUT_SECONDS || 300);
|
|
1319
1906
|
const timeoutSeconds = Number.isFinite(configuredTimeout) && configuredTimeout > 0
|
|
1320
1907
|
? configuredTimeout
|
|
1321
1908
|
: 300;
|
|
1322
|
-
const result = await this.bash(command,
|
|
1909
|
+
const result = await this.bash(command, cwd, timeoutSeconds);
|
|
1910
|
+
if (result.timed_out)
|
|
1911
|
+
return { status: "error", message: "Command timed out" };
|
|
1323
1912
|
const exitCode = result.exit_code;
|
|
1324
1913
|
const stdout = String(result.stdout || "");
|
|
1325
1914
|
const stderr = String(result.stderr || "");
|
|
@@ -1454,12 +2043,6 @@ export class CodeToolExecutor {
|
|
|
1454
2043
|
if (!(await stat(workdir)).isDirectory()) {
|
|
1455
2044
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
1456
2045
|
}
|
|
1457
|
-
// Runtime-aware callers execute commands exactly as written and report
|
|
1458
|
-
// real local paths. The legacy /home/user translation is limited to the
|
|
1459
|
-
// older non-Full-Stack replay contract.
|
|
1460
|
-
const translatedCommand = this.workspaceContext || this.config.mode === "full-stack"
|
|
1461
|
-
? command
|
|
1462
|
-
: command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
|
|
1463
2046
|
return new Promise((resolve, reject) => {
|
|
1464
2047
|
const env = {
|
|
1465
2048
|
...process.env,
|
|
@@ -1477,7 +2060,7 @@ export class CodeToolExecutor {
|
|
|
1477
2060
|
// detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
|
|
1478
2061
|
// 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
|
|
1479
2062
|
// 必须杀整组,并在宽限期后强制结算兜底。
|
|
1480
|
-
const child = spawn("/bin/bash", ["-c",
|
|
2063
|
+
const child = spawn("/bin/bash", ["-c", command], {
|
|
1481
2064
|
cwd: workdir,
|
|
1482
2065
|
env,
|
|
1483
2066
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1525,19 +2108,14 @@ export class CodeToolExecutor {
|
|
|
1525
2108
|
reject(this.signal.reason || new Error("operation cancelled"));
|
|
1526
2109
|
return;
|
|
1527
2110
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
if (timedOut && this.config.mode !== "full-stack") {
|
|
1531
|
-
stderr +=
|
|
1532
|
-
(stderr ? "\n" : "") + `command exceeded ${timeoutSeconds}s`;
|
|
1533
|
-
}
|
|
1534
|
-
const out = capBashOutput(this.virtualizeBashOutput(stdout));
|
|
1535
|
-
const err = capBashOutput(this.virtualizeBashOutput(stderr));
|
|
2111
|
+
const out = capBashOutput(stdout);
|
|
2112
|
+
const err = capBashOutput(stderr);
|
|
1536
2113
|
resolve({
|
|
1537
2114
|
status: "completed",
|
|
1538
2115
|
stderr: err.text,
|
|
1539
2116
|
stdout: out.text,
|
|
1540
2117
|
exit_code: timedOut ? null : code,
|
|
2118
|
+
timed_out: timedOut,
|
|
1541
2119
|
duration_ms: Date.now() - startedAt,
|
|
1542
2120
|
stderr_truncated: err.truncated,
|
|
1543
2121
|
stdout_truncated: out.truncated,
|
|
@@ -1596,11 +2174,7 @@ export class CodeToolExecutor {
|
|
|
1596
2174
|
const result = routifyWebFetchEnabled()
|
|
1597
2175
|
? await executeRoutifyWebFetch(url, format, { signal: this.signal })
|
|
1598
2176
|
: await this.pageReader.readFormatted(url, format);
|
|
1599
|
-
return
|
|
1600
|
-
status: "success",
|
|
1601
|
-
title: String(result.title || "No Title Found"),
|
|
1602
|
-
content: String(result.content || ""),
|
|
1603
|
-
};
|
|
2177
|
+
return boundedWebFetchResult(result);
|
|
1604
2178
|
}
|
|
1605
2179
|
async pexelsRequest(pathname, parameters) {
|
|
1606
2180
|
const apiKey = process.env.PEXELS_API_KEY;
|
|
@@ -1639,7 +2213,7 @@ export class CodeToolExecutor {
|
|
|
1639
2213
|
},
|
|
1640
2214
|
});
|
|
1641
2215
|
}
|
|
1642
|
-
mapPexelsPhotos(payload) {
|
|
2216
|
+
mapPexelsPhotos(payload, orientation) {
|
|
1643
2217
|
const photos = Array.isArray(payload.photos) ? payload.photos : [];
|
|
1644
2218
|
return photos.map((value) => {
|
|
1645
2219
|
const photo = record(value);
|
|
@@ -1648,12 +2222,14 @@ export class CodeToolExecutor {
|
|
|
1648
2222
|
type: "image",
|
|
1649
2223
|
id: Number(photo.id),
|
|
1650
2224
|
pexelsUrl: stringValue(photo.url),
|
|
1651
|
-
recommendedUrl:
|
|
2225
|
+
recommendedUrl: (orientation === "landscape" || orientation === "portrait"
|
|
2226
|
+
? stringValue(source[orientation]) : "") ||
|
|
2227
|
+
stringValue(source.large2x) ||
|
|
1652
2228
|
stringValue(source.large) ||
|
|
1653
2229
|
stringValue(source.original),
|
|
1654
|
-
thumbnailUrl: stringValue(source.
|
|
1655
|
-
stringValue(source.
|
|
1656
|
-
stringValue(source.
|
|
2230
|
+
thumbnailUrl: stringValue(source.tiny) ||
|
|
2231
|
+
stringValue(source.medium) ||
|
|
2232
|
+
stringValue(source.small),
|
|
1657
2233
|
width: Number(photo.width),
|
|
1658
2234
|
height: Number(photo.height),
|
|
1659
2235
|
creatorName: stringValue(photo.photographer),
|
|
@@ -1673,20 +2249,29 @@ export class CodeToolExecutor {
|
|
|
1673
2249
|
const files = Array.isArray(video.video_files)
|
|
1674
2250
|
? video.video_files.map(record)
|
|
1675
2251
|
: [];
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2252
|
+
// Captures contain full-resolution HD/UHD files. A quality label alone
|
|
2253
|
+
// does not order renditions; compare their actual pixel dimensions.
|
|
2254
|
+
const playable = files.filter((item) => stringValue(item.file_type) === "video/mp4" && stringValue(item.link));
|
|
2255
|
+
const preferred = playable.sort((left, right) => (Number(right.width) || 0) * (Number(right.height) || 0) -
|
|
2256
|
+
(Number(left.width) || 0) * (Number(left.height) || 0))[0] || files[0] || {};
|
|
1679
2257
|
return {
|
|
1680
2258
|
type: "video",
|
|
1681
2259
|
id: Number(video.id),
|
|
1682
2260
|
pexelsUrl: stringValue(video.url),
|
|
1683
2261
|
recommendedUrl: stringValue(preferred.link),
|
|
1684
|
-
thumbnailUrl: stringValue(pictures[0]?.picture),
|
|
2262
|
+
thumbnailUrl: stringValue(video.image) || stringValue(pictures[0]?.picture),
|
|
1685
2263
|
width: Number(video.width),
|
|
1686
2264
|
height: Number(video.height),
|
|
1687
2265
|
duration: Number(video.duration),
|
|
1688
2266
|
creatorName: stringValue(user.name),
|
|
1689
2267
|
creatorUrl: stringValue(user.url),
|
|
2268
|
+
videoFile: {
|
|
2269
|
+
url: stringValue(preferred.link),
|
|
2270
|
+
width: Number(preferred.width),
|
|
2271
|
+
height: Number(preferred.height),
|
|
2272
|
+
...(preferred.quality !== undefined ? { quality: preferred.quality } : {}),
|
|
2273
|
+
fileType: stringValue(preferred.file_type),
|
|
2274
|
+
},
|
|
1690
2275
|
};
|
|
1691
2276
|
});
|
|
1692
2277
|
}
|
|
@@ -1704,7 +2289,7 @@ export class CodeToolExecutor {
|
|
|
1704
2289
|
if (mediaType === "image" || mediaType === "both") {
|
|
1705
2290
|
const photos = await this.pexelsRequest("/v1/search", common);
|
|
1706
2291
|
totalResults += Number(photos.total_results || 0);
|
|
1707
|
-
photoResults = this.mapPexelsPhotos(photos);
|
|
2292
|
+
photoResults = this.mapPexelsPhotos(photos, orientation);
|
|
1708
2293
|
}
|
|
1709
2294
|
if (mediaType === "video" || mediaType === "both") {
|
|
1710
2295
|
const videos = await this.pexelsRequest("/v1/videos/search", common);
|
|
@@ -1726,13 +2311,20 @@ export class CodeToolExecutor {
|
|
|
1726
2311
|
}
|
|
1727
2312
|
async fetchMedia(query, mediaType, count, orientation, size, locale) {
|
|
1728
2313
|
const requestedCount = Math.min(10, Math.max(1, Math.trunc(count)));
|
|
1729
|
-
|
|
2314
|
+
try {
|
|
2315
|
+
return await this.fetchMediaFromPexels(query, mediaType, requestedCount, orientation, size, locale);
|
|
2316
|
+
}
|
|
2317
|
+
catch (error) {
|
|
2318
|
+
throw new Error(`Media fetch failed: ${errorMessage(error)}`);
|
|
2319
|
+
}
|
|
1730
2320
|
}
|
|
1731
2321
|
async generateImage(rawPath, prompt, images) {
|
|
1732
2322
|
if (this.generatedImageCount >= 10) {
|
|
1733
2323
|
return {
|
|
1734
2324
|
status: "error",
|
|
1735
|
-
message:
|
|
2325
|
+
message: this.config.mode === "react"
|
|
2326
|
+
? "Failed to generate image: Image generation limit of 10 reached for this session. Use CSS gradients, SVG, or placeholder images for remaining visuals."
|
|
2327
|
+
: "generate_image can be used at most 10 times per session",
|
|
1736
2328
|
};
|
|
1737
2329
|
}
|
|
1738
2330
|
this.generatedImageCount += 1;
|
|
@@ -1744,7 +2336,7 @@ export class CodeToolExecutor {
|
|
|
1744
2336
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1745
2337
|
const model = process.env.CODE_HARNESS_IMAGE_MODEL ||
|
|
1746
2338
|
process.env.ARENA_IMAGE_MODEL ||
|
|
1747
|
-
"gemini-
|
|
2339
|
+
"gemini-3.1-flash-image";
|
|
1748
2340
|
const references = [];
|
|
1749
2341
|
for (const imagePath of images || []) {
|
|
1750
2342
|
const source = await this.confinedPath(imagePath);
|