@armadra/agent 0.7.3 → 0.7.4
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 +37 -0
- package/CHANGELOG.zh-CN.md +37 -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 +9 -3
- package/dist/ai/apis/google-generative-ai.js +4 -2
- 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 +1 -1
- package/dist/ai/apis/openai-responses-request.js +1 -1
- 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 +31 -0
- package/dist/ai/json-body.js +208 -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 +3110 -1793
- 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/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 +15 -5
- package/dist/modes/acp/acp-sessions.d.ts +9 -0
- package/dist/modes/acp/acp-sessions.js +25 -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 +7 -3
- package/dist/session/manager.js +21 -48
- package/dist/session/migrate.d.ts +2 -1
- package/dist/session/migrate.js +2 -1
- 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 +3 -0
- package/dist/session/store.js +37 -31
- 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 +32 -0
- package/dist/tools/read-lines.js +147 -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 +5 -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 +3 -2
- package/docs/sessions.md +3 -1
- package/docs/tui.md +1 -1
- package/package.json +2 -2
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,32 @@
|
|
|
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
|
+
/** 超过这个大小的文本文件走字节窗口;不超过的保留整读路径。 */
|
|
15
|
+
export declare const STREAM_READ_THRESHOLD: number;
|
|
16
|
+
export interface LineWindow {
|
|
17
|
+
/** 窗口内的行:首行已去 BOM、已按 LF 切分、已去行尾 `\r`。 */
|
|
18
|
+
lines: string[];
|
|
19
|
+
/** 全文行数(口径同 `normalized.replace(/\n$/, "").split("\n")`)。 */
|
|
20
|
+
totalLines: number;
|
|
21
|
+
/** 因 `maxBytes` 提前停止收集。 */
|
|
22
|
+
truncatedBy?: "bytes";
|
|
23
|
+
/** 文件以 UTF-8 BOM 开头。 */
|
|
24
|
+
bom: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** 文件开头至多 `bytes` 字节(一次 `readSync`,用于二进制嗅探)。 */
|
|
27
|
+
export declare function readHead(abs: string, bytes: number): Buffer;
|
|
28
|
+
/**
|
|
29
|
+
* 读取 `abs` 第 `offset` 行(1 起)开始的至多 `limit` 行(`undefined` 不限,`0` 只计数);
|
|
30
|
+
* 收集的行累计 `字节 + 1` 超过 `maxBytes` 后停止收集(含越界的那一行)。`chunkBytes` 只供测试。
|
|
31
|
+
*/
|
|
32
|
+
export declare function readLineWindow(abs: string, offset: number, limit: number | undefined, maxBytes: number, chunkBytes?: number): LineWindow;
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
15
|
+
/** 超过这个大小的文本文件走字节窗口;不超过的保留整读路径。 */
|
|
16
|
+
export const STREAM_READ_THRESHOLD = 1024 * 1024;
|
|
17
|
+
const CHUNK_BYTES = 64 * 1024;
|
|
18
|
+
/** 截短超长行时多留的字节:盖住行号前缀与 `sliceHeadBytes` 回退到字符起点的余量。 */
|
|
19
|
+
const LINE_SLACK = 64;
|
|
20
|
+
const LF = 0x0a;
|
|
21
|
+
const CR = 0x0d;
|
|
22
|
+
/** 文件开头至多 `bytes` 字节(一次 `readSync`,用于二进制嗅探)。 */
|
|
23
|
+
export function readHead(abs, bytes) {
|
|
24
|
+
const fd = openSync(abs, "r");
|
|
25
|
+
try {
|
|
26
|
+
const buf = Buffer.alloc(bytes);
|
|
27
|
+
let got = 0;
|
|
28
|
+
while (got < bytes) {
|
|
29
|
+
const n = readSync(fd, buf, got, bytes - got, null);
|
|
30
|
+
if (n === 0)
|
|
31
|
+
break;
|
|
32
|
+
got += n;
|
|
33
|
+
}
|
|
34
|
+
return buf.subarray(0, got);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
closeSync(fd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 读取 `abs` 第 `offset` 行(1 起)开始的至多 `limit` 行(`undefined` 不限,`0` 只计数);
|
|
42
|
+
* 收集的行累计 `字节 + 1` 超过 `maxBytes` 后停止收集(含越界的那一行)。`chunkBytes` 只供测试。
|
|
43
|
+
*/
|
|
44
|
+
export function readLineWindow(abs, offset, limit, maxBytes, chunkBytes = CHUNK_BYTES) {
|
|
45
|
+
const first = offset - 1;
|
|
46
|
+
const stop = limit === undefined ? Infinity : first + limit;
|
|
47
|
+
const lineCap = maxBytes + LINE_SLACK;
|
|
48
|
+
const lines = [];
|
|
49
|
+
let truncatedBy;
|
|
50
|
+
let collected = 0;
|
|
51
|
+
let index = 0; // 已完成的行数
|
|
52
|
+
let parts = [];
|
|
53
|
+
let partBytes = 0;
|
|
54
|
+
let pending = false; // 最后一个终止符之后已有字节
|
|
55
|
+
let skipLF = false; // 上一块以 `\r` 结尾:下一块开头的 `\n` 属于同一终止符
|
|
56
|
+
let bom = false;
|
|
57
|
+
const collecting = () => truncatedBy === undefined && index >= first && index < stop;
|
|
58
|
+
const take = (buf, from, to) => {
|
|
59
|
+
if (to <= from || partBytes >= lineCap)
|
|
60
|
+
return;
|
|
61
|
+
const end = Math.min(to, from + lineCap - partBytes);
|
|
62
|
+
parts.push(Buffer.from(buf.subarray(from, end)));
|
|
63
|
+
partBytes += end - from;
|
|
64
|
+
};
|
|
65
|
+
const finish = () => {
|
|
66
|
+
if (collecting()) {
|
|
67
|
+
const line = (parts.length === 1 ? parts[0] : Buffer.concat(parts)).toString("utf8");
|
|
68
|
+
lines.push(line);
|
|
69
|
+
collected += Buffer.byteLength(line) + 1;
|
|
70
|
+
if (collected > maxBytes)
|
|
71
|
+
truncatedBy = "bytes";
|
|
72
|
+
}
|
|
73
|
+
if (partBytes > 0)
|
|
74
|
+
parts = []; // 窗口外的行不分配:40 万行时每行一个空数组就是十几 MB 临时垃圾
|
|
75
|
+
partBytes = 0;
|
|
76
|
+
pending = false;
|
|
77
|
+
index++;
|
|
78
|
+
};
|
|
79
|
+
const size = Math.max(3, chunkBytes);
|
|
80
|
+
const fd = openSync(abs, "r");
|
|
81
|
+
try {
|
|
82
|
+
const buf = Buffer.alloc(size);
|
|
83
|
+
let carry = 0; // 首块凑满 3 字节判断 BOM
|
|
84
|
+
let firstChunk = true;
|
|
85
|
+
for (;;) {
|
|
86
|
+
const n = readSync(fd, buf, carry, size - carry, null);
|
|
87
|
+
const end = carry + n;
|
|
88
|
+
carry = 0;
|
|
89
|
+
if (n === 0 && end === 0)
|
|
90
|
+
break;
|
|
91
|
+
let pos = 0;
|
|
92
|
+
if (firstChunk) {
|
|
93
|
+
if (end < 3 && n > 0) {
|
|
94
|
+
carry = end;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
firstChunk = false;
|
|
98
|
+
if (end >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
|
|
99
|
+
bom = true;
|
|
100
|
+
pos = 3;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (skipLF && pos < end) {
|
|
104
|
+
if (buf[pos] === LF)
|
|
105
|
+
pos++;
|
|
106
|
+
skipLF = false;
|
|
107
|
+
}
|
|
108
|
+
let nextLF = buf.indexOf(LF, pos);
|
|
109
|
+
let nextCR = buf.indexOf(CR, pos);
|
|
110
|
+
while (pos < end) {
|
|
111
|
+
if (nextLF !== -1 && nextLF < pos)
|
|
112
|
+
nextLF = buf.indexOf(LF, pos);
|
|
113
|
+
if (nextCR !== -1 && nextCR < pos)
|
|
114
|
+
nextCR = buf.indexOf(CR, pos);
|
|
115
|
+
const lf = nextLF === -1 || nextLF >= end ? end : nextLF;
|
|
116
|
+
const cr = nextCR === -1 || nextCR >= end ? end : nextCR;
|
|
117
|
+
const at = Math.min(lf, cr);
|
|
118
|
+
if (at > pos) {
|
|
119
|
+
pending = true;
|
|
120
|
+
if (collecting())
|
|
121
|
+
take(buf, pos, at);
|
|
122
|
+
}
|
|
123
|
+
if (at === end)
|
|
124
|
+
break;
|
|
125
|
+
finish();
|
|
126
|
+
pos = at + 1;
|
|
127
|
+
if (at === cr) {
|
|
128
|
+
if (pos < end) {
|
|
129
|
+
if (buf[pos] === LF)
|
|
130
|
+
pos++;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
skipLF = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (n === 0)
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (pending)
|
|
141
|
+
finish();
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
closeSync(fd);
|
|
145
|
+
}
|
|
146
|
+
return { lines, totalLines: index, ...(truncatedBy ? { truncatedBy } : {}), bom };
|
|
147
|
+
}
|
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, readHead, readLineWindow } 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(readHead(abs, SNIFF_BYTES)))
|
|
103
|
+
return error(binaryMessage(shown));
|
|
104
|
+
const want = Math.min(input.limit ?? Infinity, DEFAULT_MAX_LINES + 1);
|
|
105
|
+
const win = readLineWindow(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)时的描述。 */
|
package/dist/tools/task.js
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 { AgentCatalog } from "../agents/catalog.js";
|
|
@@ -26,15 +28,20 @@ const THINKING = [
|
|
|
26
28
|
"high",
|
|
27
29
|
"xhigh",
|
|
28
30
|
];
|
|
31
|
+
/**
|
|
32
|
+
* [ME-A] 两版描述共用的开头:fresh / fork 两种上下文。default+task 与 codemode-only 的预算余量都只有
|
|
33
|
+
* 几十 token,加 `context` 参数时整段改写、字符数基本不增(`agent` 参数的「Default general」也去掉,
|
|
34
|
+
* 清单里 general 已标 default)。
|
|
35
|
+
*/
|
|
36
|
+
export const TASK_CONTEXT_LEAD = "Delegate to a sub-agent (fresh context: give full instructions; context fork: inherits " +
|
|
37
|
+
"this conversation).";
|
|
29
38
|
/** 缺省前台(`-p`、`subagents.background: never`)时的描述。 */
|
|
30
|
-
export const FOREGROUND_DESCRIPTION =
|
|
31
|
-
"
|
|
32
|
-
"returns a taskId now (see task_ctl); taskId: continue that task.";
|
|
39
|
+
export const FOREGROUND_DESCRIPTION = `${TASK_CONTEXT_LEAD} Returns its final report. Parallel in one reply; writers use isolation ` +
|
|
40
|
+
"worktree. background: returns a taskId (see task_ctl); taskId: continue it.";
|
|
33
41
|
/** [W7-B1] 缺省后台(交互 / RPC / ACP,docs/agents-concurrency-plan.md §2.6)时的描述。 */
|
|
34
|
-
export const BACKGROUND_DESCRIPTION =
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"reply run in parallel; writers use isolation worktree.";
|
|
42
|
+
export const BACKGROUND_DESCRIPTION = `${TASK_CONTEXT_LEAD} Runs in the background by default: returns a taskId; a ` +
|
|
43
|
+
"<task-notification> follows; keep working. background:false waits for the result. " +
|
|
44
|
+
"taskId: continue it. Parallel in one reply; writers use isolation worktree.";
|
|
38
45
|
/** 描述里类型清单的标题(清单本身另有 400 token 预算,见 agents/catalog.ts)。 */
|
|
39
46
|
export const TASK_AGENTS_HEADING = "\nAgents:\n";
|
|
40
47
|
const catalogs = new WeakMap();
|
|
@@ -68,6 +75,8 @@ export function buildSubagentRequest(input, ctx) {
|
|
|
68
75
|
return 'isolation must be "none" or "worktree"';
|
|
69
76
|
if (input.budgetUsd !== undefined && !(input.budgetUsd > 0))
|
|
70
77
|
return "budgetUsd must be > 0";
|
|
78
|
+
if (input.context !== undefined && input.context !== "fork" && input.context !== "fresh")
|
|
79
|
+
return 'context must be "fork" or "fresh"';
|
|
71
80
|
const request = {
|
|
72
81
|
prompt: input.prompt,
|
|
73
82
|
parentToolCallId: ctx.toolCallId,
|
|
@@ -96,6 +105,8 @@ export function buildSubagentRequest(input, ctx) {
|
|
|
96
105
|
request.model = input.model;
|
|
97
106
|
if (input.thinkingLevel !== undefined)
|
|
98
107
|
request.thinkingLevel = input.thinkingLevel;
|
|
108
|
+
if (input.context !== undefined)
|
|
109
|
+
request.context = input.context;
|
|
99
110
|
return request;
|
|
100
111
|
}
|
|
101
112
|
export function createTaskTool(_options = {}) {
|
|
@@ -111,7 +122,7 @@ export function createTaskTool(_options = {}) {
|
|
|
111
122
|
type: "object",
|
|
112
123
|
properties: {
|
|
113
124
|
prompt: { type: "string" },
|
|
114
|
-
agent: { type: "string"
|
|
125
|
+
agent: { type: "string" },
|
|
115
126
|
description: { type: "string" },
|
|
116
127
|
background: { type: "boolean" },
|
|
117
128
|
taskId: { type: "string" },
|
|
@@ -121,6 +132,7 @@ export function createTaskTool(_options = {}) {
|
|
|
121
132
|
model: { type: "string" },
|
|
122
133
|
thinkingLevel: { type: "string", enum: [...THINKING] },
|
|
123
134
|
maxTurns: { type: "integer" },
|
|
135
|
+
context: { type: "string", enum: ["fork", "fresh"] },
|
|
124
136
|
},
|
|
125
137
|
required: ["prompt"],
|
|
126
138
|
additionalProperties: false,
|
|
@@ -145,6 +157,7 @@ export function createTaskTool(_options = {}) {
|
|
|
145
157
|
...(result.status !== undefined ? { status: result.status } : {}),
|
|
146
158
|
...(result.sessionFile !== undefined ? { sessionFile: result.sessionFile } : {}),
|
|
147
159
|
...(result.outputFile !== undefined ? { outputFile: result.outputFile } : {}),
|
|
160
|
+
...(result.context !== undefined ? { context: result.context } : {}),
|
|
148
161
|
usage: result.usage,
|
|
149
162
|
stopReason: result.stopReason,
|
|
150
163
|
};
|
package/dist/tools/truncate.d.ts
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const DEFAULT_MAX_LINES = 2000;
|
|
10
10
|
export declare const DEFAULT_MAX_BYTES: number;
|
|
11
|
+
/** [ME-D] 自截时给说明行留的余量(字符)。 */
|
|
12
|
+
export declare const RESULT_NOTE_RESERVE = 512;
|
|
13
|
+
/**
|
|
14
|
+
* [ME-D] 工具自截的字节上限(D11):会话的结果上限(`ToolContext.maxResultChars`)减去说明行余量,
|
|
15
|
+
* 不超过 50 KB——一次截到位,不再被会话层二次截中段。字节数 ≤ N 时字符数必然 ≤ N。
|
|
16
|
+
*/
|
|
17
|
+
export declare function toolOutputBytes(maxResultChars: number | undefined): number;
|
|
11
18
|
/** grep 匹配行的字符上限。 */
|
|
12
19
|
export declare const GREP_MAX_LINE_CHARS = 500;
|
|
13
20
|
export interface TruncateOptions {
|
package/dist/tools/truncate.js
CHANGED
|
@@ -11,6 +11,17 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
export const DEFAULT_MAX_LINES = 2000;
|
|
13
13
|
export const DEFAULT_MAX_BYTES = 50 * 1024;
|
|
14
|
+
/** [ME-D] 自截时给说明行留的余量(字符)。 */
|
|
15
|
+
export const RESULT_NOTE_RESERVE = 512;
|
|
16
|
+
/**
|
|
17
|
+
* [ME-D] 工具自截的字节上限(D11):会话的结果上限(`ToolContext.maxResultChars`)减去说明行余量,
|
|
18
|
+
* 不超过 50 KB——一次截到位,不再被会话层二次截中段。字节数 ≤ N 时字符数必然 ≤ N。
|
|
19
|
+
*/
|
|
20
|
+
export function toolOutputBytes(maxResultChars) {
|
|
21
|
+
if (maxResultChars === undefined)
|
|
22
|
+
return DEFAULT_MAX_BYTES;
|
|
23
|
+
return Math.max(1024, Math.min(DEFAULT_MAX_BYTES, maxResultChars - RESULT_NOTE_RESERVE));
|
|
24
|
+
}
|
|
14
25
|
/** grep 匹配行的字符上限。 */
|
|
15
26
|
export const GREP_MAX_LINE_CHARS = 500;
|
|
16
27
|
export function byteLength(text) {
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -102,6 +102,8 @@ export interface SubagentRequest {
|
|
|
102
102
|
isolation?: "none" | "worktree";
|
|
103
103
|
/** [W5-C0] 外部 Agent 的美元预算。 */
|
|
104
104
|
budgetUsd?: number;
|
|
105
|
+
/** [ME-C0] `fork`:继承父会话已完成的回合(同模型、同前缀);缺省 `fresh`。 */
|
|
106
|
+
context?: "fork" | "fresh";
|
|
105
107
|
}
|
|
106
108
|
/** [W5-C0] 子 Agent 任务的终态(`subagent_end.status`)。 */
|
|
107
109
|
export type SubagentStatus = "completed" | "failed" | "aborted" | "max_turns" | "interrupted";
|
|
@@ -133,6 +135,8 @@ export interface SubagentResult {
|
|
|
133
135
|
/** [W5-C0] 结果全文(> 50 KB 截断时、后台任务的输出文件)。 */
|
|
134
136
|
outputFile?: string;
|
|
135
137
|
sessionRef?: SubagentSessionRef;
|
|
138
|
+
/** [ME-A] 请求了 `context: "fork"` 时的实际模式(回落为 `fresh`);未请求 fork 时缺省。 */
|
|
139
|
+
context?: "fork" | "fresh";
|
|
136
140
|
}
|
|
137
141
|
/** runner 上报的进度(由注册表转成 `subagent_update` 事件)。 */
|
|
138
142
|
export type SubagentEvent = {
|
|
@@ -173,6 +177,11 @@ export type SubagentEvent = {
|
|
|
173
177
|
type: "notice";
|
|
174
178
|
level: "info" | "warn";
|
|
175
179
|
text: string;
|
|
180
|
+
}
|
|
181
|
+
/** [ME-A] ama 子会话请求了 `context: "fork"` 时的实际模式(注册表写进 `TaskInfo.context`)。 */
|
|
182
|
+
| {
|
|
183
|
+
type: "context";
|
|
184
|
+
mode: "fork" | "fresh";
|
|
176
185
|
};
|
|
177
186
|
export interface SubagentRunRequest {
|
|
178
187
|
prompt: string;
|
|
@@ -225,6 +234,8 @@ export interface TaskInfo {
|
|
|
225
234
|
contextWindow?: number;
|
|
226
235
|
outputFile?: string;
|
|
227
236
|
sessionRef?: SubagentSessionRef;
|
|
237
|
+
/** [ME-C0] 子会话的实际上下文模式(fork 回落为 fresh 时记 `fresh`);外部 Agent 缺省。 */
|
|
238
|
+
context?: "fork" | "fresh";
|
|
228
239
|
}
|
|
229
240
|
/** [W5-C0] 任务注册表的只读视图(W5-G 的 `SubagentRegistry` 提供)。 */
|
|
230
241
|
export interface TaskRegistryView {
|
|
@@ -243,6 +254,8 @@ export interface ToolContext {
|
|
|
243
254
|
readonly thinkingLevel?: ModelThinkingLevel;
|
|
244
255
|
/** 截断全文的落盘目录(`<sessionDir>/outputs`);内存会话为 undefined。 */
|
|
245
256
|
readonly outputDir?: string;
|
|
257
|
+
/** [ME-C0] 会话层的工具结果上限(字符);工具据此一次截到位。宿主 / SDK 自建上下文可不给。 */
|
|
258
|
+
readonly maxResultChars?: number;
|
|
246
259
|
onUpdate(partial: string): void;
|
|
247
260
|
/** 本会话已 read 的绝对路径(write / edit 先读后写检查)。 */
|
|
248
261
|
readonly readFiles: ReadonlySet<string>;
|
|
@@ -33,7 +33,17 @@ export function traceData(entry) {
|
|
|
33
33
|
? data
|
|
34
34
|
: undefined;
|
|
35
35
|
}
|
|
36
|
+
/** [ME-A] fork 式子会话从父复制来的条目(首条 `ama.task{context:"fork"}` 之后到 forkedFrom 为止)。 */
|
|
37
|
+
function forkInherited(entries) {
|
|
38
|
+
const head = entries[0];
|
|
39
|
+
const data = head?.type === "custom" && head.customType === TASK_CUSTOM_TYPE ? head.data : {};
|
|
40
|
+
if (!isObject(data) || data["context"] !== "fork" || typeof data["forkedFrom"] !== "string")
|
|
41
|
+
return new Set();
|
|
42
|
+
const end = entries.findIndex((entry) => entry.id === data["forkedFrom"]);
|
|
43
|
+
return new Set(entries.slice(1, end + 1).map((entry) => entry.id));
|
|
44
|
+
}
|
|
36
45
|
export function buildIndex(entries) {
|
|
46
|
+
const inherited = forkInherited(entries);
|
|
37
47
|
const index = {
|
|
38
48
|
stepByAssistant: new Map(),
|
|
39
49
|
toolTiming: new Map(),
|
|
@@ -52,9 +62,11 @@ export function buildIndex(entries) {
|
|
|
52
62
|
}
|
|
53
63
|
if (entry.type === "custom" && entry.customType === TASK_CUSTOM_TYPE) {
|
|
54
64
|
const data = entry.data;
|
|
55
|
-
// 子会话自己的首条没有 status
|
|
65
|
+
// 子会话自己的首条没有 status,不是父会话的快照;fork 继承来的父快照也不算
|
|
56
66
|
if (!isObject(data) || typeof data["taskId"] !== "string" || data["status"] === undefined)
|
|
57
67
|
continue;
|
|
68
|
+
if (inherited.has(entry.id))
|
|
69
|
+
continue;
|
|
58
70
|
const parent = typeof data["parentToolCallId"] === "string" ? data["parentToolCallId"] : "";
|
|
59
71
|
const byTask = index.tasks.get(parent) ?? new Map();
|
|
60
72
|
byTask.set(data["taskId"], { data: data, entryId: entry.id });
|
package/dist/tui/ansi.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* - 所有截断 / 切片在保留 SGR 样式的同时,于末尾补 `\x1b[0m`,保证「每行末尾重置样式」。
|
|
8
8
|
*/
|
|
9
9
|
export declare const SGR_RESET = "\u001B[0m";
|
|
10
|
+
/** 字素切分器,首次用到才构造(-p / RPC / ACP / --version 不付 ICU 断字数据的初始化)。 */
|
|
11
|
+
export declare function graphemeSegmenter(): Intl.Segmenter;
|
|
10
12
|
/** 单个码点的列宽(0 / 1 / 2)。 */
|
|
11
13
|
export declare function codePointWidth(cp: number): 0 | 1 | 2;
|
|
12
14
|
/** 一个字素簇的列宽:首码点决定基础宽度;VS16(U+FE0F)把文本型 emoji 升为 2 列。 */
|
package/dist/tui/ansi.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* - 所有截断 / 切片在保留 SGR 样式的同时,于末尾补 `\x1b[0m`,保证「每行末尾重置样式」。
|
|
8
8
|
*/
|
|
9
9
|
export const SGR_RESET = "\x1b[0m";
|
|
10
|
-
|
|
10
|
+
let segmenter;
|
|
11
|
+
/** 字素切分器,首次用到才构造(-p / RPC / ACP / --version 不付 ICU 断字数据的初始化)。 */
|
|
12
|
+
export function graphemeSegmenter() {
|
|
13
|
+
return (segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }));
|
|
14
|
+
}
|
|
11
15
|
/** 东亚宽(W)与全角(F)区段,含常见 emoji 区块;按起点升序,二分查找。 */
|
|
12
16
|
const WIDE_RANGES = [
|
|
13
17
|
[0x1100, 0x115f],
|
|
@@ -193,7 +197,7 @@ function textWidth(text) {
|
|
|
193
197
|
if (isPlainAscii(text))
|
|
194
198
|
return text.length;
|
|
195
199
|
let width = 0;
|
|
196
|
-
for (const { segment } of
|
|
200
|
+
for (const { segment } of graphemeSegmenter().segment(text))
|
|
197
201
|
width += graphemeWidth(segment);
|
|
198
202
|
return width;
|
|
199
203
|
}
|
|
@@ -221,7 +225,7 @@ function* pieces(s) {
|
|
|
221
225
|
yield { text: ch, width: 1, escape: false };
|
|
222
226
|
}
|
|
223
227
|
else {
|
|
224
|
-
for (const { segment } of
|
|
228
|
+
for (const { segment } of graphemeSegmenter().segment(run)) {
|
|
225
229
|
yield { text: segment, width: graphemeWidth(segment), escape: false };
|
|
226
230
|
}
|
|
227
231
|
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* - 撤销:每次编辑前存快照;连续输入同类字符合并为一步;上限 50 步。
|
|
8
8
|
* - 不做选区(无鼠标、无 kill ring,§12.9)。
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
import { graphemeSegmenter } from "../ansi.js";
|
|
11
11
|
function graphemeStarts(line) {
|
|
12
12
|
const starts = [];
|
|
13
|
-
for (const { index } of
|
|
13
|
+
for (const { index } of graphemeSegmenter().segment(line))
|
|
14
14
|
starts.push(index);
|
|
15
15
|
starts.push(line.length);
|
|
16
16
|
return starts;
|