@earendil-works/pi-durable 0.87.1 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -4
- package/dist/documents.d.ts +66 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +98 -0
- package/dist/documents.js.map +1 -0
- package/dist/entries.d.ts +13 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +18 -0
- package/dist/entries.js.map +1 -0
- package/dist/env/index.d.ts +131 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +45 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/node.d.ts +45 -0
- package/dist/env/node.d.ts.map +1 -0
- package/dist/env/node.js +908 -0
- package/dist/env/node.js.map +1 -0
- package/dist/env/utils/adaptive-publisher.d.ts +24 -0
- package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
- package/dist/env/utils/adaptive-publisher.js +79 -0
- package/dist/env/utils/adaptive-publisher.js.map +1 -0
- package/dist/env/utils/output-capture.d.ts +31 -0
- package/dist/env/utils/output-capture.d.ts.map +1 -0
- package/dist/env/utils/output-capture.js +238 -0
- package/dist/env/utils/output-capture.js.map +1 -0
- package/dist/env/utils/truncate.d.ts +71 -0
- package/dist/env/utils/truncate.d.ts.map +1 -0
- package/dist/env/utils/truncate.js +294 -0
- package/dist/env/utils/truncate.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness/config.d.ts +20 -0
- package/dist/harness/config.d.ts.map +1 -0
- package/dist/harness/config.js +18 -0
- package/dist/harness/config.js.map +1 -0
- package/dist/harness/context.d.ts +27 -0
- package/dist/harness/context.d.ts.map +1 -0
- package/dist/harness/context.js +109 -0
- package/dist/harness/context.js.map +1 -0
- package/dist/harness/generation.d.ts +36 -0
- package/dist/harness/generation.d.ts.map +1 -0
- package/dist/harness/generation.js +266 -0
- package/dist/harness/generation.js.map +1 -0
- package/dist/harness/harness.d.ts +10 -0
- package/dist/harness/harness.d.ts.map +1 -0
- package/dist/harness/harness.js +266 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/live.d.ts +40 -0
- package/dist/harness/live.d.ts.map +1 -0
- package/dist/harness/live.js +41 -0
- package/dist/harness/live.js.map +1 -0
- package/dist/harness/prompt.d.ts +24 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +88 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/registry.d.ts +7 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +309 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/scheduler.d.ts +63 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +605 -0
- package/dist/harness/scheduler.js.map +1 -0
- package/dist/harness/submissions.d.ts +24 -0
- package/dist/harness/submissions.d.ts.map +1 -0
- package/dist/harness/submissions.js +133 -0
- package/dist/harness/submissions.js.map +1 -0
- package/dist/harness/types.d.ts +358 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/util.d.ts +14 -0
- package/dist/harness/util.d.ts.map +1 -0
- package/dist/harness/util.js +55 -0
- package/dist/harness/util.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +9 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/session/forks.d.ts +10 -0
- package/dist/session/forks.d.ts.map +1 -0
- package/dist/session/forks.js +42 -0
- package/dist/session/forks.js.map +1 -0
- package/dist/session/observation.d.ts +28 -0
- package/dist/session/observation.d.ts.map +1 -0
- package/dist/session/observation.js +280 -0
- package/dist/session/observation.js.map +1 -0
- package/dist/session/session.d.ts +59 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +380 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/transaction.d.ts +105 -0
- package/dist/session/transaction.d.ts.map +1 -0
- package/dist/session/transaction.js +718 -0
- package/dist/session/transaction.js.map +1 -0
- package/dist/storage/jsonl/index.d.ts +2 -0
- package/dist/storage/jsonl/index.d.ts.map +1 -0
- package/dist/storage/jsonl/index.js +2 -0
- package/dist/storage/jsonl/index.js.map +1 -0
- package/dist/storage/jsonl/node.d.ts +6 -0
- package/dist/storage/jsonl/node.d.ts.map +1 -0
- package/dist/storage/jsonl/node.js +8 -0
- package/dist/storage/jsonl/node.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +115 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -0
- package/dist/storage/jsonl/storage.js +681 -0
- package/dist/storage/jsonl/storage.js.map +1 -0
- package/dist/storage/memory.d.ts +31 -14
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +208 -61
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/database.d.ts +5 -1
- package/dist/storage/sqlite/database.d.ts.map +1 -1
- package/dist/storage/sqlite/database.js.map +1 -1
- package/dist/storage/sqlite/index.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.d.ts +1 -1
- package/dist/storage/sqlite/migrations.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.js +8 -1
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/node.d.ts.map +1 -1
- package/dist/storage/sqlite/node.js +2 -2
- package/dist/storage/sqlite/node.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts +20 -14
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +159 -55
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +5 -0
- package/dist/tasks.js.map +1 -0
- package/dist/testing/assertions.d.ts +5 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +25 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/runner.d.ts +10 -0
- package/dist/testing/runner.d.ts.map +1 -0
- package/dist/testing/runner.js +14 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/storage-benchmark.d.ts +41 -0
- package/dist/testing/storage-benchmark.d.ts.map +1 -0
- package/dist/testing/storage-benchmark.js +351 -0
- package/dist/testing/storage-benchmark.js.map +1 -0
- package/dist/testing/storage-conformance.d.ts +4 -0
- package/dist/testing/storage-conformance.d.ts.map +1 -0
- package/dist/testing/storage-conformance.js +1122 -0
- package/dist/testing/storage-conformance.js.map +1 -0
- package/dist/testing/types.d.ts +19 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/types.d.ts +422 -54
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/chord-delta-findings.md +1 -1
- package/docs/pico-v5-chord-usage.md +47 -60
- package/docs/pico-v5-handoff.md +469 -264
- package/docs/pico-v5.md +1523 -639
- package/package.json +30 -5
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared truncation utilities for tool outputs.
|
|
3
|
+
*
|
|
4
|
+
* Truncation is based on two independent limits - whichever is hit first wins:
|
|
5
|
+
* - Line limit (default: 2000 lines)
|
|
6
|
+
* - Byte limit (default: 50KB)
|
|
7
|
+
*
|
|
8
|
+
* Never returns partial lines (except bash tail truncation edge case).
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_MAX_LINES = 2000;
|
|
11
|
+
export declare const DEFAULT_MAX_BYTES: number;
|
|
12
|
+
export declare const GREP_MAX_LINE_LENGTH = 500;
|
|
13
|
+
export interface TruncationResult {
|
|
14
|
+
/** The truncated content */
|
|
15
|
+
content: string;
|
|
16
|
+
/** Whether truncation occurred */
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
/** Which limit was hit: "lines", "bytes", or null if not truncated */
|
|
19
|
+
truncatedBy: "lines" | "bytes" | null;
|
|
20
|
+
/** Total number of lines in the original content */
|
|
21
|
+
totalLines: number;
|
|
22
|
+
/** Total number of bytes in the original content */
|
|
23
|
+
totalBytes: number;
|
|
24
|
+
/** Number of complete lines in the truncated output */
|
|
25
|
+
outputLines: number;
|
|
26
|
+
/** Number of bytes in the truncated output */
|
|
27
|
+
outputBytes: number;
|
|
28
|
+
/** Whether the last line was partially truncated (only for tail truncation edge case) */
|
|
29
|
+
lastLinePartial: boolean;
|
|
30
|
+
/** Whether the first line exceeded the byte limit (for head truncation) */
|
|
31
|
+
firstLineExceedsLimit: boolean;
|
|
32
|
+
/** The max lines limit that was applied */
|
|
33
|
+
maxLines: number;
|
|
34
|
+
/** The max bytes limit that was applied */
|
|
35
|
+
maxBytes: number;
|
|
36
|
+
}
|
|
37
|
+
export interface TruncationOptions {
|
|
38
|
+
/** Maximum number of lines (default: 2000) */
|
|
39
|
+
maxLines?: number;
|
|
40
|
+
/** Maximum number of bytes (default: 50KB) */
|
|
41
|
+
maxBytes?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function utf8ByteLength(content: string): number;
|
|
44
|
+
/**
|
|
45
|
+
* Format bytes as human-readable size.
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatSize(bytes: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Truncate content from the head (keep first N lines/bytes).
|
|
50
|
+
* Suitable for file reads where you want to see the beginning.
|
|
51
|
+
*
|
|
52
|
+
* Never returns partial lines. If first line exceeds byte limit,
|
|
53
|
+
* returns empty content with firstLineExceedsLimit=true.
|
|
54
|
+
*/
|
|
55
|
+
export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
|
|
56
|
+
/**
|
|
57
|
+
* Truncate content from the tail (keep last N lines/bytes).
|
|
58
|
+
* Suitable for bash output where you want to see the end (errors, final results).
|
|
59
|
+
*
|
|
60
|
+
* May return partial first line if the last line of original content exceeds byte limit.
|
|
61
|
+
*/
|
|
62
|
+
export declare function truncateTail(content: string, options?: TruncationOptions): TruncationResult;
|
|
63
|
+
/**
|
|
64
|
+
* Truncate a single line to max characters, adding [truncated] suffix.
|
|
65
|
+
* Used for grep match lines.
|
|
66
|
+
*/
|
|
67
|
+
export declare function truncateLine(line: string, maxChars?: number): {
|
|
68
|
+
text: string;
|
|
69
|
+
wasTruncated: boolean;
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=truncate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../../src/env/utils/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAC3C,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,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;AAgCD;;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,CAgF/F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAuE/F;AA2CD;;;GAGG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAA6B,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAKzC"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared truncation utilities for tool outputs.
|
|
3
|
+
*
|
|
4
|
+
* Truncation is based on two independent limits - whichever is hit first wins:
|
|
5
|
+
* - Line limit (default: 2000 lines)
|
|
6
|
+
* - Byte limit (default: 50KB)
|
|
7
|
+
*
|
|
8
|
+
* Never returns partial lines (except bash tail truncation edge case).
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_MAX_LINES = 2000;
|
|
11
|
+
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
12
|
+
export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
|
|
13
|
+
const runtimeBuffer = globalThis.Buffer;
|
|
14
|
+
const nonAsciiPattern = /[^\x00-\x7f]/;
|
|
15
|
+
export function utf8ByteLength(content) {
|
|
16
|
+
if (runtimeBuffer)
|
|
17
|
+
return runtimeBuffer.byteLength(content, "utf8");
|
|
18
|
+
const firstNonAscii = content.search(nonAsciiPattern);
|
|
19
|
+
if (firstNonAscii === -1)
|
|
20
|
+
return content.length;
|
|
21
|
+
let bytes = firstNonAscii;
|
|
22
|
+
for (let i = firstNonAscii; i < content.length; i++) {
|
|
23
|
+
const code = content.charCodeAt(i);
|
|
24
|
+
if (code <= 0x7f) {
|
|
25
|
+
bytes += 1;
|
|
26
|
+
}
|
|
27
|
+
else if (code <= 0x7ff) {
|
|
28
|
+
bytes += 2;
|
|
29
|
+
}
|
|
30
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < content.length) {
|
|
31
|
+
const next = content.charCodeAt(i + 1);
|
|
32
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
33
|
+
bytes += 4;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
bytes += 3;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
bytes += 3;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
function splitLinesForCounting(content) {
|
|
47
|
+
if (content.length === 0)
|
|
48
|
+
return [];
|
|
49
|
+
const lines = content.split("\n");
|
|
50
|
+
if (content.endsWith("\n"))
|
|
51
|
+
lines.pop();
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
function replaceUnpairedSurrogates(content) {
|
|
55
|
+
let output = "";
|
|
56
|
+
for (let i = 0; i < content.length; i++) {
|
|
57
|
+
const code = content.charCodeAt(i);
|
|
58
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
59
|
+
if (i + 1 < content.length) {
|
|
60
|
+
const next = content.charCodeAt(i + 1);
|
|
61
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
62
|
+
output += content[i] + content[i + 1];
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
output += "�";
|
|
68
|
+
}
|
|
69
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
70
|
+
output += "�";
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
output += content[i];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return output;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Format bytes as human-readable size.
|
|
80
|
+
*/
|
|
81
|
+
export function formatSize(bytes) {
|
|
82
|
+
if (bytes < 1024) {
|
|
83
|
+
return `${bytes}B`;
|
|
84
|
+
}
|
|
85
|
+
else if (bytes < 1024 * 1024) {
|
|
86
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Truncate content from the head (keep first N lines/bytes).
|
|
94
|
+
* Suitable for file reads where you want to see the beginning.
|
|
95
|
+
*
|
|
96
|
+
* Never returns partial lines. If first line exceeds byte limit,
|
|
97
|
+
* returns empty content with firstLineExceedsLimit=true.
|
|
98
|
+
*/
|
|
99
|
+
export function truncateHead(content, options = {}) {
|
|
100
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
101
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
102
|
+
const totalBytes = utf8ByteLength(content);
|
|
103
|
+
const lines = splitLinesForCounting(content);
|
|
104
|
+
const totalLines = lines.length;
|
|
105
|
+
// Check if no truncation needed
|
|
106
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
107
|
+
return {
|
|
108
|
+
content,
|
|
109
|
+
truncated: false,
|
|
110
|
+
truncatedBy: null,
|
|
111
|
+
totalLines,
|
|
112
|
+
totalBytes,
|
|
113
|
+
outputLines: totalLines,
|
|
114
|
+
outputBytes: totalBytes,
|
|
115
|
+
lastLinePartial: false,
|
|
116
|
+
firstLineExceedsLimit: false,
|
|
117
|
+
maxLines,
|
|
118
|
+
maxBytes,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Check if first line alone exceeds byte limit
|
|
122
|
+
const firstLineBytes = utf8ByteLength(lines[0]);
|
|
123
|
+
if (firstLineBytes > maxBytes) {
|
|
124
|
+
return {
|
|
125
|
+
content: "",
|
|
126
|
+
truncated: true,
|
|
127
|
+
truncatedBy: "bytes",
|
|
128
|
+
totalLines,
|
|
129
|
+
totalBytes,
|
|
130
|
+
outputLines: 0,
|
|
131
|
+
outputBytes: 0,
|
|
132
|
+
lastLinePartial: false,
|
|
133
|
+
firstLineExceedsLimit: true,
|
|
134
|
+
maxLines,
|
|
135
|
+
maxBytes,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Collect complete lines that fit
|
|
139
|
+
const outputLinesArr = [];
|
|
140
|
+
let outputBytesCount = 0;
|
|
141
|
+
let truncatedBy = "lines";
|
|
142
|
+
for (let i = 0; i < lines.length && i < maxLines; i++) {
|
|
143
|
+
const line = lines[i];
|
|
144
|
+
const lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline
|
|
145
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
146
|
+
truncatedBy = "bytes";
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
outputLinesArr.push(line);
|
|
150
|
+
outputBytesCount += lineBytes;
|
|
151
|
+
}
|
|
152
|
+
// Without a byte break, only omitted lines prove the line limit was reached; otherwise a trailing newline exceeded bytes.
|
|
153
|
+
if (truncatedBy !== "bytes")
|
|
154
|
+
truncatedBy = outputLinesArr.length < totalLines ? "lines" : "bytes";
|
|
155
|
+
const outputContent = outputLinesArr.join("\n");
|
|
156
|
+
const finalOutputBytes = utf8ByteLength(outputContent);
|
|
157
|
+
return {
|
|
158
|
+
content: outputContent,
|
|
159
|
+
truncated: true,
|
|
160
|
+
truncatedBy,
|
|
161
|
+
totalLines,
|
|
162
|
+
totalBytes,
|
|
163
|
+
outputLines: outputLinesArr.length,
|
|
164
|
+
outputBytes: finalOutputBytes,
|
|
165
|
+
lastLinePartial: false,
|
|
166
|
+
firstLineExceedsLimit: false,
|
|
167
|
+
maxLines,
|
|
168
|
+
maxBytes,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Truncate content from the tail (keep last N lines/bytes).
|
|
173
|
+
* Suitable for bash output where you want to see the end (errors, final results).
|
|
174
|
+
*
|
|
175
|
+
* May return partial first line if the last line of original content exceeds byte limit.
|
|
176
|
+
*/
|
|
177
|
+
export function truncateTail(content, options = {}) {
|
|
178
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
179
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
180
|
+
const totalBytes = utf8ByteLength(content);
|
|
181
|
+
const lines = splitLinesForCounting(content);
|
|
182
|
+
const totalLines = lines.length;
|
|
183
|
+
// Check if no truncation needed
|
|
184
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
185
|
+
return {
|
|
186
|
+
content,
|
|
187
|
+
truncated: false,
|
|
188
|
+
truncatedBy: null,
|
|
189
|
+
totalLines,
|
|
190
|
+
totalBytes,
|
|
191
|
+
outputLines: totalLines,
|
|
192
|
+
outputBytes: totalBytes,
|
|
193
|
+
lastLinePartial: false,
|
|
194
|
+
firstLineExceedsLimit: false,
|
|
195
|
+
maxLines,
|
|
196
|
+
maxBytes,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Work backwards from the end
|
|
200
|
+
const outputLinesArr = [];
|
|
201
|
+
let outputBytesCount = 0;
|
|
202
|
+
let truncatedBy = "lines";
|
|
203
|
+
let lastLinePartial = false;
|
|
204
|
+
for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
|
|
205
|
+
const line = lines[i];
|
|
206
|
+
const lineBytes = utf8ByteLength(line) + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
|
|
207
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
208
|
+
truncatedBy = "bytes";
|
|
209
|
+
// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
|
|
210
|
+
// take the end of the line (partial)
|
|
211
|
+
if (outputLinesArr.length === 0) {
|
|
212
|
+
const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
|
|
213
|
+
outputLinesArr.unshift(truncatedLine);
|
|
214
|
+
outputBytesCount = utf8ByteLength(truncatedLine);
|
|
215
|
+
lastLinePartial = true;
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
outputLinesArr.unshift(line);
|
|
220
|
+
outputBytesCount += lineBytes;
|
|
221
|
+
}
|
|
222
|
+
// Without a byte break, only omitted lines prove the line limit was reached; otherwise a trailing newline exceeded bytes.
|
|
223
|
+
if (truncatedBy !== "bytes")
|
|
224
|
+
truncatedBy = outputLinesArr.length < totalLines ? "lines" : "bytes";
|
|
225
|
+
const outputContent = outputLinesArr.join("\n");
|
|
226
|
+
const finalOutputBytes = utf8ByteLength(outputContent);
|
|
227
|
+
return {
|
|
228
|
+
content: outputContent,
|
|
229
|
+
truncated: true,
|
|
230
|
+
truncatedBy,
|
|
231
|
+
totalLines,
|
|
232
|
+
totalBytes,
|
|
233
|
+
outputLines: outputLinesArr.length,
|
|
234
|
+
outputBytes: finalOutputBytes,
|
|
235
|
+
lastLinePartial,
|
|
236
|
+
firstLineExceedsLimit: false,
|
|
237
|
+
maxLines,
|
|
238
|
+
maxBytes,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Truncate a string to fit within a byte limit (from the end).
|
|
243
|
+
* Handles multi-byte UTF-8 characters correctly.
|
|
244
|
+
*/
|
|
245
|
+
function truncateStringToBytesFromEnd(str, maxBytes) {
|
|
246
|
+
if (maxBytes <= 0)
|
|
247
|
+
return "";
|
|
248
|
+
let outputBytes = 0;
|
|
249
|
+
let start = str.length;
|
|
250
|
+
let needsReplacement = false;
|
|
251
|
+
for (let i = str.length; i > 0;) {
|
|
252
|
+
let characterStart = i - 1;
|
|
253
|
+
const code = str.charCodeAt(characterStart);
|
|
254
|
+
let characterBytes;
|
|
255
|
+
let unpairedSurrogate = false;
|
|
256
|
+
if (code >= 0xdc00 && code <= 0xdfff && characterStart > 0) {
|
|
257
|
+
const previous = str.charCodeAt(characterStart - 1);
|
|
258
|
+
if (previous >= 0xd800 && previous <= 0xdbff) {
|
|
259
|
+
characterStart--;
|
|
260
|
+
characterBytes = 4;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
characterBytes = 3;
|
|
264
|
+
unpairedSurrogate = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (code >= 0xd800 && code <= 0xdfff) {
|
|
268
|
+
characterBytes = 3;
|
|
269
|
+
unpairedSurrogate = true;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
characterBytes = code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3;
|
|
273
|
+
}
|
|
274
|
+
if (outputBytes + characterBytes > maxBytes)
|
|
275
|
+
break;
|
|
276
|
+
outputBytes += characterBytes;
|
|
277
|
+
start = characterStart;
|
|
278
|
+
needsReplacement ||= unpairedSurrogate;
|
|
279
|
+
i = characterStart;
|
|
280
|
+
}
|
|
281
|
+
const output = str.slice(start);
|
|
282
|
+
return needsReplacement ? replaceUnpairedSurrogates(output) : output;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Truncate a single line to max characters, adding [truncated] suffix.
|
|
286
|
+
* Used for grep match lines.
|
|
287
|
+
*/
|
|
288
|
+
export function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
|
|
289
|
+
if (line.length <= maxChars) {
|
|
290
|
+
return { text: line, wasTruncated: false };
|
|
291
|
+
}
|
|
292
|
+
return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=truncate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../../src/env/utils/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 (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\nexport const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line\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\nfunction replaceUnpairedSurrogates(content: string): string {\n\tlet output = \"\";\n\tfor (let i = 0; i < content.length; i++) {\n\t\tconst code = content.charCodeAt(i);\n\t\tif (code >= 0xd800 && code <= 0xdbff) {\n\t\t\tif (i + 1 < content.length) {\n\t\t\t\tconst next = content.charCodeAt(i + 1);\n\t\t\t\tif (next >= 0xdc00 && next <= 0xdfff) {\n\t\t\t\t\toutput += content[i] + content[i + 1];\n\t\t\t\t\ti++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\toutput += \"�\";\n\t\t} else if (code >= 0xdc00 && code <= 0xdfff) {\n\t\t\toutput += \"�\";\n\t\t} else {\n\t\t\toutput += content[i];\n\t\t}\n\t}\n\treturn output;\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\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(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// Work backwards from the end\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\tlet lastLinePartial = false;\n\n\tfor (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = utf8ByteLength(line) + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\t// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n\t\t\t// take the end of the line (partial)\n\t\t\tif (outputLinesArr.length === 0) {\n\t\t\t\tconst truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n\t\t\t\toutputLinesArr.unshift(truncatedLine);\n\t\t\t\toutputBytesCount = utf8ByteLength(truncatedLine);\n\t\t\t\tlastLinePartial = true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.unshift(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,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n\tif (maxBytes <= 0) return \"\";\n\n\tlet outputBytes = 0;\n\tlet start = str.length;\n\tlet needsReplacement = false;\n\tfor (let i = str.length; i > 0; ) {\n\t\tlet characterStart = i - 1;\n\t\tconst code = str.charCodeAt(characterStart);\n\t\tlet characterBytes: number;\n\t\tlet unpairedSurrogate = false;\n\t\tif (code >= 0xdc00 && code <= 0xdfff && characterStart > 0) {\n\t\t\tconst previous = str.charCodeAt(characterStart - 1);\n\t\t\tif (previous >= 0xd800 && previous <= 0xdbff) {\n\t\t\t\tcharacterStart--;\n\t\t\t\tcharacterBytes = 4;\n\t\t\t} else {\n\t\t\t\tcharacterBytes = 3;\n\t\t\t\tunpairedSurrogate = true;\n\t\t\t}\n\t\t} else if (code >= 0xd800 && code <= 0xdfff) {\n\t\t\tcharacterBytes = 3;\n\t\t\tunpairedSurrogate = true;\n\t\t} else {\n\t\t\tcharacterBytes = code <= 0x7f ? 1 : code <= 0x7ff ? 2 : 3;\n\t\t}\n\t\tif (outputBytes + characterBytes > maxBytes) break;\n\t\toutputBytes += characterBytes;\n\t\tstart = characterStart;\n\t\tneedsReplacement ||= unpairedSurrogate;\n\t\ti = characterStart;\n\t}\n\n\tconst output = str.slice(start);\n\treturn needsReplacement ? replaceUnpairedSurrogates(output) : output;\n}\n\n/**\n * Truncate a single line to max characters, adding [truncated] suffix.\n * Used for grep match lines.\n */\nexport function truncateLine(\n\tline: string,\n\tmaxChars: number = GREP_MAX_LINE_LENGTH,\n): { text: string; wasTruncated: boolean } {\n\tif (line.length <= maxChars) {\n\t\treturn { text: line, wasTruncated: false };\n\t}\n\treturn { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };\n}\n"]}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConversationId } from "./types.ts";
|
|
2
|
+
/** A transaction read a table after its first table write. Read every required row before writing. */
|
|
3
|
+
export declare class ReadAfterWrite extends Error {
|
|
4
|
+
constructor(method: string);
|
|
5
|
+
}
|
|
6
|
+
/** Storage rejected a batch before any durable effect; the owning Session may continue safely. */
|
|
7
|
+
export declare class StorageRejected extends Error {
|
|
8
|
+
constructor(message: string, options?: ErrorOptions);
|
|
9
|
+
}
|
|
10
|
+
/** A submission reached a busy conversation and was not admitted. */
|
|
11
|
+
export declare class ConversationBusy extends Error {
|
|
12
|
+
readonly conversationId: ConversationId;
|
|
13
|
+
constructor(conversationId: ConversationId);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,sGAAsG;AACtG,qBAAa,cAAe,SAAQ,KAAK;IACxC,YAAY,MAAM,EAAE,MAAM,EAGzB;CACD;AAED,kGAAkG;AAClG,qBAAa,eAAgB,SAAQ,KAAK;IACzC,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAGlD;CACD;AAED,qEAAqE;AACrE,qBAAa,gBAAiB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IAExC,YAAY,cAAc,EAAE,cAAc,EAIzC;CACD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** A transaction read a table after its first table write. Read every required row before writing. */
|
|
2
|
+
export class ReadAfterWrite extends Error {
|
|
3
|
+
constructor(method) {
|
|
4
|
+
super(`Tx.${method}() cannot read tables after the first table write`);
|
|
5
|
+
this.name = "ReadAfterWrite";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Storage rejected a batch before any durable effect; the owning Session may continue safely. */
|
|
9
|
+
export class StorageRejected extends Error {
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "StorageRejected";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** A submission reached a busy conversation and was not admitted. */
|
|
16
|
+
export class ConversationBusy extends Error {
|
|
17
|
+
conversationId;
|
|
18
|
+
constructor(conversationId) {
|
|
19
|
+
super(`Conversation ${conversationId} is busy`);
|
|
20
|
+
this.name = "ConversationBusy";
|
|
21
|
+
this.conversationId = conversationId;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,sGAAsG;AACtG,MAAM,OAAO,cAAe,SAAQ,KAAK;IACxC,YAAY,MAAc;QACzB,KAAK,CAAC,MAAM,MAAM,mDAAmD,CAAC,CAAC;QACvE,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC9B,CAAC;CACD;AAED,kGAAkG;AAClG,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACzC,YAAY,OAAe,EAAE,OAAsB;QAClD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAC/B,CAAC;CACD;AAED,qEAAqE;AACrE,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACjC,cAAc,CAAiB;IAExC,YAAY,cAA8B;QACzC,KAAK,CAAC,gBAAgB,cAAc,UAAU,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACtC,CAAC;CACD","sourcesContent":["import type { ConversationId } from \"./types.ts\";\n\n/** A transaction read a table after its first table write. Read every required row before writing. */\nexport class ReadAfterWrite extends Error {\n\tconstructor(method: string) {\n\t\tsuper(`Tx.${method}() cannot read tables after the first table write`);\n\t\tthis.name = \"ReadAfterWrite\";\n\t}\n}\n\n/** Storage rejected a batch before any durable effect; the owning Session may continue safely. */\nexport class StorageRejected extends Error {\n\tconstructor(message: string, options?: ErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.name = \"StorageRejected\";\n\t}\n}\n\n/** A submission reached a busy conversation and was not admitted. */\nexport class ConversationBusy extends Error {\n\treadonly conversationId: ConversationId;\n\n\tconstructor(conversationId: ConversationId) {\n\t\tsuper(`Conversation ${conversationId} is busy`);\n\t\tthis.name = \"ConversationBusy\";\n\t\tthis.conversationId = conversationId;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ConversationRetryPolicy, ConversationStreamOptions } from "./types.ts";
|
|
3
|
+
/** Durable per-conversation model, thinking level, request options, and desired tool loadout. */
|
|
4
|
+
export type ConversationConfigState = {
|
|
5
|
+
model?: {
|
|
6
|
+
provider: string;
|
|
7
|
+
modelId: string;
|
|
8
|
+
};
|
|
9
|
+
thinkingLevel: ModelThinkingLevel;
|
|
10
|
+
/** Desired tool names in offered order; names may be unregistered in the current process. */
|
|
11
|
+
activeTools: string[];
|
|
12
|
+
/** Forwarded to every generation request. */
|
|
13
|
+
streamOptions?: ConversationStreamOptions;
|
|
14
|
+
/** Durable generation attempt retries; absent uses `DEFAULT_RETRY_POLICY`. */
|
|
15
|
+
retry?: ConversationRetryPolicy;
|
|
16
|
+
};
|
|
17
|
+
export declare const DEFAULT_RETRY_POLICY: ConversationRetryPolicy;
|
|
18
|
+
/** Built-in configuration document; rewindable so forks start from the configuration at their fork entry. */
|
|
19
|
+
export declare const ConversationConfig: import("../types.ts").RewindableConversationDocToken<ConversationConfigState>;
|
|
20
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/harness/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAErF,iGAAiG;AACjG,MAAM,MAAM,uBAAuB,GAAG;IACrC,KAAK,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,aAAa,EAAE,kBAAkB,CAAC;IAClC,6FAA6F;IAC7F,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,6CAA6C;IAC7C,aAAa,CAAC,EAAE,yBAAyB,CAAC;IAC1C,8EAA8E;IAC9E,KAAK,CAAC,EAAE,uBAAuB,CAAC;CAChC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,uBAKlC,CAAC;AAEF,6GAA6G;AAC7G,eAAO,MAAM,kBAAkB,+EAQ7B,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineDoc } from "../documents.js";
|
|
2
|
+
export const DEFAULT_RETRY_POLICY = {
|
|
3
|
+
enabled: true,
|
|
4
|
+
maxRetries: 3,
|
|
5
|
+
baseDelayMs: 2000,
|
|
6
|
+
maxAgentDelayMs: 60000,
|
|
7
|
+
};
|
|
8
|
+
/** Built-in configuration document; rewindable so forks start from the configuration at their fork entry. */
|
|
9
|
+
export const ConversationConfig = defineDoc({
|
|
10
|
+
kind: "pi.conversation.config",
|
|
11
|
+
version: 1,
|
|
12
|
+
scope: "conversation",
|
|
13
|
+
history: "rewindable",
|
|
14
|
+
fork: "asOf",
|
|
15
|
+
initial: () => ({ thinkingLevel: "off", activeTools: [] }),
|
|
16
|
+
checkpointWhen: () => true,
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/harness/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAe5C,MAAM,CAAC,MAAM,oBAAoB,GAA4B;IAC5D,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,IAAI;IACjB,eAAe,EAAE,KAAK;CACtB,CAAC;AAEF,6GAA6G;AAC7G,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAA0B;IACpE,IAAI,EAAE,wBAAwB;IAC9B,OAAO,EAAE,CAAC;IACV,KAAK,EAAE,cAAc;IACrB,OAAO,EAAE,YAAY;IACrB,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IAC1D,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI;CAC1B,CAAC,CAAC","sourcesContent":["import type { ModelThinkingLevel } from \"@earendil-works/pi-ai\";\nimport { defineDoc } from \"../documents.ts\";\nimport type { ConversationRetryPolicy, ConversationStreamOptions } from \"./types.ts\";\n\n/** Durable per-conversation model, thinking level, request options, and desired tool loadout. */\nexport type ConversationConfigState = {\n\tmodel?: { provider: string; modelId: string };\n\tthinkingLevel: ModelThinkingLevel;\n\t/** Desired tool names in offered order; names may be unregistered in the current process. */\n\tactiveTools: string[];\n\t/** Forwarded to every generation request. */\n\tstreamOptions?: ConversationStreamOptions;\n\t/** Durable generation attempt retries; absent uses `DEFAULT_RETRY_POLICY`. */\n\tretry?: ConversationRetryPolicy;\n};\n\nexport const DEFAULT_RETRY_POLICY: ConversationRetryPolicy = {\n\tenabled: true,\n\tmaxRetries: 3,\n\tbaseDelayMs: 2000,\n\tmaxAgentDelayMs: 60000,\n};\n\n/** Built-in configuration document; rewindable so forks start from the configuration at their fork entry. */\nexport const ConversationConfig = defineDoc<ConversationConfigState>({\n\tkind: \"pi.conversation.config\",\n\tversion: 1,\n\tscope: \"conversation\",\n\thistory: \"rewindable\",\n\tfork: \"asOf\",\n\tinitial: () => ({ thinkingLevel: \"off\", activeTools: [] }),\n\tcheckpointWhen: () => true,\n});\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/chord";
|
|
2
|
+
import type { SessionImpl } from "../session/session.ts";
|
|
3
|
+
import type { ConversationId, EntryId, EntryRecord, Storage } from "../types.ts";
|
|
4
|
+
import type { ContextView } from "./types.ts";
|
|
5
|
+
/** Head marker and newest visible entry that fix one committed context range. */
|
|
6
|
+
export type ContextBounds = {
|
|
7
|
+
readonly head: (EntryRecord & {
|
|
8
|
+
readonly head: EntryId;
|
|
9
|
+
}) | undefined;
|
|
10
|
+
readonly tail: EntryId;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Capture the bounds of the current context, or of the context cut off at the visible entry `at`, with two O(1)
|
|
14
|
+
* reads. Run this on the Session line; entries at or below the tail are immutable, so `deriveContext()` can then scan
|
|
15
|
+
* them off the line.
|
|
16
|
+
*/
|
|
17
|
+
export declare function captureContextBounds(storage: Storage, conversationId: ConversationId, context: Context, at?: EntryId): Promise<ContextBounds | undefined>;
|
|
18
|
+
/** Committed context of one conversation: bounds captured on the Session line, entries derived off it. */
|
|
19
|
+
export declare function readContext(session: SessionImpl, storage: Storage, conversationId: ConversationId, context: Context, at?: EntryId): Promise<ContextView>;
|
|
20
|
+
/**
|
|
21
|
+
* Derive the active transcript and model context of one conversation within captured bounds.
|
|
22
|
+
*
|
|
23
|
+
* H = newest visible head marker; the range runs from `H.head` (or transcript start) through the tail. Per target,
|
|
24
|
+
* the newest edit in the range wins. Context entries are H followed by the range's non-head entries.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deriveContext(storage: Storage, conversationId: ConversationId, bounds: ContextBounds | undefined, context: Context): Promise<ContextView>;
|
|
27
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/harness/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAe,cAAc,EAAU,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C,iFAAiF;AACjF,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,SAAS,CAAC;IACtE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,cAAc,EAC9B,OAAO,EAAE,OAAO,EAChB,EAAE,CAAC,EAAE,OAAO,GACV,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAYpC;AAED,0GAA0G;AAC1G,wBAAsB,WAAW,CAChC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,cAAc,EAC9B,OAAO,EAAE,OAAO,EAChB,EAAE,CAAC,EAAE,OAAO,GACV,OAAO,CAAC,WAAW,CAAC,CAGtB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAClC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,cAAc,EAC9B,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,WAAW,CAAC,CAkCtB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const SCAN_PAGE_SIZE = 256;
|
|
2
|
+
const EXCLUDED_STOP_REASONS = new Set(["aborted", "error", "deferred"]);
|
|
3
|
+
const MISSING_RESULT_TEXT = "Tool result unavailable: history ends before this call completed.";
|
|
4
|
+
/**
|
|
5
|
+
* Capture the bounds of the current context, or of the context cut off at the visible entry `at`, with two O(1)
|
|
6
|
+
* reads. Run this on the Session line; entries at or below the tail are immutable, so `deriveContext()` can then scan
|
|
7
|
+
* them off the line.
|
|
8
|
+
*/
|
|
9
|
+
export async function captureContextBounds(storage, conversationId, context, at) {
|
|
10
|
+
let tail;
|
|
11
|
+
if (at === undefined) {
|
|
12
|
+
tail = (await storage.scanEntries({ conversationId }, 1, undefined, context)).items[0]?.id;
|
|
13
|
+
if (tail === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
if ((await storage.entry(conversationId, at, context)) === undefined) {
|
|
18
|
+
throw new Error(`Entry ${at} is not visible from conversation ${conversationId}`);
|
|
19
|
+
}
|
|
20
|
+
tail = at;
|
|
21
|
+
}
|
|
22
|
+
return { head: await storage.findLatestHeadMarker(conversationId, tail, context), tail };
|
|
23
|
+
}
|
|
24
|
+
/** Committed context of one conversation: bounds captured on the Session line, entries derived off it. */
|
|
25
|
+
export async function readContext(session, storage, conversationId, context, at) {
|
|
26
|
+
const bounds = await session.readOnLine(() => captureContextBounds(storage, conversationId, context, at));
|
|
27
|
+
return deriveContext(storage, conversationId, bounds, context);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Derive the active transcript and model context of one conversation within captured bounds.
|
|
31
|
+
*
|
|
32
|
+
* H = newest visible head marker; the range runs from `H.head` (or transcript start) through the tail. Per target,
|
|
33
|
+
* the newest edit in the range wins. Context entries are H followed by the range's non-head entries.
|
|
34
|
+
*/
|
|
35
|
+
export async function deriveContext(storage, conversationId, bounds, context) {
|
|
36
|
+
if (bounds === undefined)
|
|
37
|
+
return { head: undefined, entries: [], messages: [] };
|
|
38
|
+
const head = bounds.head;
|
|
39
|
+
const range = [];
|
|
40
|
+
let cursor;
|
|
41
|
+
do {
|
|
42
|
+
const page = await storage.scanEntries(head === undefined
|
|
43
|
+
? { conversationId, maxEntryId: bounds.tail }
|
|
44
|
+
: { conversationId, minEntryId: head.head, maxEntryId: bounds.tail }, SCAN_PAGE_SIZE, cursor, context);
|
|
45
|
+
range.push(...page.items);
|
|
46
|
+
cursor = page.next;
|
|
47
|
+
} while (cursor !== undefined);
|
|
48
|
+
range.reverse();
|
|
49
|
+
const edits = new Map();
|
|
50
|
+
for (const entry of range)
|
|
51
|
+
for (const edit of entry.edits ?? [])
|
|
52
|
+
edits.set(edit.target, edit);
|
|
53
|
+
const entries = head === undefined ? range : [head, ...range.filter((entry) => entry.head === undefined)];
|
|
54
|
+
const messages = [];
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const edit = edits.get(entry.id);
|
|
57
|
+
if (edit?.action === "omit")
|
|
58
|
+
continue;
|
|
59
|
+
const contributed = edit?.action === "replace" ? edit.messages : (entry.model ?? []);
|
|
60
|
+
for (const message of contributed) {
|
|
61
|
+
if (message.role === "assistant" && EXCLUDED_STOP_REASONS.has(message.stopReason))
|
|
62
|
+
continue;
|
|
63
|
+
messages.push(message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { head, entries, messages: orderToolResults(messages) };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Place each assistant's tool results directly after it in call order. Results are taken from the messages before
|
|
70
|
+
* the next assistant; a missing result is synthesized and unmatched results are dropped.
|
|
71
|
+
*/
|
|
72
|
+
function orderToolResults(messages) {
|
|
73
|
+
const ordered = [];
|
|
74
|
+
for (let index = 0; index < messages.length; index++) {
|
|
75
|
+
const message = messages[index];
|
|
76
|
+
if (message.role === "toolResult")
|
|
77
|
+
continue;
|
|
78
|
+
ordered.push(message);
|
|
79
|
+
if (message.role !== "assistant")
|
|
80
|
+
continue;
|
|
81
|
+
const calls = message.content.filter((content) => content.type === "toolCall");
|
|
82
|
+
if (calls.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
const results = new Map();
|
|
85
|
+
for (let next = index + 1; next < messages.length && messages[next].role !== "assistant"; next++) {
|
|
86
|
+
const candidate = messages[next];
|
|
87
|
+
if (candidate.role === "toolResult" && !results.has(candidate.toolCallId)) {
|
|
88
|
+
results.set(candidate.toolCallId, next);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const call of calls) {
|
|
92
|
+
const resultIndex = results.get(call.id);
|
|
93
|
+
ordered.push(resultIndex === undefined ? missingResult(call, message.timestamp) : messages[resultIndex]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return ordered;
|
|
97
|
+
}
|
|
98
|
+
function missingResult(call, timestamp) {
|
|
99
|
+
return {
|
|
100
|
+
role: "toolResult",
|
|
101
|
+
toolCallId: call.id,
|
|
102
|
+
toolName: call.name,
|
|
103
|
+
content: [{ type: "text", text: MISSING_RESULT_TEXT }],
|
|
104
|
+
isError: true,
|
|
105
|
+
details: { reason: "missing_result" },
|
|
106
|
+
timestamp,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=context.js.map
|