@earendil-works/pi-durable 1.0.1 → 1.0.3
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 +36 -0
- package/README.md +21 -5
- package/dist/env/decode.d.ts +16 -0
- package/dist/env/decode.d.ts.map +1 -0
- package/dist/env/decode.js +28 -0
- package/dist/env/decode.js.map +1 -0
- package/dist/env/index.d.ts +155 -3
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js +2 -0
- package/dist/env/index.js.map +1 -1
- package/dist/env/line-scan.d.ts +13 -0
- package/dist/env/line-scan.d.ts.map +1 -0
- package/dist/env/line-scan.js +147 -0
- package/dist/env/line-scan.js.map +1 -0
- package/dist/env/node-watch.d.ts +29 -0
- package/dist/env/node-watch.d.ts.map +1 -0
- package/dist/env/node-watch.js +431 -0
- package/dist/env/node-watch.js.map +1 -0
- package/dist/env/node.d.ts +11 -2
- package/dist/env/node.d.ts.map +1 -1
- package/dist/env/node.js +284 -21
- package/dist/env/node.js.map +1 -1
- package/dist/harness/agent.d.ts +2 -1
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +5 -0
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +2 -0
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +8 -5
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +4 -1
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/output.d.ts +13 -6
- package/dist/harness/output.d.ts.map +1 -1
- package/dist/harness/output.js +61 -16
- package/dist/harness/output.js.map +1 -1
- package/dist/harness/provider.d.ts +14 -0
- package/dist/harness/provider.d.ts.map +1 -0
- package/dist/harness/provider.js +30 -0
- package/dist/harness/provider.js.map +1 -0
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +12 -4
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/types.d.ts +24 -3
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/view.d.ts +1 -1
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +8 -1
- package/dist/harness/view.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/testing/env-conformance.d.ts +7 -0
- package/dist/testing/env-conformance.d.ts.map +1 -0
- package/dist/testing/env-conformance.js +397 -0
- package/dist/testing/env-conformance.js.map +1 -0
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/runner.d.ts +4 -2
- package/dist/testing/runner.d.ts.map +1 -1
- package/dist/testing/runner.js +9 -0
- package/dist/testing/runner.js.map +1 -1
- package/dist/testing/types.d.ts +18 -0
- package/dist/testing/types.d.ts.map +1 -1
- package/dist/testing/types.js.map +1 -1
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +5 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/image.d.ts +10 -0
- package/dist/tools/image.d.ts.map +1 -1
- package/dist/tools/image.js +39 -0
- package/dist/tools/image.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +148 -75
- package/dist/tools/read.js.map +1 -1
- package/dist/truncate.d.ts +9 -0
- package/dist/truncate.d.ts.map +1 -1
- package/dist/truncate.js +12 -4
- package/dist/truncate.js.map +1 -1
- package/package.json +3 -3
package/dist/tools/read.js
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { getOrThrow } from "../env/index.js";
|
|
2
|
+
import { getOrThrow, rangeDecoder, startsWithBom, } from "../env/index.js";
|
|
3
3
|
import { defineTool } from "../harness/define.js";
|
|
4
4
|
import { characterEnd } from "../harness/output.js";
|
|
5
|
-
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize,
|
|
5
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHeadOf, utf8ByteLength, } from "../truncate.js";
|
|
6
6
|
import { requireEnv } from "./env.js";
|
|
7
|
-
import {
|
|
7
|
+
import { detectSupportedImageMimeTypeOf } from "./image.js";
|
|
8
8
|
import { resolveReadToolPath } from "./path-utils.js";
|
|
9
9
|
const readSchema = Type.Object({
|
|
10
10
|
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
11
11
|
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
12
12
|
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
13
13
|
});
|
|
14
|
+
const READ_CHUNK = 64 * 1024;
|
|
15
|
+
/** `Array.prototype.slice`'s conversion of an index: NaN is 0, other values truncate toward zero. */
|
|
16
|
+
function sliceIndex(value) {
|
|
17
|
+
return Number.isNaN(value) ? 0 : Math.trunc(value);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The decoded start of bytes `[start, end)` of the file, decoded as part of the whole file: all of it, or enough for
|
|
21
|
+
* `truncateHeadOf` (more than `DEFAULT_MAX_BYTES + 1` bytes, or `DEFAULT_MAX_LINES` newlines).
|
|
22
|
+
*/
|
|
23
|
+
async function readHead(reader, start, end, skipBom, context) {
|
|
24
|
+
const decoder = rangeDecoder();
|
|
25
|
+
let text = "";
|
|
26
|
+
let newlines = 0;
|
|
27
|
+
for (let position = skipBom && start === 0 ? 3 : start; position < end;) {
|
|
28
|
+
const bytes = getOrThrow(await reader.read(position, Math.min(READ_CHUNK, end - position), context));
|
|
29
|
+
if (bytes.length === 0)
|
|
30
|
+
break;
|
|
31
|
+
position += bytes.length;
|
|
32
|
+
const decoded = decoder.decode(bytes, { stream: true });
|
|
33
|
+
text += decoded;
|
|
34
|
+
for (let index = decoded.indexOf("\n"); index !== -1; index = decoded.indexOf("\n", index + 1))
|
|
35
|
+
newlines++;
|
|
36
|
+
if (newlines >= DEFAULT_MAX_LINES || utf8ByteLength(text) > DEFAULT_MAX_BYTES + 1)
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
return text + decoder.decode();
|
|
40
|
+
}
|
|
14
41
|
/** Reads text files. Remarks about truncation and continuation are diagnostics; the content is only file text. */
|
|
15
42
|
export function createReadTool() {
|
|
16
43
|
return defineTool({
|
|
@@ -21,81 +48,127 @@ export function createReadTool() {
|
|
|
21
48
|
const { path, offset, limit } = args;
|
|
22
49
|
const env = requireEnv(api);
|
|
23
50
|
const absolutePath = await resolveReadToolPath(env, path, context);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
const textContent = new TextDecoder().decode(bytes);
|
|
41
|
-
const allLines = textContent.split("\n");
|
|
42
|
-
const totalFileLines = allLines.length;
|
|
43
|
-
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
44
|
-
const startLineDisplay = startLine + 1;
|
|
45
|
-
if (startLine >= allLines.length) {
|
|
46
|
-
throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
|
|
47
|
-
}
|
|
48
|
-
let selectedContent;
|
|
49
|
-
let userLimitedLines;
|
|
50
|
-
if (limit !== undefined) {
|
|
51
|
-
const endLine = Math.min(startLine + limit, allLines.length);
|
|
52
|
-
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
53
|
-
userLimitedLines = endLine - startLine;
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
selectedContent = allLines.slice(startLine).join("\n");
|
|
51
|
+
const reader = getOrThrow(await env.openBinaryReader(absolutePath, undefined, context));
|
|
52
|
+
try {
|
|
53
|
+
// A concurrent writer can change the file between the scan and the reads; retry once from a fresh scan.
|
|
54
|
+
for (let attempt = 0;; attempt++) {
|
|
55
|
+
const before = getOrThrow(await reader.info(context));
|
|
56
|
+
const result = await readText(reader, before, path, offset, limit, context);
|
|
57
|
+
const after = getOrThrow(await reader.info(context));
|
|
58
|
+
if (after.size === before.size && after.mtimeMs === before.mtimeMs)
|
|
59
|
+
return result;
|
|
60
|
+
if (attempt === 1)
|
|
61
|
+
throw new Error(`${path} changed while it was read`);
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
let outputText = headText;
|
|
61
|
-
let details;
|
|
62
|
-
if (truncation.firstLineExceedsLimit) {
|
|
63
|
-
// Show the start of the line, cut at the byte limit on a character boundary.
|
|
64
|
-
const lineBytes = new TextEncoder().encode(allLines[startLine]);
|
|
65
|
-
const end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);
|
|
66
|
-
outputText = new TextDecoder().decode(lineBytes.subarray(0, end));
|
|
67
|
-
diagnostics.push({
|
|
68
|
-
severity: "warn",
|
|
69
|
-
code: "truncated",
|
|
70
|
-
message: `Line ${startLineDisplay} is ${formatSize(lineBytes.byteLength)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,
|
|
71
|
-
});
|
|
72
|
-
details = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };
|
|
64
|
+
finally {
|
|
65
|
+
await reader.close(context);
|
|
73
66
|
}
|
|
74
|
-
else if (truncation.truncated) {
|
|
75
|
-
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
76
|
-
const nextOffset = endLineDisplay + 1;
|
|
77
|
-
const limitText = truncation.truncatedBy === "lines" ? "" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;
|
|
78
|
-
diagnostics.push({
|
|
79
|
-
severity: "info",
|
|
80
|
-
code: "truncated",
|
|
81
|
-
message: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,
|
|
82
|
-
});
|
|
83
|
-
details = { truncation };
|
|
84
|
-
}
|
|
85
|
-
else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
|
86
|
-
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
87
|
-
const nextOffset = startLine + userLimitedLines + 1;
|
|
88
|
-
diagnostics.push({
|
|
89
|
-
severity: "info",
|
|
90
|
-
message: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
content: outputText === "" ? [] : [{ type: "text", text: outputText }],
|
|
95
|
-
...(details === undefined ? {} : { details }),
|
|
96
|
-
diagnostics,
|
|
97
|
-
};
|
|
98
67
|
},
|
|
99
68
|
});
|
|
100
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* The read result for the opened file. It equals decoding the whole file with `TextDecoder`, splitting it on `\n`,
|
|
72
|
+
* and bounding the selected lines with `truncateHead`, while reading only one scan's worth of the file plus the head.
|
|
73
|
+
*/
|
|
74
|
+
async function readText(reader, info, path, offset, limit, context) {
|
|
75
|
+
const mimeType = await detectSupportedImageMimeTypeOf({
|
|
76
|
+
size: info.size,
|
|
77
|
+
read: async (position, length) => getOrThrow(await reader.read(position, length, context)),
|
|
78
|
+
});
|
|
79
|
+
if (mimeType) {
|
|
80
|
+
// Image content is not supported yet.
|
|
81
|
+
return {
|
|
82
|
+
content: [],
|
|
83
|
+
isError: true,
|
|
84
|
+
diagnostics: [
|
|
85
|
+
{
|
|
86
|
+
severity: "error",
|
|
87
|
+
code: "unsupported_image",
|
|
88
|
+
message: `${path} is an image (${mimeType}); reading images is not supported`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
94
|
+
const startLineDisplay = startLine + 1;
|
|
95
|
+
// Lines are selected like `allLines.slice(startLine, endLine)`, which truncates fractional indices.
|
|
96
|
+
const sliceStart = sliceIndex(startLine);
|
|
97
|
+
// One pass finds the line count and the selection; a selection past the last line ends with it, as `slice` does,
|
|
98
|
+
// and an empty one (a zero or negative limit) is scanned as one line and then ignored.
|
|
99
|
+
// A start beyond any file is scanned from 0 only to count lines; the offset check below then fails as before.
|
|
100
|
+
const scanStart = Number.isSafeInteger(sliceStart) ? sliceStart : 0;
|
|
101
|
+
const requestedEnd = limit === undefined ? undefined : Math.max(scanStart + 1, sliceIndex(startLine + limit));
|
|
102
|
+
const scanEnd = requestedEnd !== undefined && Number.isSafeInteger(requestedEnd) ? requestedEnd : undefined;
|
|
103
|
+
const scanOf = async (endLine) => getOrThrow(await reader.scanLines({ startLine: scanStart, ...(endLine === undefined ? {} : { endLine }) }, context));
|
|
104
|
+
let scan = await scanOf(scanEnd);
|
|
105
|
+
const totalFileLines = scan.newlines + 1;
|
|
106
|
+
if (startLine >= totalFileLines) {
|
|
107
|
+
throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);
|
|
108
|
+
}
|
|
109
|
+
let userLimitedLines;
|
|
110
|
+
let selectedLineCount = totalFileLines - sliceStart;
|
|
111
|
+
if (limit !== undefined) {
|
|
112
|
+
const endLine = Math.min(startLine + limit, totalFileLines);
|
|
113
|
+
userLimitedLines = endLine - startLine;
|
|
114
|
+
// `slice` counts a negative end from the end of the lines, which only the line count tells; scan again for it.
|
|
115
|
+
const relativeEnd = sliceIndex(endLine);
|
|
116
|
+
const sliceEnd = relativeEnd < 0 ? Math.max(totalFileLines + relativeEnd, 0) : relativeEnd;
|
|
117
|
+
selectedLineCount = Math.max(0, sliceEnd - sliceStart);
|
|
118
|
+
if (selectedLineCount > 0 && relativeEnd < 0)
|
|
119
|
+
scan = await scanOf(sliceEnd);
|
|
120
|
+
}
|
|
121
|
+
const empty = selectedLineCount === 0;
|
|
122
|
+
// Counted like `truncateHead`: a trailing newline adds no line, and empty text has none.
|
|
123
|
+
const endsWithNewline = !empty && scan.lastLineStart === scan.end && scan.lastLineStart > scan.start;
|
|
124
|
+
const totals = {
|
|
125
|
+
lines: empty || scan.selectedBytes === 0 ? 0 : selectedLineCount - (endsWithNewline ? 1 : 0),
|
|
126
|
+
bytes: empty ? 0 : scan.selectedBytes,
|
|
127
|
+
};
|
|
128
|
+
const firstBytes = getOrThrow(await reader.read(0, 3, context));
|
|
129
|
+
const head = empty ? "" : await readHead(reader, scan.start, scan.end, startsWithBom(firstBytes), context);
|
|
130
|
+
const { content: headText, ...truncation } = truncateHeadOf(head, totals);
|
|
131
|
+
const diagnostics = [];
|
|
132
|
+
let outputText = headText;
|
|
133
|
+
let details;
|
|
134
|
+
if (truncation.firstLineExceedsLimit) {
|
|
135
|
+
// Show the start of the line, cut at the byte limit on a character boundary. Like `allLines[startLine]`, a
|
|
136
|
+
// fractional start line names no line.
|
|
137
|
+
const integral = Number.isInteger(startLine);
|
|
138
|
+
const lineBytes = new TextEncoder().encode(integral ? (head.split("\n")[0] ?? "") : "");
|
|
139
|
+
const lineSize = integral ? scan.firstLineBytes : 0;
|
|
140
|
+
const end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);
|
|
141
|
+
outputText = new TextDecoder().decode(lineBytes.subarray(0, end));
|
|
142
|
+
diagnostics.push({
|
|
143
|
+
severity: "warn",
|
|
144
|
+
code: "truncated",
|
|
145
|
+
message: `Line ${startLineDisplay} is ${formatSize(lineSize)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,
|
|
146
|
+
});
|
|
147
|
+
details = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };
|
|
148
|
+
}
|
|
149
|
+
else if (truncation.truncated) {
|
|
150
|
+
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
151
|
+
const nextOffset = endLineDisplay + 1;
|
|
152
|
+
const limitText = truncation.truncatedBy === "lines" ? "" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;
|
|
153
|
+
diagnostics.push({
|
|
154
|
+
severity: "info",
|
|
155
|
+
code: "truncated",
|
|
156
|
+
message: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,
|
|
157
|
+
});
|
|
158
|
+
details = { truncation };
|
|
159
|
+
}
|
|
160
|
+
else if (userLimitedLines !== undefined && startLine + userLimitedLines < totalFileLines) {
|
|
161
|
+
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
162
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
163
|
+
diagnostics.push({
|
|
164
|
+
severity: "info",
|
|
165
|
+
message: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
content: outputText === "" ? [] : [{ type: "text", text: outputText }],
|
|
170
|
+
...(details === undefined ? {} : { details }),
|
|
171
|
+
diagnostics,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
101
174
|
//# sourceMappingURL=read.js.map
|
package/dist/tools/read.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Static, Type } from \"typebox\";\nimport { getOrThrow } from \"../env/index.ts\";\nimport { defineTool } from \"../harness/define.ts\";\nimport { characterEnd } from \"../harness/output.ts\";\nimport type { ToolDiagnostic, ToolRegistration } from \"../harness/types.ts\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"../truncate.ts\";\nimport { requireEnv } from \"./env.ts\";\nimport { detectSupportedImageMimeType } from \"./image.ts\";\nimport { resolveReadToolPath } from \"./path-utils.ts\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport type ReadToolDetails = {\n\t/** How the shown text was cut; the text itself is the result content. */\n\ttruncation?: Omit<TruncationResult, \"content\">;\n};\n\n/** Reads text files. Remarks about truncation and continuation are diagnostics; the content is only file text. */\nexport function createReadTool(): ToolRegistration<typeof readSchema, ReadToolDetails> {\n\treturn defineTool({\n\t\tname: \"read\",\n\t\tdescription: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tparameters: readSchema,\n\t\tasync execute(args, api, context) {\n\t\t\tconst { path, offset, limit } = args;\n\t\t\tconst env = requireEnv(api);\n\t\t\tconst absolutePath = await resolveReadToolPath(env, path, context);\n\t\t\tconst bytes = getOrThrow(await env.readBinaryFile(absolutePath, context));\n\t\t\tconst mimeType = detectSupportedImageMimeType(bytes);\n\t\t\tif (mimeType) {\n\t\t\t\t// Image content is not supported yet.\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [],\n\t\t\t\t\tisError: true,\n\t\t\t\t\tdiagnostics: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tseverity: \"error\",\n\t\t\t\t\t\t\tcode: \"unsupported_image\",\n\t\t\t\t\t\t\tmessage: `${path} is an image (${mimeType}); reading images is not supported`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst textContent = new TextDecoder().decode(bytes);\n\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\tconst totalFileLines = allLines.length;\n\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\tif (startLine >= allLines.length) {\n\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t}\n\n\t\t\tlet selectedContent: string;\n\t\t\tlet userLimitedLines: number | undefined;\n\t\t\tif (limit !== undefined) {\n\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t} else {\n\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t}\n\n\t\t\tconst { content: headText, ...truncation } = truncateHead(selectedContent);\n\t\t\tconst diagnostics: ToolDiagnostic[] = [];\n\t\t\tlet outputText = headText;\n\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t// Show the start of the line, cut at the byte limit on a character boundary.\n\t\t\t\tconst lineBytes = new TextEncoder().encode(allLines[startLine]);\n\t\t\t\tconst end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);\n\t\t\t\toutputText = new TextDecoder().decode(lineBytes.subarray(0, end));\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"warn\",\n\t\t\t\t\tcode: \"truncated\",\n\t\t\t\t\tmessage: `Line ${startLineDisplay} is ${formatSize(lineBytes.byteLength)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,\n\t\t\t\t});\n\t\t\t\tdetails = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };\n\t\t\t} else if (truncation.truncated) {\n\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\tconst limitText = truncation.truncatedBy === \"lines\" ? \"\" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"info\",\n\t\t\t\t\tcode: \"truncated\",\n\t\t\t\t\tmessage: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,\n\t\t\t\t});\n\t\t\t\tdetails = { truncation };\n\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"info\",\n\t\t\t\t\tmessage: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: outputText === \"\" ? [] : [{ type: \"text\", text: outputText }],\n\t\t\t\t...(details === undefined ? {} : { details }),\n\t\t\t\tdiagnostics,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context } from \"@earendil-works/chord\";\nimport { type Static, Type } from \"typebox\";\nimport {\n\ttype BinaryReader,\n\ttype FileInfo,\n\tgetOrThrow,\n\ttype LineScan,\n\trangeDecoder,\n\tstartsWithBom,\n} from \"../env/index.ts\";\nimport { defineTool } from \"../harness/define.ts\";\nimport { characterEnd } from \"../harness/output.ts\";\nimport type { ToolDiagnostic, ToolRegistration } from \"../harness/types.ts\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tDEFAULT_MAX_LINES,\n\tformatSize,\n\ttype TruncationResult,\n\ttruncateHeadOf,\n\tutf8ByteLength,\n} from \"../truncate.ts\";\nimport { requireEnv } from \"./env.ts\";\nimport { detectSupportedImageMimeTypeOf } from \"./image.ts\";\nimport { resolveReadToolPath } from \"./path-utils.ts\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport type ReadToolDetails = {\n\t/** How the shown text was cut; the text itself is the result content. */\n\ttruncation?: Omit<TruncationResult, \"content\">;\n};\n\nconst READ_CHUNK = 64 * 1024;\n\n/** `Array.prototype.slice`'s conversion of an index: NaN is 0, other values truncate toward zero. */\nfunction sliceIndex(value: number): number {\n\treturn Number.isNaN(value) ? 0 : Math.trunc(value);\n}\n\n/**\n * The decoded start of bytes `[start, end)` of the file, decoded as part of the whole file: all of it, or enough for\n * `truncateHeadOf` (more than `DEFAULT_MAX_BYTES + 1` bytes, or `DEFAULT_MAX_LINES` newlines).\n */\nasync function readHead(\n\treader: BinaryReader,\n\tstart: number,\n\tend: number,\n\tskipBom: boolean,\n\tcontext: Context,\n): Promise<string> {\n\tconst decoder = rangeDecoder();\n\tlet text = \"\";\n\tlet newlines = 0;\n\tfor (let position = skipBom && start === 0 ? 3 : start; position < end; ) {\n\t\tconst bytes = getOrThrow(await reader.read(position, Math.min(READ_CHUNK, end - position), context));\n\t\tif (bytes.length === 0) break;\n\t\tposition += bytes.length;\n\t\tconst decoded = decoder.decode(bytes, { stream: true });\n\t\ttext += decoded;\n\t\tfor (let index = decoded.indexOf(\"\\n\"); index !== -1; index = decoded.indexOf(\"\\n\", index + 1)) newlines++;\n\t\tif (newlines >= DEFAULT_MAX_LINES || utf8ByteLength(text) > DEFAULT_MAX_BYTES + 1) return text;\n\t}\n\treturn text + decoder.decode();\n}\n\n/** Reads text files. Remarks about truncation and continuation are diagnostics; the content is only file text. */\nexport function createReadTool(): ToolRegistration<typeof readSchema, ReadToolDetails> {\n\treturn defineTool({\n\t\tname: \"read\",\n\t\tdescription: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tparameters: readSchema,\n\t\tasync execute(args, api, context) {\n\t\t\tconst { path, offset, limit } = args;\n\t\t\tconst env = requireEnv(api);\n\t\t\tconst absolutePath = await resolveReadToolPath(env, path, context);\n\t\t\tconst reader = getOrThrow(await env.openBinaryReader(absolutePath, undefined, context));\n\t\t\ttry {\n\t\t\t\t// A concurrent writer can change the file between the scan and the reads; retry once from a fresh scan.\n\t\t\t\tfor (let attempt = 0; ; attempt++) {\n\t\t\t\t\tconst before = getOrThrow(await reader.info(context));\n\t\t\t\t\tconst result = await readText(reader, before, path, offset, limit, context);\n\t\t\t\t\tconst after = getOrThrow(await reader.info(context));\n\t\t\t\t\tif (after.size === before.size && after.mtimeMs === before.mtimeMs) return result;\n\t\t\t\t\tif (attempt === 1) throw new Error(`${path} changed while it was read`);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tawait reader.close(context);\n\t\t\t}\n\t\t},\n\t});\n}\n\n/**\n * The read result for the opened file. It equals decoding the whole file with `TextDecoder`, splitting it on `\\n`,\n * and bounding the selected lines with `truncateHead`, while reading only one scan's worth of the file plus the head.\n */\nasync function readText(\n\treader: BinaryReader,\n\tinfo: FileInfo,\n\tpath: string,\n\toffset: number | undefined,\n\tlimit: number | undefined,\n\tcontext: Context,\n) {\n\tconst mimeType = await detectSupportedImageMimeTypeOf({\n\t\tsize: info.size,\n\t\tread: async (position, length) => getOrThrow(await reader.read(position, length, context)),\n\t});\n\tif (mimeType) {\n\t\t// Image content is not supported yet.\n\t\treturn {\n\t\t\tcontent: [],\n\t\t\tisError: true,\n\t\t\tdiagnostics: [\n\t\t\t\t{\n\t\t\t\t\tseverity: \"error\" as const,\n\t\t\t\t\tcode: \"unsupported_image\",\n\t\t\t\t\tmessage: `${path} is an image (${mimeType}); reading images is not supported`,\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\tconst startLineDisplay = startLine + 1;\n\t// Lines are selected like `allLines.slice(startLine, endLine)`, which truncates fractional indices.\n\tconst sliceStart = sliceIndex(startLine);\n\t// One pass finds the line count and the selection; a selection past the last line ends with it, as `slice` does,\n\t// and an empty one (a zero or negative limit) is scanned as one line and then ignored.\n\t// A start beyond any file is scanned from 0 only to count lines; the offset check below then fails as before.\n\tconst scanStart = Number.isSafeInteger(sliceStart) ? sliceStart : 0;\n\tconst requestedEnd = limit === undefined ? undefined : Math.max(scanStart + 1, sliceIndex(startLine + limit));\n\tconst scanEnd = requestedEnd !== undefined && Number.isSafeInteger(requestedEnd) ? requestedEnd : undefined;\n\tconst scanOf = async (endLine: number | undefined): Promise<LineScan> =>\n\t\tgetOrThrow(\n\t\t\tawait reader.scanLines({ startLine: scanStart, ...(endLine === undefined ? {} : { endLine }) }, context),\n\t\t);\n\tlet scan = await scanOf(scanEnd);\n\tconst totalFileLines = scan.newlines + 1;\n\tif (startLine >= totalFileLines) {\n\t\tthrow new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);\n\t}\n\n\tlet userLimitedLines: number | undefined;\n\tlet selectedLineCount = totalFileLines - sliceStart;\n\tif (limit !== undefined) {\n\t\tconst endLine = Math.min(startLine + limit, totalFileLines);\n\t\tuserLimitedLines = endLine - startLine;\n\t\t// `slice` counts a negative end from the end of the lines, which only the line count tells; scan again for it.\n\t\tconst relativeEnd = sliceIndex(endLine);\n\t\tconst sliceEnd = relativeEnd < 0 ? Math.max(totalFileLines + relativeEnd, 0) : relativeEnd;\n\t\tselectedLineCount = Math.max(0, sliceEnd - sliceStart);\n\t\tif (selectedLineCount > 0 && relativeEnd < 0) scan = await scanOf(sliceEnd);\n\t}\n\tconst empty = selectedLineCount === 0;\n\t// Counted like `truncateHead`: a trailing newline adds no line, and empty text has none.\n\tconst endsWithNewline = !empty && scan.lastLineStart === scan.end && scan.lastLineStart > scan.start;\n\tconst totals = {\n\t\tlines: empty || scan.selectedBytes === 0 ? 0 : selectedLineCount - (endsWithNewline ? 1 : 0),\n\t\tbytes: empty ? 0 : scan.selectedBytes,\n\t};\n\tconst firstBytes = getOrThrow(await reader.read(0, 3, context));\n\tconst head = empty ? \"\" : await readHead(reader, scan.start, scan.end, startsWithBom(firstBytes), context);\n\n\tconst { content: headText, ...truncation } = truncateHeadOf(head, totals);\n\tconst diagnostics: ToolDiagnostic[] = [];\n\tlet outputText = headText;\n\tlet details: ReadToolDetails | undefined;\n\tif (truncation.firstLineExceedsLimit) {\n\t\t// Show the start of the line, cut at the byte limit on a character boundary. Like `allLines[startLine]`, a\n\t\t// fractional start line names no line.\n\t\tconst integral = Number.isInteger(startLine);\n\t\tconst lineBytes = new TextEncoder().encode(integral ? (head.split(\"\\n\")[0] ?? \"\") : \"\");\n\t\tconst lineSize = integral ? scan.firstLineBytes : 0;\n\t\tconst end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);\n\t\toutputText = new TextDecoder().decode(lineBytes.subarray(0, end));\n\t\tdiagnostics.push({\n\t\t\tseverity: \"warn\",\n\t\t\tcode: \"truncated\",\n\t\t\tmessage: `Line ${startLineDisplay} is ${formatSize(lineSize)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,\n\t\t});\n\t\tdetails = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };\n\t} else if (truncation.truncated) {\n\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\tconst nextOffset = endLineDisplay + 1;\n\t\tconst limitText = truncation.truncatedBy === \"lines\" ? \"\" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;\n\t\tdiagnostics.push({\n\t\t\tseverity: \"info\",\n\t\t\tcode: \"truncated\",\n\t\t\tmessage: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,\n\t\t});\n\t\tdetails = { truncation };\n\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < totalFileLines) {\n\t\tconst remaining = totalFileLines - (startLine + userLimitedLines);\n\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\tdiagnostics.push({\n\t\t\tseverity: \"info\",\n\t\t\tmessage: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,\n\t\t});\n\t}\n\n\treturn {\n\t\tcontent: outputText === \"\" ? [] : [{ type: \"text\" as const, text: outputText }],\n\t\t...(details === undefined ? {} : { details }),\n\t\tdiagnostics,\n\t};\n}\n"]}
|
package/dist/truncate.d.ts
CHANGED
|
@@ -52,4 +52,13 @@ export declare function formatSize(bytes: number): string;
|
|
|
52
52
|
* returns empty content with firstLineExceedsLimit=true.
|
|
53
53
|
*/
|
|
54
54
|
export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
|
|
55
|
+
/**
|
|
56
|
+
* `truncateHead` of a text known by a prefix and its totals (`lines` counted like `truncateHead`, ignoring a trailing
|
|
57
|
+
* newline). The prefix must be the whole text, or longer than `maxBytes + 1` UTF-8 bytes, or hold at least `maxLines`
|
|
58
|
+
* newlines; then the result equals `truncateHead` of the whole text.
|
|
59
|
+
*/
|
|
60
|
+
export declare function truncateHeadOf(prefix: string, totals: {
|
|
61
|
+
readonly lines: number;
|
|
62
|
+
readonly bytes: number;
|
|
63
|
+
}, options?: TruncationOptions): TruncationResult;
|
|
55
64
|
//# sourceMappingURL=truncate.d.ts.map
|
package/dist/truncate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAChC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AASD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BtD;AASD;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAChC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AASD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BtD;AASD;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAM/F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC7B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC1D,OAAO,GAAE,iBAAsB,GAC7B,gBAAgB,CAgFlB"}
|
package/dist/truncate.js
CHANGED
|
@@ -72,15 +72,23 @@ export function formatSize(bytes) {
|
|
|
72
72
|
* returns empty content with firstLineExceedsLimit=true.
|
|
73
73
|
*/
|
|
74
74
|
export function truncateHead(content, options = {}) {
|
|
75
|
+
return truncateHeadOf(content, { lines: splitLinesForCounting(content).length, bytes: utf8ByteLength(content) }, options);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `truncateHead` of a text known by a prefix and its totals (`lines` counted like `truncateHead`, ignoring a trailing
|
|
79
|
+
* newline). The prefix must be the whole text, or longer than `maxBytes + 1` UTF-8 bytes, or hold at least `maxLines`
|
|
80
|
+
* newlines; then the result equals `truncateHead` of the whole text.
|
|
81
|
+
*/
|
|
82
|
+
export function truncateHeadOf(prefix, totals, options = {}) {
|
|
75
83
|
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
76
84
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
77
|
-
const totalBytes =
|
|
78
|
-
const lines = splitLinesForCounting(
|
|
79
|
-
const totalLines = lines
|
|
85
|
+
const totalBytes = totals.bytes;
|
|
86
|
+
const lines = splitLinesForCounting(prefix);
|
|
87
|
+
const totalLines = totals.lines;
|
|
80
88
|
// Check if no truncation needed
|
|
81
89
|
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
82
90
|
return {
|
|
83
|
-
content,
|
|
91
|
+
content: prefix,
|
|
84
92
|
truncated: false,
|
|
85
93
|
truncatedBy: null,
|
|
86
94
|
totalLines,
|
package/dist/truncate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"truncate.js","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines. Tool output streams are bounded by `src/harness/output.ts` instead.\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n\t/** The max lines limit that was applied */\n\tmaxLines: number;\n\t/** The max bytes limit that was applied */\n\tmaxBytes: number;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\ninterface RuntimeBuffer {\n\tbyteLength(content: string, encoding: \"utf8\"): number;\n}\n\nconst runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer;\nconst nonAsciiPattern = /[^\\x00-\\x7f]/;\n\nexport function utf8ByteLength(content: string): number {\n\tif (runtimeBuffer) return runtimeBuffer.byteLength(content, \"utf8\");\n\n\tconst firstNonAscii = content.search(nonAsciiPattern);\n\tif (firstNonAscii === -1) return content.length;\n\n\tlet bytes = firstNonAscii;\n\tfor (let i = firstNonAscii; i < content.length; i++) {\n\t\tconst code = content.charCodeAt(i);\n\t\tif (code <= 0x7f) {\n\t\t\tbytes += 1;\n\t\t} else if (code <= 0x7ff) {\n\t\t\tbytes += 2;\n\t\t} else if (code >= 0xd800 && code <= 0xdbff && i + 1 < content.length) {\n\t\t\tconst next = content.charCodeAt(i + 1);\n\t\t\tif (next >= 0xdc00 && next <= 0xdfff) {\n\t\t\t\tbytes += 4;\n\t\t\t\ti++;\n\t\t\t} else {\n\t\t\t\tbytes += 3;\n\t\t\t}\n\t\t} else {\n\t\t\tbytes += 3;\n\t\t}\n\t}\n\treturn bytes;\n}\n\nfunction splitLinesForCounting(content: string): string[] {\n\tif (content.length === 0) return [];\n\tconst lines = content.split(\"\\n\");\n\tif (content.endsWith(\"\\n\")) lines.pop();\n\treturn lines;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = utf8ByteLength(content);\n\tconst lines = splitLinesForCounting(content);\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = utf8ByteLength(lines[0]);\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// Without a byte break, only omitted lines prove the line limit was reached; otherwise a trailing newline exceeded bytes.\n\tif (truncatedBy !== \"bytes\") truncatedBy = outputLinesArr.length < totalLines ? \"lines\" : \"bytes\";\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = utf8ByteLength(outputContent);\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"truncate.js","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines. Tool output streams are bounded by `src/harness/output.ts` instead.\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n\t/** The max lines limit that was applied */\n\tmaxLines: number;\n\t/** The max bytes limit that was applied */\n\tmaxBytes: number;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\ninterface RuntimeBuffer {\n\tbyteLength(content: string, encoding: \"utf8\"): number;\n}\n\nconst runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer;\nconst nonAsciiPattern = /[^\\x00-\\x7f]/;\n\nexport function utf8ByteLength(content: string): number {\n\tif (runtimeBuffer) return runtimeBuffer.byteLength(content, \"utf8\");\n\n\tconst firstNonAscii = content.search(nonAsciiPattern);\n\tif (firstNonAscii === -1) return content.length;\n\n\tlet bytes = firstNonAscii;\n\tfor (let i = firstNonAscii; i < content.length; i++) {\n\t\tconst code = content.charCodeAt(i);\n\t\tif (code <= 0x7f) {\n\t\t\tbytes += 1;\n\t\t} else if (code <= 0x7ff) {\n\t\t\tbytes += 2;\n\t\t} else if (code >= 0xd800 && code <= 0xdbff && i + 1 < content.length) {\n\t\t\tconst next = content.charCodeAt(i + 1);\n\t\t\tif (next >= 0xdc00 && next <= 0xdfff) {\n\t\t\t\tbytes += 4;\n\t\t\t\ti++;\n\t\t\t} else {\n\t\t\t\tbytes += 3;\n\t\t\t}\n\t\t} else {\n\t\t\tbytes += 3;\n\t\t}\n\t}\n\treturn bytes;\n}\n\nfunction splitLinesForCounting(content: string): string[] {\n\tif (content.length === 0) return [];\n\tconst lines = content.split(\"\\n\");\n\tif (content.endsWith(\"\\n\")) lines.pop();\n\treturn lines;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\treturn truncateHeadOf(\n\t\tcontent,\n\t\t{ lines: splitLinesForCounting(content).length, bytes: utf8ByteLength(content) },\n\t\toptions,\n\t);\n}\n\n/**\n * `truncateHead` of a text known by a prefix and its totals (`lines` counted like `truncateHead`, ignoring a trailing\n * newline). The prefix must be the whole text, or longer than `maxBytes + 1` UTF-8 bytes, or hold at least `maxLines`\n * newlines; then the result equals `truncateHead` of the whole text.\n */\nexport function truncateHeadOf(\n\tprefix: string,\n\ttotals: { readonly lines: number; readonly bytes: number },\n\toptions: TruncationOptions = {},\n): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = totals.bytes;\n\tconst lines = splitLinesForCounting(prefix);\n\tconst totalLines = totals.lines;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent: prefix,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = utf8ByteLength(lines[0]);\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// Without a byte break, only omitted lines prove the line limit was reached; otherwise a trailing newline exceeded bytes.\n\tif (truncatedBy !== \"bytes\") truncatedBy = outputLinesArr.length < totalLines ? \"lines\" : \"bytes\";\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = utf8ByteLength(outputContent);\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@earendil-works/pi-durable",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Durable conversation, task, and document runtime for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"node": ">=22.19.0"
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
|
-
"@earendil-works/chord": "^1.0.
|
|
104
|
-
"@earendil-works/pi-ai": "^1.0.
|
|
103
|
+
"@earendil-works/chord": "^1.0.3",
|
|
104
|
+
"@earendil-works/pi-ai": "^1.0.3",
|
|
105
105
|
"diff": "8.0.4",
|
|
106
106
|
"typebox": "1.3.27"
|
|
107
107
|
},
|