@armadra/agent 0.7.3 → 0.7.5
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/CHANGELOG.md +48 -0
- package/CHANGELOG.zh-CN.md +48 -0
- package/dist/agent/retry.d.ts +14 -5
- package/dist/agent/retry.js +30 -8
- package/dist/agent/session-cache-key.d.ts +8 -0
- package/dist/agent/session-cache-key.js +46 -0
- package/dist/agent/session-cache.d.ts +2 -3
- package/dist/agent/session-cache.js +19 -37
- package/dist/agent/session-classifier.d.ts +3 -2
- package/dist/agent/session-classifier.js +36 -10
- package/dist/agent/session-compaction.d.ts +24 -5
- package/dist/agent/session-compaction.js +54 -20
- package/dist/agent/session-core.d.ts +2 -0
- package/dist/agent/session-run.d.ts +12 -0
- package/dist/agent/session-run.js +29 -10
- package/dist/agent/session-subagent.d.ts +8 -1
- package/dist/agent/session-subagent.js +37 -14
- package/dist/agent/session-sync.d.ts +8 -2
- package/dist/agent/session-sync.js +7 -10
- package/dist/agent/session-tools.js +3 -2
- package/dist/agent/subagent-fork.d.ts +67 -0
- package/dist/agent/subagent-fork.js +87 -0
- package/dist/agent/subagent-registry.d.ts +2 -2
- package/dist/agent/subagent-registry.js +2 -2
- package/dist/agent/tool-availability.d.ts +8 -0
- package/dist/agent/tool-availability.js +8 -0
- package/dist/agent/tool-runner.js +13 -4
- package/dist/agent/worktree.d.ts +8 -0
- package/dist/agent/worktree.js +51 -4
- package/dist/agents/parse.js +6 -1
- package/dist/agents/task-record.js +3 -0
- package/dist/agents/types.d.ts +2 -0
- package/dist/ai/apis/anthropic-messages.js +10 -5
- package/dist/ai/apis/anthropic-request.d.ts +2 -1
- package/dist/ai/apis/anthropic-request.js +15 -4
- package/dist/ai/apis/cache-params.js +2 -1
- package/dist/ai/apis/google-generative-ai.js +4 -2
- package/dist/ai/apis/google-request.js +2 -1
- package/dist/ai/apis/max-tokens.d.ts +42 -0
- package/dist/ai/apis/max-tokens.js +121 -0
- package/dist/ai/apis/openai-completions.js +10 -5
- package/dist/ai/apis/openai-request.js +4 -2
- package/dist/ai/apis/openai-responses-request.js +3 -2
- package/dist/ai/apis/openai-responses.js +9 -5
- package/dist/ai/apis/shared.d.ts +3 -1
- package/dist/ai/apis/shared.js +21 -3
- package/dist/ai/cache/fingerprint.d.ts +4 -1
- package/dist/ai/cache/fingerprint.js +18 -1
- package/dist/ai/cache/miss.d.ts +2 -1
- package/dist/ai/cache/miss.js +9 -3
- package/dist/ai/cache/reporting.js +5 -1
- package/dist/ai/cache/types.d.ts +10 -2
- package/dist/ai/cache/warmer.d.ts +6 -1
- package/dist/ai/cache/warmer.js +7 -0
- package/dist/ai/context.d.ts +12 -4
- package/dist/ai/context.js +43 -24
- package/dist/ai/fake/fake-provider.d.ts +8 -1
- package/dist/ai/fake/fake-provider.js +19 -7
- package/dist/ai/fake/fake-script.d.ts +2 -0
- package/dist/ai/http.d.ts +17 -9
- package/dist/ai/http.js +33 -13
- package/dist/ai/image-intern.d.ts +29 -0
- package/dist/ai/image-intern.js +58 -0
- package/dist/ai/json-body.d.ts +63 -0
- package/dist/ai/json-body.js +286 -0
- package/dist/ai/overflow.js +5 -0
- package/dist/ai/providers/catalog-data.js +13 -13
- package/dist/ai/providers/catalog.d.ts +33 -0
- package/dist/ai/providers/catalog.js +105 -6
- package/dist/ai/providers/enrich.d.ts +13 -3
- package/dist/ai/providers/enrich.js +87 -31
- package/dist/ai/sse.d.ts +3 -2
- package/dist/ai/sse.js +3 -2
- package/dist/ai/types.d.ts +9 -2
- package/dist/bundle/ama.cjs +3637 -1973
- package/dist/cli/bootstrap.js +13 -5
- package/dist/cli/compose-agents.js +3 -0
- package/dist/cli/compose-extensions.js +2 -0
- package/dist/cli/compose-request.d.ts +20 -0
- package/dist/cli/compose-request.js +37 -0
- package/dist/cli/compose-session.js +2 -2
- package/dist/cli/deps.d.ts +1 -1
- package/dist/cli/subcommands/model-meta.d.ts +2 -1
- package/dist/cli/subcommands/model-meta.js +3 -2
- package/dist/codemode/host-side.d.ts +11 -4
- package/dist/codemode/host-side.js +25 -6
- package/dist/codemode/tool.d.ts +2 -0
- package/dist/codemode/tool.js +4 -0
- package/dist/config/json-schema.js +9 -2
- package/dist/config/key-docs.js +13 -4
- package/dist/config/schema-w5.js +9 -1
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +19 -3
- package/dist/config/settings-registry.js +6 -0
- package/dist/config/types-w5.d.ts +2 -0
- package/dist/config/types.d.ts +19 -1
- package/dist/config/types.js +4 -0
- package/dist/i18n/catalog.d.ts +13 -2
- package/dist/i18n/messages/cli.d.ts +2 -0
- package/dist/i18n/messages/cli.js +2 -0
- package/dist/i18n/messages/config-keys.d.ts +10 -0
- package/dist/i18n/messages/config-keys.js +12 -2
- package/dist/i18n/messages/config.d.ts +14 -0
- package/dist/i18n/messages/config.js +4 -0
- package/dist/i18n/messages/errors.d.ts +3 -2
- package/dist/i18n/messages/errors.js +15 -2
- package/dist/i18n/messages/report.d.ts +2 -0
- package/dist/i18n/messages/report.js +2 -0
- package/dist/i18n/messages/settings.d.ts +4 -0
- package/dist/i18n/messages/settings.js +6 -2
- package/dist/i18n/messages/subcommands.d.ts +2 -2
- package/dist/i18n/messages/subcommands.js +4 -2
- package/dist/modes/acp/acp-server.js +30 -18
- package/dist/modes/acp/acp-sessions.d.ts +22 -0
- package/dist/modes/acp/acp-sessions.js +68 -0
- package/dist/modes/context-report.js +3 -2
- package/dist/modes/print/json-event.d.ts +12 -1
- package/dist/modes/print/json-event.js +48 -1
- package/dist/modes/rpc/rpc-mode.d.ts +1 -0
- package/dist/modes/rpc/rpc-mode.js +9 -2
- package/dist/modes/session-report.d.ts +1 -1
- package/dist/modes/session-report.js +12 -6
- package/dist/rpc.d.ts +5 -2
- package/dist/session/line-reader.d.ts +28 -0
- package/dist/session/line-reader.js +82 -0
- package/dist/session/list.d.ts +14 -0
- package/dist/session/list.js +150 -0
- package/dist/session/manager.d.ts +23 -3
- package/dist/session/manager.js +91 -62
- package/dist/session/migrate.d.ts +2 -1
- package/dist/session/migrate.js +2 -1
- package/dist/session/offload.d.ts +51 -0
- package/dist/session/offload.js +195 -0
- package/dist/session/projection.d.ts +16 -3
- package/dist/session/projection.js +96 -29
- package/dist/session/scan.d.ts +3 -3
- package/dist/session/scan.js +9 -18
- package/dist/session/store.d.ts +15 -4
- package/dist/session/store.js +54 -38
- package/dist/session/types.d.ts +7 -2
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.js +5 -4
- package/dist/tools/glob.d.ts +2 -2
- package/dist/tools/glob.js +2 -2
- package/dist/tools/grep.js +4 -6
- package/dist/tools/image-file.js +2 -1
- package/dist/tools/read-lines.d.ts +71 -0
- package/dist/tools/read-lines.js +224 -0
- package/dist/tools/read.d.ts +6 -2
- package/dist/tools/read.js +47 -27
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +21 -8
- package/dist/tools/truncate.d.ts +7 -0
- package/dist/tools/truncate.js +11 -0
- package/dist/tools/types.d.ts +13 -0
- package/dist/trace/build-index.js +13 -1
- package/dist/tui/ansi.d.ts +2 -0
- package/dist/tui/ansi.js +7 -3
- package/dist/tui/components/editor-buffer.js +2 -2
- package/dist/tui/components/editor.js +5 -6
- package/dist/tui/keys.js +2 -2
- package/dist/tui/stdin-buffer.js +3 -3
- package/docs/acp.md +8 -1
- package/docs/agents.md +42 -6
- package/docs/codemode.md +4 -2
- package/docs/en/acp.md +3 -2
- package/docs/en/host-api.md +1 -1
- package/docs/en/permissions.md +2 -2
- package/docs/en/providers.md +54 -20
- package/docs/en/rpc.md +23 -7
- package/docs/en/sessions.md +4 -2
- package/docs/en/tui.md +2 -2
- package/docs/permissions.md +4 -1
- package/docs/providers.md +85 -22
- package/docs/rpc.md +22 -6
- package/docs/session-format.md +13 -2
- package/docs/sessions.md +3 -1
- package/docs/tui.md +1 -1
- package/package.json +2 -2
package/dist/tools/bash.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - `spawn(shell, args, { detached: !win, stdio: [ignore, pipe, pipe] })`,stdout / stderr 合流;
|
|
5
5
|
* - 超时(缺省 120 s,上限 600 s)与 abort 都走 `killProcessTree`:SIGTERM → 2 s → SIGKILL;
|
|
6
|
-
* - 滚动尾部节流后经 `ctx.onUpdate` 流出;超限(2000 行 / 50 KB
|
|
6
|
+
* - 滚动尾部节流后经 `ctx.onUpdate` 流出;超限(2000 行 / 50 KB 与会话结果上限减余量取小)尾截断并落全文;
|
|
7
7
|
* - 结构化结果 `{ output, exit_code, truncated, full_output_path?, wall_time_seconds }`;
|
|
8
8
|
* 信号退出 `128 + signo`;非零退出码 → isError;
|
|
9
9
|
* - 注入 `AMA_*` 环境变量(shell.ts)。shell 退出后不等仍占着管道的后台进程。
|
package/dist/tools/bash.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - `spawn(shell, args, { detached: !win, stdio: [ignore, pipe, pipe] })`,stdout / stderr 合流;
|
|
5
5
|
* - 超时(缺省 120 s,上限 600 s)与 abort 都走 `killProcessTree`:SIGTERM → 2 s → SIGKILL;
|
|
6
|
-
* - 滚动尾部节流后经 `ctx.onUpdate` 流出;超限(2000 行 / 50 KB
|
|
6
|
+
* - 滚动尾部节流后经 `ctx.onUpdate` 流出;超限(2000 行 / 50 KB 与会话结果上限减余量取小)尾截断并落全文;
|
|
7
7
|
* - 结构化结果 `{ output, exit_code, truncated, full_output_path?, wall_time_seconds }`;
|
|
8
8
|
* 信号退出 `128 + signo`;非零退出码 → isError;
|
|
9
9
|
* - 注入 `AMA_*` 环境变量(shell.ts)。shell 退出后不等仍占着管道的后台进程。
|
|
@@ -21,7 +21,7 @@ import { resolvePath } from "./paths.js";
|
|
|
21
21
|
import { buildShellArgs, buildToolEnv, resolveShell } from "./shell.js";
|
|
22
22
|
import { exitCodeOf, isGroupAlive, killProcessTree, trackProcessGroup, untrackProcessGroup, } from "./process-tree.js";
|
|
23
23
|
import { OutputAccumulator } from "./output-accumulator.js";
|
|
24
|
-
import { formatSize, resolveOutputDir, safeFileName } from "./truncate.js";
|
|
24
|
+
import { formatSize, resolveOutputDir, safeFileName, toolOutputBytes } from "./truncate.js";
|
|
25
25
|
import { jobsForSession } from "./background-jobs.js";
|
|
26
26
|
import { looksLikeSandboxDenial, runsSandboxed, sandboxDenialHint, wrapBashCommand, } from "../sandbox/bash.js";
|
|
27
27
|
export const DEFAULT_BASH_TIMEOUT_MS = 120_000;
|
|
@@ -128,6 +128,7 @@ export async function executeBash(input, ctx, options = {}) {
|
|
|
128
128
|
return startBackground(command, { cwd, env, shell, isWindows, sandbox }, ctx, options);
|
|
129
129
|
const started = Date.now();
|
|
130
130
|
const acc = new OutputAccumulator({
|
|
131
|
+
maxBytes: toolOutputBytes(ctx.maxResultChars),
|
|
131
132
|
spillPath: () => join(resolveOutputDir(ctx.outputDir), safeFileName(`bash-${ctx.toolCallId}.log`)),
|
|
132
133
|
});
|
|
133
134
|
let child;
|
|
@@ -345,8 +346,8 @@ export function createBashTool(options = {}) {
|
|
|
345
346
|
return {
|
|
346
347
|
name: "bash",
|
|
347
348
|
label: "Bash",
|
|
348
|
-
description: "Run a shell command; stdout+stderr combined, keeps the
|
|
349
|
-
`
|
|
349
|
+
description: "Run a shell command; stdout+stderr combined, keeps the tail (full output saved to a " +
|
|
350
|
+
`file). Timeout ${DEFAULT_BASH_TIMEOUT_MS / 1000} s, max ${MAX_BASH_TIMEOUT_MS / 1000} s. ` +
|
|
350
351
|
"background:true gives a job id for {job, action}." +
|
|
351
352
|
sandboxDescription(options.sandbox),
|
|
352
353
|
parameters: {
|
package/dist/tools/glob.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 以 `!` 开头的模式是排除。不含 `/` 的模式匹配**文件名**(任意深度),含 `/` 的匹配相对搜索根的
|
|
6
6
|
* 路径。点文件同样参与匹配(`.git` 由遍历层排除)。Windows 上大小写不敏感。
|
|
7
7
|
*
|
|
8
|
-
* 工具:遍历尊重 `.gitignore` / `.ignore`,按 mtime 倒序,缺省上限 1000
|
|
8
|
+
* 工具:遍历尊重 `.gitignore` / `.ignore`,按 mtime 倒序,缺省上限 200([ME-D] P2-2,原 1000)。
|
|
9
9
|
*/
|
|
10
10
|
import type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
|
|
11
11
|
export interface GlobBodyOptions {
|
|
@@ -32,7 +32,7 @@ export interface GlobInput {
|
|
|
32
32
|
path?: string;
|
|
33
33
|
limit?: number;
|
|
34
34
|
}
|
|
35
|
-
export declare const DEFAULT_GLOB_LIMIT =
|
|
35
|
+
export declare const DEFAULT_GLOB_LIMIT = 200;
|
|
36
36
|
export interface GlobMatchFile {
|
|
37
37
|
abs: string;
|
|
38
38
|
rel: string;
|
package/dist/tools/glob.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 以 `!` 开头的模式是排除。不含 `/` 的模式匹配**文件名**(任意深度),含 `/` 的匹配相对搜索根的
|
|
6
6
|
* 路径。点文件同样参与匹配(`.git` 由遍历层排除)。Windows 上大小写不敏感。
|
|
7
7
|
*
|
|
8
|
-
* 工具:遍历尊重 `.gitignore` / `.ignore`,按 mtime 倒序,缺省上限 1000
|
|
8
|
+
* 工具:遍历尊重 `.gitignore` / `.ignore`,按 mtime 倒序,缺省上限 200([ME-D] P2-2,原 1000)。
|
|
9
9
|
*/
|
|
10
10
|
import { stat } from "node:fs/promises";
|
|
11
11
|
import { displayPath, resolvePath } from "./paths.js";
|
|
@@ -145,7 +145,7 @@ export class GlobMatcher {
|
|
|
145
145
|
return included && !this.exclude.some((g) => GlobMatcher.hit(g, rel));
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
export const DEFAULT_GLOB_LIMIT =
|
|
148
|
+
export const DEFAULT_GLOB_LIMIT = 200;
|
|
149
149
|
/** 收集匹配文件并按 mtime 倒序(同 mtime 按路径)。 */
|
|
150
150
|
export async function globFiles(root, pattern, signal) {
|
|
151
151
|
const matcher = new GlobMatcher(pattern);
|
package/dist/tools/grep.js
CHANGED
|
@@ -13,7 +13,7 @@ import { displayPath, resolvePath } from "./paths.js";
|
|
|
13
13
|
import { walk } from "./ignore.js";
|
|
14
14
|
import { GlobMatcher } from "./glob.js";
|
|
15
15
|
import { isBinary } from "./read.js";
|
|
16
|
-
import {
|
|
16
|
+
import { formatSize, toolOutputBytes, truncateHead, truncateLine } from "./truncate.js";
|
|
17
17
|
export const DEFAULT_GREP_LIMIT = 100;
|
|
18
18
|
export const GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
19
19
|
export const GREP_CONCURRENCY = 16;
|
|
@@ -141,16 +141,14 @@ export async function executeGrep(input, ctx) {
|
|
|
141
141
|
if (matches === 0)
|
|
142
142
|
return { content: "No matches found", details: { matches: 0, files: 0 } };
|
|
143
143
|
const joined = blocks.join(context > 0 ? "\n--\n" : "\n");
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
147
|
-
});
|
|
144
|
+
const maxBytes = toolOutputBytes(ctx.maxResultChars);
|
|
145
|
+
const cut = truncateHead(joined, { maxLines: Number.MAX_SAFE_INTEGER, maxBytes });
|
|
148
146
|
let content = cut.content;
|
|
149
147
|
if (limited) {
|
|
150
148
|
content += `\n\n[Stopped at ${limit} matches. Narrow the search or raise limit.]`;
|
|
151
149
|
}
|
|
152
150
|
else if (cut.truncated) {
|
|
153
|
-
content +=
|
|
151
|
+
content += `\n\n[Output truncated at ${formatSize(maxBytes)}. Narrow the search.]`;
|
|
154
152
|
}
|
|
155
153
|
return {
|
|
156
154
|
content,
|
package/dist/tools/image-file.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { readFile, stat } from "node:fs/promises";
|
|
11
11
|
import { extname } from "node:path";
|
|
12
12
|
import { AmaError } from "../errors.js";
|
|
13
|
+
import { internImage } from "../ai/image-intern.js";
|
|
13
14
|
import { DEFAULT_IMAGE_BASE64_LIMIT, MAX_IMAGE_EDGE, base64Size, formatMb, } from "../ai/image-limits.js";
|
|
14
15
|
import { resizeImage } from "./image-resize.js";
|
|
15
16
|
/** 读进内存前的硬上限(原始字节):再大的文件缩放也不现实,直接拒绝。 */
|
|
@@ -218,7 +219,7 @@ export async function loadImageFile(path, options = {}) {
|
|
|
218
219
|
if (!fit.ok)
|
|
219
220
|
throw fitError(path, fit, limit, buf.length, options.resize === "off");
|
|
220
221
|
return {
|
|
221
|
-
block: { type: "image", data: fit.buf.toString("base64"), mimeType: fit.mimeType },
|
|
222
|
+
block: internImage({ type: "image", data: fit.buf.toString("base64"), mimeType: fit.mimeType }),
|
|
222
223
|
path,
|
|
223
224
|
mimeType: fit.mimeType,
|
|
224
225
|
bytes: fit.buf.length,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read` 大文件的字节窗口读取(docs/memory-plan.md D1、§2.1)。[M-E]
|
|
3
|
+
*
|
|
4
|
+
* fd + 64 KiB 块顺序扫描,只解码 `[offset, offset + limit)` 内且累计不超过 `maxBytes`(多收一行)
|
|
5
|
+
* 的行,其余行只计数;口径与小文件路径(`splitBom` + `normalizeToLF` + 去末尾一个换行再 `split`)
|
|
6
|
+
* 逐字节相同:
|
|
7
|
+
* - 行终止符是 `\r\n`、`\n` 或孤立 `\r`(`normalizeToLF` 把孤立 `\r` 也当换行,R1);块边界上的
|
|
8
|
+
* `\r` | `\n` 记作一个终止符;最后一个终止符之后还有字节则多一行;
|
|
9
|
+
* - UTF-8 BOM 只看文件前 3 字节;
|
|
10
|
+
* - 按行解码:终止符都是 ASCII,不会落在多字节序列中间,无效字节的替换结果与整段解码相同;
|
|
11
|
+
* - 窗口内单行超过 `maxBytes + LINE_SLACK` 字节时只留开头(这一行必然触发字节截断,
|
|
12
|
+
* `truncateHead` 至多取它开头 `maxBytes` 字节),超长行不会整行进内存。
|
|
13
|
+
*
|
|
14
|
+
* 解析状态机是 `LineScanner`,同步版 `readLineWindow` 与异步版 `readLineWindowAsync` 共用(#171):
|
|
15
|
+
* 异步版每块一次 `await`,大文件扫描期间不独占事件循环;峰值内存相同(仍一块缓冲)。
|
|
16
|
+
*/
|
|
17
|
+
/** 超过这个大小的文本文件走字节窗口;不超过的保留整读路径。 */
|
|
18
|
+
export declare const STREAM_READ_THRESHOLD: number;
|
|
19
|
+
export interface LineWindow {
|
|
20
|
+
/** 窗口内的行:首行已去 BOM、已按 LF 切分、已去行尾 `\r`。 */
|
|
21
|
+
lines: string[];
|
|
22
|
+
/** 全文行数(口径同 `normalized.replace(/\n$/, "").split("\n")`)。 */
|
|
23
|
+
totalLines: number;
|
|
24
|
+
/** 因 `maxBytes` 提前停止收集。 */
|
|
25
|
+
truncatedBy?: "bytes";
|
|
26
|
+
/** 文件以 UTF-8 BOM 开头。 */
|
|
27
|
+
bom: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** 文件开头至多 `bytes` 字节(一次 `readSync`,用于二进制嗅探)。 */
|
|
30
|
+
export declare function readHead(abs: string, bytes: number): Buffer;
|
|
31
|
+
/** `readHead` 的异步版(#171)。 */
|
|
32
|
+
export declare function readHeadAsync(abs: string, bytes: number): Promise<Buffer>;
|
|
33
|
+
/**
|
|
34
|
+
* 按块喂入文件字节的行扫描状态机:第 `first` 行(0 起)到第 `stop` 行(不含)之间的行被收集,
|
|
35
|
+
* 收集的行累计 `字节 + 1` 超过 `maxBytes` 后停止收集(含越界的那一行),其余行只计数。
|
|
36
|
+
*/
|
|
37
|
+
export declare class LineScanner {
|
|
38
|
+
private readonly first;
|
|
39
|
+
private readonly stop;
|
|
40
|
+
private readonly maxBytes;
|
|
41
|
+
private readonly lineCap;
|
|
42
|
+
private readonly lines;
|
|
43
|
+
private truncatedBy;
|
|
44
|
+
private collected;
|
|
45
|
+
private index;
|
|
46
|
+
private parts;
|
|
47
|
+
private partBytes;
|
|
48
|
+
private pending;
|
|
49
|
+
private skipLF;
|
|
50
|
+
private bom;
|
|
51
|
+
private firstChunk;
|
|
52
|
+
constructor(first: number, stop: number, maxBytes: number);
|
|
53
|
+
/**
|
|
54
|
+
* 处理 `buf[0, end)`;`eof` 表示这是最后一块。返回调用方下次读取前应保留在 `buf` 开头的字节数
|
|
55
|
+
* (只在首块不足 3 字节、还判断不了 BOM 时非零)。
|
|
56
|
+
*/
|
|
57
|
+
feed(buf: Buffer, end: number, eof: boolean): number;
|
|
58
|
+
/** 文件读完:收尾最后一行并给出结果。 */
|
|
59
|
+
finish(): LineWindow;
|
|
60
|
+
private collecting;
|
|
61
|
+
private take;
|
|
62
|
+
private endLine;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 读取 `abs` 第 `offset` 行(1 起)开始的至多 `limit` 行(`undefined` 不限,`0` 只计数);
|
|
66
|
+
* 收集的行累计 `字节 + 1` 超过 `maxBytes` 后停止收集(含越界的那一行)。`chunkBytes` 只供测试。
|
|
67
|
+
* 同步版,保留给单测与口径比对;`read` 工具用 `readLineWindowAsync`。
|
|
68
|
+
*/
|
|
69
|
+
export declare function readLineWindow(abs: string, offset: number, limit: number | undefined, maxBytes: number, chunkBytes?: number): LineWindow;
|
|
70
|
+
/** `readLineWindow` 的异步版(#171):每块一次 `await`,结果与同步版相同。 */
|
|
71
|
+
export declare function readLineWindowAsync(abs: string, offset: number, limit: number | undefined, maxBytes: number, chunkBytes?: number): Promise<LineWindow>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read` 大文件的字节窗口读取(docs/memory-plan.md D1、§2.1)。[M-E]
|
|
3
|
+
*
|
|
4
|
+
* fd + 64 KiB 块顺序扫描,只解码 `[offset, offset + limit)` 内且累计不超过 `maxBytes`(多收一行)
|
|
5
|
+
* 的行,其余行只计数;口径与小文件路径(`splitBom` + `normalizeToLF` + 去末尾一个换行再 `split`)
|
|
6
|
+
* 逐字节相同:
|
|
7
|
+
* - 行终止符是 `\r\n`、`\n` 或孤立 `\r`(`normalizeToLF` 把孤立 `\r` 也当换行,R1);块边界上的
|
|
8
|
+
* `\r` | `\n` 记作一个终止符;最后一个终止符之后还有字节则多一行;
|
|
9
|
+
* - UTF-8 BOM 只看文件前 3 字节;
|
|
10
|
+
* - 按行解码:终止符都是 ASCII,不会落在多字节序列中间,无效字节的替换结果与整段解码相同;
|
|
11
|
+
* - 窗口内单行超过 `maxBytes + LINE_SLACK` 字节时只留开头(这一行必然触发字节截断,
|
|
12
|
+
* `truncateHead` 至多取它开头 `maxBytes` 字节),超长行不会整行进内存。
|
|
13
|
+
*
|
|
14
|
+
* 解析状态机是 `LineScanner`,同步版 `readLineWindow` 与异步版 `readLineWindowAsync` 共用(#171):
|
|
15
|
+
* 异步版每块一次 `await`,大文件扫描期间不独占事件循环;峰值内存相同(仍一块缓冲)。
|
|
16
|
+
*/
|
|
17
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
18
|
+
import { open } from "node:fs/promises";
|
|
19
|
+
/** 超过这个大小的文本文件走字节窗口;不超过的保留整读路径。 */
|
|
20
|
+
export const STREAM_READ_THRESHOLD = 1024 * 1024;
|
|
21
|
+
const CHUNK_BYTES = 64 * 1024;
|
|
22
|
+
/** 截短超长行时多留的字节:盖住行号前缀与 `sliceHeadBytes` 回退到字符起点的余量。 */
|
|
23
|
+
const LINE_SLACK = 64;
|
|
24
|
+
const LF = 0x0a;
|
|
25
|
+
const CR = 0x0d;
|
|
26
|
+
/** 文件开头至多 `bytes` 字节(一次 `readSync`,用于二进制嗅探)。 */
|
|
27
|
+
export function readHead(abs, bytes) {
|
|
28
|
+
const fd = openSync(abs, "r");
|
|
29
|
+
try {
|
|
30
|
+
const buf = Buffer.alloc(bytes);
|
|
31
|
+
let got = 0;
|
|
32
|
+
while (got < bytes) {
|
|
33
|
+
const n = readSync(fd, buf, got, bytes - got, null);
|
|
34
|
+
if (n === 0)
|
|
35
|
+
break;
|
|
36
|
+
got += n;
|
|
37
|
+
}
|
|
38
|
+
return buf.subarray(0, got);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
closeSync(fd);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** `readHead` 的异步版(#171)。 */
|
|
45
|
+
export async function readHeadAsync(abs, bytes) {
|
|
46
|
+
const handle = await open(abs, "r");
|
|
47
|
+
try {
|
|
48
|
+
const buf = Buffer.alloc(bytes);
|
|
49
|
+
let got = 0;
|
|
50
|
+
while (got < bytes) {
|
|
51
|
+
const { bytesRead } = await handle.read(buf, got, bytes - got, null);
|
|
52
|
+
if (bytesRead === 0)
|
|
53
|
+
break;
|
|
54
|
+
got += bytesRead;
|
|
55
|
+
}
|
|
56
|
+
return buf.subarray(0, got);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await handle.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 按块喂入文件字节的行扫描状态机:第 `first` 行(0 起)到第 `stop` 行(不含)之间的行被收集,
|
|
64
|
+
* 收集的行累计 `字节 + 1` 超过 `maxBytes` 后停止收集(含越界的那一行),其余行只计数。
|
|
65
|
+
*/
|
|
66
|
+
export class LineScanner {
|
|
67
|
+
first;
|
|
68
|
+
stop;
|
|
69
|
+
maxBytes;
|
|
70
|
+
lineCap;
|
|
71
|
+
lines = [];
|
|
72
|
+
truncatedBy;
|
|
73
|
+
collected = 0;
|
|
74
|
+
index = 0; // 已完成的行数
|
|
75
|
+
parts = [];
|
|
76
|
+
partBytes = 0;
|
|
77
|
+
pending = false; // 最后一个终止符之后已有字节
|
|
78
|
+
skipLF = false; // 上一块以 `\r` 结尾:下一块开头的 `\n` 属于同一终止符
|
|
79
|
+
bom = false;
|
|
80
|
+
firstChunk = true;
|
|
81
|
+
constructor(first, stop, maxBytes) {
|
|
82
|
+
this.first = first;
|
|
83
|
+
this.stop = stop;
|
|
84
|
+
this.maxBytes = maxBytes;
|
|
85
|
+
this.lineCap = maxBytes + LINE_SLACK;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 处理 `buf[0, end)`;`eof` 表示这是最后一块。返回调用方下次读取前应保留在 `buf` 开头的字节数
|
|
89
|
+
* (只在首块不足 3 字节、还判断不了 BOM 时非零)。
|
|
90
|
+
*/
|
|
91
|
+
feed(buf, end, eof) {
|
|
92
|
+
let pos = 0;
|
|
93
|
+
if (this.firstChunk) {
|
|
94
|
+
if (end < 3 && !eof)
|
|
95
|
+
return end;
|
|
96
|
+
this.firstChunk = false;
|
|
97
|
+
if (end >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
|
|
98
|
+
this.bom = true;
|
|
99
|
+
pos = 3;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (this.skipLF && pos < end) {
|
|
103
|
+
if (buf[pos] === LF)
|
|
104
|
+
pos++;
|
|
105
|
+
this.skipLF = false;
|
|
106
|
+
}
|
|
107
|
+
let nextLF = buf.indexOf(LF, pos);
|
|
108
|
+
let nextCR = buf.indexOf(CR, pos);
|
|
109
|
+
while (pos < end) {
|
|
110
|
+
if (nextLF !== -1 && nextLF < pos)
|
|
111
|
+
nextLF = buf.indexOf(LF, pos);
|
|
112
|
+
if (nextCR !== -1 && nextCR < pos)
|
|
113
|
+
nextCR = buf.indexOf(CR, pos);
|
|
114
|
+
const lf = nextLF === -1 || nextLF >= end ? end : nextLF;
|
|
115
|
+
const cr = nextCR === -1 || nextCR >= end ? end : nextCR;
|
|
116
|
+
const at = Math.min(lf, cr);
|
|
117
|
+
if (at > pos) {
|
|
118
|
+
this.pending = true;
|
|
119
|
+
if (this.collecting())
|
|
120
|
+
this.take(buf, pos, at);
|
|
121
|
+
}
|
|
122
|
+
if (at === end)
|
|
123
|
+
break;
|
|
124
|
+
this.endLine();
|
|
125
|
+
pos = at + 1;
|
|
126
|
+
if (at === cr) {
|
|
127
|
+
if (pos < end) {
|
|
128
|
+
if (buf[pos] === LF)
|
|
129
|
+
pos++;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.skipLF = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
/** 文件读完:收尾最后一行并给出结果。 */
|
|
139
|
+
finish() {
|
|
140
|
+
if (this.pending)
|
|
141
|
+
this.endLine();
|
|
142
|
+
const { lines, truncatedBy, bom } = this;
|
|
143
|
+
return { lines, totalLines: this.index, ...(truncatedBy ? { truncatedBy } : {}), bom };
|
|
144
|
+
}
|
|
145
|
+
collecting() {
|
|
146
|
+
return this.truncatedBy === undefined && this.index >= this.first && this.index < this.stop;
|
|
147
|
+
}
|
|
148
|
+
take(buf, from, to) {
|
|
149
|
+
if (to <= from || this.partBytes >= this.lineCap)
|
|
150
|
+
return;
|
|
151
|
+
const end = Math.min(to, from + this.lineCap - this.partBytes);
|
|
152
|
+
this.parts.push(Buffer.from(buf.subarray(from, end)));
|
|
153
|
+
this.partBytes += end - from;
|
|
154
|
+
}
|
|
155
|
+
endLine() {
|
|
156
|
+
if (this.collecting()) {
|
|
157
|
+
const { parts } = this;
|
|
158
|
+
const line = (parts.length === 1 ? parts[0] : Buffer.concat(parts)).toString("utf8");
|
|
159
|
+
this.lines.push(line);
|
|
160
|
+
this.collected += Buffer.byteLength(line) + 1;
|
|
161
|
+
if (this.collected > this.maxBytes)
|
|
162
|
+
this.truncatedBy = "bytes";
|
|
163
|
+
}
|
|
164
|
+
if (this.partBytes > 0)
|
|
165
|
+
this.parts = []; // 窗口外的行不分配:40 万行时每行一个空数组就是十几 MB 临时垃圾
|
|
166
|
+
this.partBytes = 0;
|
|
167
|
+
this.pending = false;
|
|
168
|
+
this.index++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function scanner(offset, limit, maxBytes) {
|
|
172
|
+
const first = offset - 1;
|
|
173
|
+
return new LineScanner(first, limit === undefined ? Infinity : first + limit, maxBytes);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 读取 `abs` 第 `offset` 行(1 起)开始的至多 `limit` 行(`undefined` 不限,`0` 只计数);
|
|
177
|
+
* 收集的行累计 `字节 + 1` 超过 `maxBytes` 后停止收集(含越界的那一行)。`chunkBytes` 只供测试。
|
|
178
|
+
* 同步版,保留给单测与口径比对;`read` 工具用 `readLineWindowAsync`。
|
|
179
|
+
*/
|
|
180
|
+
export function readLineWindow(abs, offset, limit, maxBytes, chunkBytes = CHUNK_BYTES) {
|
|
181
|
+
const scan = scanner(offset, limit, maxBytes);
|
|
182
|
+
const size = Math.max(3, chunkBytes);
|
|
183
|
+
const fd = openSync(abs, "r");
|
|
184
|
+
try {
|
|
185
|
+
const buf = Buffer.alloc(size);
|
|
186
|
+
let carry = 0; // 首块凑满 3 字节判断 BOM
|
|
187
|
+
for (;;) {
|
|
188
|
+
const n = readSync(fd, buf, carry, size - carry, null);
|
|
189
|
+
const end = carry + n;
|
|
190
|
+
if (end === 0)
|
|
191
|
+
break;
|
|
192
|
+
carry = scan.feed(buf, end, n === 0);
|
|
193
|
+
if (n === 0)
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
closeSync(fd);
|
|
199
|
+
}
|
|
200
|
+
return scan.finish();
|
|
201
|
+
}
|
|
202
|
+
/** `readLineWindow` 的异步版(#171):每块一次 `await`,结果与同步版相同。 */
|
|
203
|
+
export async function readLineWindowAsync(abs, offset, limit, maxBytes, chunkBytes = CHUNK_BYTES) {
|
|
204
|
+
const scan = scanner(offset, limit, maxBytes);
|
|
205
|
+
const size = Math.max(3, chunkBytes);
|
|
206
|
+
const handle = await open(abs, "r");
|
|
207
|
+
try {
|
|
208
|
+
const buf = Buffer.alloc(size);
|
|
209
|
+
let carry = 0;
|
|
210
|
+
for (;;) {
|
|
211
|
+
const { bytesRead: n } = await handle.read(buf, carry, size - carry, null);
|
|
212
|
+
const end = carry + n;
|
|
213
|
+
if (end === 0)
|
|
214
|
+
break;
|
|
215
|
+
carry = scan.feed(buf, end, n === 0);
|
|
216
|
+
if (n === 0)
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
await handle.close();
|
|
222
|
+
}
|
|
223
|
+
return scan.finish();
|
|
224
|
+
}
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `read` 工具(设计 §5.2)。[B3]
|
|
3
3
|
*
|
|
4
|
-
* 文本按 `cat -n` 形状返回(行号右对齐 6 位 + Tab);头截断 2000 行 / 50 KB
|
|
5
|
-
* `offset` 续读;NUL 嗅探判二进制并拒绝;
|
|
4
|
+
* 文本按 `cat -n` 形状返回(行号右对齐 6 位 + Tab);头截断 2000 行 / 字节上限先到者(50 KB 与
|
|
5
|
+
* 会话结果上限减余量取小,[ME-D] 一次截到位),末尾提示 `offset` 续读;NUL 嗅探判二进制并拒绝;
|
|
6
|
+
* png / jpg / gif / webp 作为 ImageBlock 返回(MIME 按文件头,
|
|
6
7
|
* 与 `--image` / `@图片` 共用 image-file.ts;模型不支持图片、超过当前端点的单图上限(base64 后,
|
|
7
8
|
* [W5-I] 按端点分档)或任一边 > 8000 px 时只给路径与尺寸);成功后 `ctx.markRead(abs)`。
|
|
9
|
+
* [M-E] 超过 1 MiB 的文本按字节窗口读(read-lines.ts),输出与整读逐字节相同。
|
|
8
10
|
*/
|
|
9
11
|
import type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
|
|
10
12
|
import { type ImageFitOptions } from "./image-file.js";
|
|
@@ -19,6 +21,8 @@ export interface ReadToolOptions {
|
|
|
19
21
|
supportsImages?(ctx: ToolContext): boolean;
|
|
20
22
|
/** [W5-I] 当前模型的单图上限与缩放设置;缺省 5 MB(base64 后)。 */
|
|
21
23
|
imageOptions?(ctx: ToolContext): ImageFitOptions;
|
|
24
|
+
/** [M-E] 超过这个字节数的文本文件走字节窗口;缺省 `STREAM_READ_THRESHOLD`,测试注入 0 / Infinity。 */
|
|
25
|
+
streamThreshold?: number;
|
|
22
26
|
}
|
|
23
27
|
export declare function isBinary(buf: Buffer): boolean;
|
|
24
28
|
/**
|
package/dist/tools/read.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `read` 工具(设计 §5.2)。[B3]
|
|
3
3
|
*
|
|
4
|
-
* 文本按 `cat -n` 形状返回(行号右对齐 6 位 + Tab);头截断 2000 行 / 50 KB
|
|
5
|
-
* `offset` 续读;NUL 嗅探判二进制并拒绝;
|
|
4
|
+
* 文本按 `cat -n` 形状返回(行号右对齐 6 位 + Tab);头截断 2000 行 / 字节上限先到者(50 KB 与
|
|
5
|
+
* 会话结果上限减余量取小,[ME-D] 一次截到位),末尾提示 `offset` 续读;NUL 嗅探判二进制并拒绝;
|
|
6
|
+
* png / jpg / gif / webp 作为 ImageBlock 返回(MIME 按文件头,
|
|
6
7
|
* 与 `--image` / `@图片` 共用 image-file.ts;模型不支持图片、超过当前端点的单图上限(base64 后,
|
|
7
8
|
* [W5-I] 按端点分档)或任一边 > 8000 px 时只给路径与尺寸);成功后 `ctx.markRead(abs)`。
|
|
9
|
+
* [M-E] 超过 1 MiB 的文本按字节窗口读(read-lines.ts),输出与整读逐字节相同。
|
|
8
10
|
*/
|
|
9
11
|
import { readFile, stat } from "node:fs/promises";
|
|
12
|
+
import { internImage } from "../ai/image-intern.js";
|
|
10
13
|
import { displayPath, resolvePath } from "./paths.js";
|
|
11
|
-
import {
|
|
14
|
+
import { DEFAULT_MAX_LINES, formatSize, toolOutputBytes, truncateHead } from "./truncate.js";
|
|
12
15
|
import { normalizeToLF, splitBom } from "./edit-fuzzy.js";
|
|
16
|
+
import { STREAM_READ_THRESHOLD, readHeadAsync, readLineWindowAsync } from "./read-lines.js";
|
|
13
17
|
import { MAX_IMAGE_FILE_BYTES, fitImage, imageMimeFromPath, imageSize, sniffImageMime, } from "./image-file.js";
|
|
14
18
|
export { imageSize } from "./image-file.js";
|
|
15
19
|
const SNIFF_BYTES = 8000;
|
|
@@ -20,6 +24,7 @@ export function isBinary(buf) {
|
|
|
20
24
|
return true;
|
|
21
25
|
return false;
|
|
22
26
|
}
|
|
27
|
+
const binaryMessage = (shown) => `${shown} appears to be a binary file; refusing to read it`;
|
|
23
28
|
function error(message) {
|
|
24
29
|
return { content: message, isError: true };
|
|
25
30
|
}
|
|
@@ -68,7 +73,7 @@ async function readImage(abs, shown, ctx, options) {
|
|
|
68
73
|
: "";
|
|
69
74
|
const blocks = [
|
|
70
75
|
{ type: "text", text: caption + note },
|
|
71
|
-
{ type: "image", data: fit.buf.toString("base64"), mimeType: fit.mimeType },
|
|
76
|
+
internImage({ type: "image", data: fit.buf.toString("base64"), mimeType: fit.mimeType }),
|
|
72
77
|
];
|
|
73
78
|
return { content: blocks, details };
|
|
74
79
|
}
|
|
@@ -86,42 +91,58 @@ export async function executeRead(input, ctx, options = {}) {
|
|
|
86
91
|
return error(`${shown} is a directory; ${directoryHint(shown, ctx)}`);
|
|
87
92
|
if (imageMimeFromPath(abs) !== undefined)
|
|
88
93
|
return readImage(abs, shown, ctx, options);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
const offset = input.offset ?? 1;
|
|
95
|
+
const badOffset = !Number.isInteger(offset) || offset < 1;
|
|
96
|
+
const badLimit = input.limit !== undefined && (!Number.isInteger(input.limit) || input.limit < 1);
|
|
97
|
+
const maxBytes = toolOutputBytes(ctx.maxResultChars);
|
|
98
|
+
let totalLines;
|
|
99
|
+
let selected;
|
|
100
|
+
if (info.size > (options.streamThreshold ?? STREAM_READ_THRESHOLD)) {
|
|
101
|
+
// [M-E] 大文件按字节窗口读:只解码要显示的行,结果与整读逐字节相同(D1)
|
|
102
|
+
if (isBinary(await readHeadAsync(abs, SNIFF_BYTES)))
|
|
103
|
+
return error(binaryMessage(shown));
|
|
104
|
+
const want = Math.min(input.limit ?? Infinity, DEFAULT_MAX_LINES + 1);
|
|
105
|
+
const win = await readLineWindowAsync(abs, badOffset ? 1 : offset, badOffset || badLimit ? 0 : want, maxBytes);
|
|
106
|
+
totalLines = win.totalLines;
|
|
107
|
+
selected = win.lines;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const buf = await readFile(abs);
|
|
111
|
+
if (isBinary(buf))
|
|
112
|
+
return error(binaryMessage(shown));
|
|
113
|
+
const { text } = splitBom(buf.toString("utf8"));
|
|
114
|
+
const normalized = normalizeToLF(text);
|
|
115
|
+
const allLines = normalized === "" ? [] : normalized.replace(/\n$/, "").split("\n");
|
|
116
|
+
totalLines = allLines.length;
|
|
117
|
+
const end = input.limit === undefined ? undefined : offset - 1 + input.limit;
|
|
118
|
+
selected = badOffset || badLimit ? [] : allLines.slice(offset - 1, end);
|
|
119
|
+
}
|
|
95
120
|
ctx.markRead(abs);
|
|
96
|
-
if (
|
|
121
|
+
if (totalLines === 0) {
|
|
97
122
|
return { content: `(${shown} is empty)`, details: { path: abs, totalLines: 0 } };
|
|
98
123
|
}
|
|
99
|
-
|
|
100
|
-
if (!Number.isInteger(offset) || offset < 1)
|
|
124
|
+
if (badOffset)
|
|
101
125
|
return error("offset must be an integer ≥ 1");
|
|
102
|
-
if (offset >
|
|
103
|
-
return error(`offset ${offset} is beyond the end of ${shown} (${
|
|
126
|
+
if (offset > totalLines) {
|
|
127
|
+
return error(`offset ${offset} is beyond the end of ${shown} (${totalLines} lines)`);
|
|
104
128
|
}
|
|
105
|
-
if (
|
|
129
|
+
if (badLimit)
|
|
106
130
|
return error("limit must be an integer ≥ 1");
|
|
107
|
-
}
|
|
108
|
-
const end = input.limit === undefined ? allLines.length : offset - 1 + input.limit;
|
|
109
|
-
const selected = allLines.slice(offset - 1, end);
|
|
110
131
|
const numbered = numberLines(selected, offset);
|
|
111
|
-
const cut = truncateHead(numbered, { maxLines: DEFAULT_MAX_LINES, maxBytes
|
|
132
|
+
const cut = truncateHead(numbered, { maxLines: DEFAULT_MAX_LINES, maxBytes });
|
|
112
133
|
const lastShown = offset - 1 + cut.outputLines;
|
|
113
134
|
let content = cut.content;
|
|
114
|
-
if (lastShown <
|
|
135
|
+
if (lastShown < totalLines) {
|
|
115
136
|
const reason = cut.truncated
|
|
116
|
-
? ` (output limit ${cut.truncatedBy === "bytes" ? formatSize(
|
|
137
|
+
? ` (output limit ${cut.truncatedBy === "bytes" ? formatSize(maxBytes) : `${DEFAULT_MAX_LINES} lines`} reached)`
|
|
117
138
|
: "";
|
|
118
|
-
content += `\n\n[Showing lines ${offset}-${lastShown} of ${
|
|
139
|
+
content += `\n\n[Showing lines ${offset}-${lastShown} of ${totalLines}${reason}. Use offset=${lastShown + 1} to continue.]`;
|
|
119
140
|
}
|
|
120
141
|
return {
|
|
121
142
|
content,
|
|
122
143
|
details: {
|
|
123
144
|
path: abs,
|
|
124
|
-
totalLines
|
|
145
|
+
totalLines,
|
|
125
146
|
firstLine: offset,
|
|
126
147
|
lastLine: lastShown,
|
|
127
148
|
truncated: cut.truncated,
|
|
@@ -132,9 +153,8 @@ export function createReadTool(options = {}) {
|
|
|
132
153
|
return {
|
|
133
154
|
name: "read",
|
|
134
155
|
label: "Read",
|
|
135
|
-
description: "Read a file
|
|
136
|
-
|
|
137
|
-
"Images (png, jpg, gif, webp) come as attachments.",
|
|
156
|
+
description: "Read a file with line numbers (cat -n); page with offset/limit, a cut result says where " +
|
|
157
|
+
"to continue. Images (png, jpg, gif, webp) come as attachments.",
|
|
138
158
|
parameters: {
|
|
139
159
|
type: "object",
|
|
140
160
|
properties: {
|
package/dist/tools/task.d.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* - **parallel**:同一回复里的多个 task 真并行(会话注册表的池限流,缺省 4);与 sequential 工具
|
|
13
13
|
* (edit 等)同批时仍按 tool-runner 的规则整批串行。
|
|
14
14
|
* - 深度 ≤ 1:子会话的工具表里保留 task(与父字节一致,缓存前缀可复用),运行时在这里拒绝。
|
|
15
|
+
* - [ME-A] `context: "fork"`:子会话继承本会话已完成的回合(同模型、同前缀);`details.context` 是实际
|
|
16
|
+
* 模式(不满足条件时回落 `fresh`)。
|
|
15
17
|
* - 结果 = 子 Agent 的最终报告(> 50 KB 截头尾、全文落 outputs/,见 agents/result.ts)+ details。
|
|
16
18
|
*/
|
|
17
19
|
import type { ModelThinkingLevel } from "../ai/types.js";
|
|
@@ -31,10 +33,17 @@ export interface TaskInput {
|
|
|
31
33
|
model?: string;
|
|
32
34
|
thinkingLevel?: ModelThinkingLevel;
|
|
33
35
|
maxTurns?: number;
|
|
36
|
+
context?: "fork" | "fresh";
|
|
34
37
|
}
|
|
35
38
|
/** 预留(并发上限在会话的任务注册表,见 agent/subagent-registry.ts)。 */
|
|
36
39
|
export interface TaskToolOptions {
|
|
37
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* [ME-A] 两版描述共用的开头:fresh / fork 两种上下文。default+task 与 codemode-only 的预算余量都只有
|
|
43
|
+
* 几十 token,加 `context` 参数时整段改写、字符数基本不增(`agent` 参数的「Default general」也去掉,
|
|
44
|
+
* 清单里 general 已标 default)。
|
|
45
|
+
*/
|
|
46
|
+
export declare const TASK_CONTEXT_LEAD: string;
|
|
38
47
|
/** 缺省前台(`-p`、`subagents.background: never`)时的描述。 */
|
|
39
48
|
export declare const FOREGROUND_DESCRIPTION: string;
|
|
40
49
|
/** [W7-B1] 缺省后台(交互 / RPC / ACP,docs/agents-concurrency-plan.md §2.6)时的描述。 */
|