@earendil-works/pi-agent-core 0.85.1 → 0.86.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 +16 -6
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +64 -22
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent.d.ts +5 -3
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -6
- package/dist/agent.js.map +1 -1
- package/dist/harness/config.d.ts +1 -1
- package/dist/harness/config.d.ts.map +1 -1
- package/dist/harness/config.js +10 -2
- package/dist/harness/config.js.map +1 -1
- package/dist/harness/env/nodejs.d.ts +2 -1
- package/dist/harness/env/nodejs.d.ts.map +1 -1
- package/dist/harness/env/nodejs.js +98 -27
- package/dist/harness/env/nodejs.js.map +1 -1
- package/dist/harness/execution/tools.d.ts +1 -1
- package/dist/harness/execution/tools.d.ts.map +1 -1
- package/dist/harness/execution/tools.js +0 -2
- package/dist/harness/execution/tools.js.map +1 -1
- package/dist/harness/messages.d.ts.map +1 -1
- package/dist/harness/messages.js +1 -0
- package/dist/harness/messages.js.map +1 -1
- package/dist/harness/pico3/bash.d.ts +10 -0
- package/dist/harness/pico3/bash.d.ts.map +1 -0
- package/dist/harness/pico3/bash.js +25 -0
- package/dist/harness/pico3/bash.js.map +1 -0
- package/dist/harness/pico3/bounded.d.ts +21 -0
- package/dist/harness/pico3/bounded.d.ts.map +1 -0
- package/dist/harness/pico3/bounded.js +98 -0
- package/dist/harness/pico3/bounded.js.map +1 -0
- package/dist/harness/pico3/chord.d.ts +41 -0
- package/dist/harness/pico3/chord.d.ts.map +1 -0
- package/dist/harness/pico3/chord.js +130 -0
- package/dist/harness/pico3/chord.js.map +1 -0
- package/dist/harness/pico3/context.d.ts +16 -0
- package/dist/harness/pico3/context.d.ts.map +1 -0
- package/dist/harness/pico3/context.js +88 -0
- package/dist/harness/pico3/context.js.map +1 -0
- package/dist/harness/pico3/harness.d.ts +197 -0
- package/dist/harness/pico3/harness.d.ts.map +1 -0
- package/dist/harness/pico3/harness.js +654 -0
- package/dist/harness/pico3/harness.js.map +1 -0
- package/dist/harness/pico3/hooks.d.ts +10 -0
- package/dist/harness/pico3/hooks.d.ts.map +1 -0
- package/dist/harness/pico3/hooks.js +33 -0
- package/dist/harness/pico3/hooks.js.map +1 -0
- package/dist/harness/pico3/index.d.ts +19 -0
- package/dist/harness/pico3/index.d.ts.map +1 -0
- package/dist/harness/pico3/index.js +15 -0
- package/dist/harness/pico3/index.js.map +1 -0
- package/dist/harness/pico3/jsonl.d.ts +58 -0
- package/dist/harness/pico3/jsonl.d.ts.map +1 -0
- package/dist/harness/pico3/jsonl.js +325 -0
- package/dist/harness/pico3/jsonl.js.map +1 -0
- package/dist/harness/pico3/kinds/collapse.d.ts +49 -0
- package/dist/harness/pico3/kinds/collapse.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/collapse.js +191 -0
- package/dist/harness/pico3/kinds/collapse.js.map +1 -0
- package/dist/harness/pico3/kinds/entries.d.ts +78 -0
- package/dist/harness/pico3/kinds/entries.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/entries.js +13 -0
- package/dist/harness/pico3/kinds/entries.js.map +1 -0
- package/dist/harness/pico3/kinds/frames.d.ts +9 -0
- package/dist/harness/pico3/kinds/frames.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/frames.js +75 -0
- package/dist/harness/pico3/kinds/frames.js.map +1 -0
- package/dist/harness/pico3/kinds/generation.d.ts +94 -0
- package/dist/harness/pico3/kinds/generation.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/generation.js +504 -0
- package/dist/harness/pico3/kinds/generation.js.map +1 -0
- package/dist/harness/pico3/kinds/job.d.ts +44 -0
- package/dist/harness/pico3/kinds/job.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/job.js +143 -0
- package/dist/harness/pico3/kinds/job.js.map +1 -0
- package/dist/harness/pico3/kinds/plugin.d.ts +15 -0
- package/dist/harness/pico3/kinds/plugin.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/plugin.js +35 -0
- package/dist/harness/pico3/kinds/plugin.js.map +1 -0
- package/dist/harness/pico3/kinds/post-tools.d.ts +22 -0
- package/dist/harness/pico3/kinds/post-tools.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/post-tools.js +145 -0
- package/dist/harness/pico3/kinds/post-tools.js.map +1 -0
- package/dist/harness/pico3/kinds/task-api.d.ts +3 -0
- package/dist/harness/pico3/kinds/task-api.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/task-api.js +45 -0
- package/dist/harness/pico3/kinds/task-api.js.map +1 -0
- package/dist/harness/pico3/kinds/tool.d.ts +43 -0
- package/dist/harness/pico3/kinds/tool.d.ts.map +1 -0
- package/dist/harness/pico3/kinds/tool.js +373 -0
- package/dist/harness/pico3/kinds/tool.js.map +1 -0
- package/dist/harness/pico3/membrane.d.ts +23 -0
- package/dist/harness/pico3/membrane.d.ts.map +1 -0
- package/dist/harness/pico3/membrane.js +141 -0
- package/dist/harness/pico3/membrane.js.map +1 -0
- package/dist/harness/pico3/memory.d.ts +53 -0
- package/dist/harness/pico3/memory.d.ts.map +1 -0
- package/dist/harness/pico3/memory.js +264 -0
- package/dist/harness/pico3/memory.js.map +1 -0
- package/dist/harness/pico3/scheduler.d.ts +46 -0
- package/dist/harness/pico3/scheduler.d.ts.map +1 -0
- package/dist/harness/pico3/scheduler.js +438 -0
- package/dist/harness/pico3/scheduler.js.map +1 -0
- package/dist/harness/pico3/session.d.ts +263 -0
- package/dist/harness/pico3/session.d.ts.map +1 -0
- package/dist/harness/pico3/session.js +1347 -0
- package/dist/harness/pico3/session.js.map +1 -0
- package/dist/harness/pico3/system.d.ts +126 -0
- package/dist/harness/pico3/system.d.ts.map +1 -0
- package/dist/harness/pico3/system.js +244 -0
- package/dist/harness/pico3/system.js.map +1 -0
- package/dist/harness/pico3/types.d.ts +973 -0
- package/dist/harness/pico3/types.d.ts.map +1 -0
- package/dist/harness/pico3/types.js +96 -0
- package/dist/harness/pico3/types.js.map +1 -0
- package/dist/harness/pico3/view.d.ts +34 -0
- package/dist/harness/pico3/view.d.ts.map +1 -0
- package/dist/harness/pico3/view.js +417 -0
- package/dist/harness/pico3/view.js.map +1 -0
- package/dist/harness/runtime/drive/boundary.d.ts.map +1 -1
- package/dist/harness/runtime/drive/boundary.js +6 -3
- package/dist/harness/runtime/drive/boundary.js.map +1 -1
- package/dist/harness/runtime/drive/response.d.ts.map +1 -1
- package/dist/harness/runtime/drive/response.js +4 -4
- package/dist/harness/runtime/drive/response.js.map +1 -1
- package/dist/harness/runtime/drive/retry.d.ts +2 -2
- package/dist/harness/runtime/drive/retry.d.ts.map +1 -1
- package/dist/harness/runtime/drive/retry.js +3 -6
- package/dist/harness/runtime/drive/retry.js.map +1 -1
- package/dist/harness/runtime/drive/structural.d.ts.map +1 -1
- package/dist/harness/runtime/drive/structural.js +5 -5
- package/dist/harness/runtime/drive/structural.js.map +1 -1
- package/dist/harness/runtime/drive/tool-placement.d.ts.map +1 -1
- package/dist/harness/runtime/drive/tool-placement.js +2 -27
- package/dist/harness/runtime/drive/tool-placement.js.map +1 -1
- package/dist/harness/runtime/drive/tools.d.ts.map +1 -1
- package/dist/harness/runtime/drive/tools.js.map +1 -1
- package/dist/harness/session/fork-policy.d.ts +20 -12
- package/dist/harness/session/fork-policy.d.ts.map +1 -1
- package/dist/harness/session/fork-policy.js +45 -12
- package/dist/harness/session/fork-policy.js.map +1 -1
- package/dist/harness/session/fork.d.ts +0 -2
- package/dist/harness/session/fork.d.ts.map +1 -1
- package/dist/harness/session/fork.js +20 -68
- package/dist/harness/session/fork.js.map +1 -1
- package/dist/harness/session/in-memory-storage-state.d.ts +5 -5
- package/dist/harness/session/in-memory-storage-state.d.ts.map +1 -1
- package/dist/harness/session/in-memory-storage-state.js +99 -39
- package/dist/harness/session/in-memory-storage-state.js.map +1 -1
- package/dist/harness/session/index.d.ts +1 -1
- package/dist/harness/session/index.d.ts.map +1 -1
- package/dist/harness/session/index.js +1 -1
- package/dist/harness/session/index.js.map +1 -1
- package/dist/harness/session/jsonl/fork.d.ts +38 -0
- package/dist/harness/session/jsonl/fork.d.ts.map +1 -0
- package/dist/harness/session/jsonl/fork.js +264 -0
- package/dist/harness/session/jsonl/fork.js.map +1 -0
- package/dist/harness/session/jsonl/io.d.ts +14 -0
- package/dist/harness/session/jsonl/io.d.ts.map +1 -0
- package/dist/harness/session/jsonl/io.js +86 -0
- package/dist/harness/session/jsonl/io.js.map +1 -0
- package/dist/harness/session/jsonl/legacy-v3.d.ts +33 -8
- package/dist/harness/session/jsonl/legacy-v3.d.ts.map +1 -1
- package/dist/harness/session/jsonl/legacy-v3.js +285 -206
- package/dist/harness/session/jsonl/legacy-v3.js.map +1 -1
- package/dist/harness/session/jsonl/repo.d.ts +2 -1
- package/dist/harness/session/jsonl/repo.d.ts.map +1 -1
- package/dist/harness/session/jsonl/repo.js +36 -20
- package/dist/harness/session/jsonl/repo.js.map +1 -1
- package/dist/harness/session/jsonl/storage.d.ts +4 -5
- package/dist/harness/session/jsonl/storage.d.ts.map +1 -1
- package/dist/harness/session/jsonl/storage.js +39 -113
- package/dist/harness/session/jsonl/storage.js.map +1 -1
- package/dist/harness/session/memory.d.ts +3 -5
- package/dist/harness/session/memory.d.ts.map +1 -1
- package/dist/harness/session/memory.js +9 -14
- package/dist/harness/session/memory.js.map +1 -1
- package/dist/harness/session/testing/conformance/session-repo.d.ts +2 -0
- package/dist/harness/session/testing/conformance/session-repo.d.ts.map +1 -1
- package/dist/harness/session/testing/conformance/session-repo.js +220 -1
- package/dist/harness/session/testing/conformance/session-repo.js.map +1 -1
- package/dist/harness/session/testing/index.d.ts +1 -1
- package/dist/harness/session/testing/index.d.ts.map +1 -1
- package/dist/harness/session/testing/index.js +1 -1
- package/dist/harness/session/testing/index.js.map +1 -1
- package/dist/harness/session/types.d.ts +1 -0
- package/dist/harness/session/types.d.ts.map +1 -1
- package/dist/harness/session/types.js.map +1 -1
- package/dist/harness/types.d.ts +14 -0
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/proxy.d.ts +2 -2
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js.map +1 -1
- package/dist/types.d.ts +30 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +10 -6
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded byte collector. It retains a prefix or suffix constrained by both a byte
|
|
3
|
+
* budget and a newline budget, and accounts for every discarded byte/newline.
|
|
4
|
+
*/
|
|
5
|
+
export declare class Bounded {
|
|
6
|
+
private bytes;
|
|
7
|
+
private readonly maxBytes;
|
|
8
|
+
private readonly maxLines;
|
|
9
|
+
private readonly retain;
|
|
10
|
+
droppedBytes: number;
|
|
11
|
+
droppedLines: number;
|
|
12
|
+
total: number;
|
|
13
|
+
constructor(maxBytes: number, maxLines: number, retain: "head" | "tail");
|
|
14
|
+
push(chunk: Uint8Array): void;
|
|
15
|
+
private pushHead;
|
|
16
|
+
private pushTail;
|
|
17
|
+
private drop;
|
|
18
|
+
get dropped(): number;
|
|
19
|
+
text(): string;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=bounded.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounded.d.ts","sourceRoot":"","sources":["../../../src/harness/pico3/bounded.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,OAAO;IACnB,OAAO,CAAC,KAAK,CAAiD;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,YAAY,SAAK;IACjB,YAAY,SAAK;IACjB,KAAK,SAAK;IAEV,YAAY,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAItE;IAED,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAY5B;IAED,OAAO,CAAC,QAAQ;IAqBhB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,IAAI;IAKZ,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,IAAI,MAAM,CAEb;CACD","sourcesContent":["/**\n * Bounded byte collector. It retains a prefix or suffix constrained by both a byte\n * budget and a newline budget, and accounts for every discarded byte/newline.\n */\nexport class Bounded {\n\tprivate bytes: Uint8Array<ArrayBufferLike> = new Uint8Array();\n\tprivate readonly maxBytes: number;\n\tprivate readonly maxLines: number;\n\tprivate readonly retain: \"head\" | \"tail\";\n\tdroppedBytes = 0;\n\tdroppedLines = 0;\n\ttotal = 0;\n\n\tconstructor(maxBytes: number, maxLines: number, retain: \"head\" | \"tail\") {\n\t\tthis.maxBytes = Math.max(0, maxBytes);\n\t\tthis.maxLines = Math.max(0, maxLines);\n\t\tthis.retain = retain;\n\t}\n\n\tpush(chunk: Uint8Array): void {\n\t\tthis.total += chunk.length;\n\t\tif (chunk.length === 0) return;\n\t\tif (this.maxBytes === 0 || this.maxLines === 0) {\n\t\t\tthis.drop(chunk);\n\t\t\treturn;\n\t\t}\n\t\tif (this.retain === \"head\") {\n\t\t\tthis.pushHead(chunk);\n\t\t\treturn;\n\t\t}\n\t\tthis.pushTail(chunk);\n\t}\n\n\tprivate pushHead(chunk: Uint8Array): void {\n\t\tconst remainingBytes = this.maxBytes - this.bytes.length;\n\t\tconst remainingLines = this.maxLines - countNewlines(this.bytes);\n\t\tif (remainingBytes <= 0 || remainingLines <= 0) {\n\t\t\tthis.drop(chunk);\n\t\t\treturn;\n\t\t}\n\t\tlet take = Math.min(chunk.length, remainingBytes);\n\t\tlet lines = 0;\n\t\tfor (let i = 0; i < take; i++) {\n\t\t\tif (chunk[i] !== 0x0a) continue;\n\t\t\tlines++;\n\t\t\tif (lines === remainingLines) {\n\t\t\t\ttake = i + 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tthis.bytes = concat(this.bytes, chunk.subarray(0, take));\n\t\tthis.drop(chunk.subarray(take));\n\t}\n\n\tprivate pushTail(chunk: Uint8Array): void {\n\t\tconst incomingStart = tailStart(chunk, this.maxBytes, this.maxLines);\n\t\tthis.drop(chunk.subarray(0, incomingStart));\n\t\tconst combined = concat(this.bytes, chunk.subarray(incomingStart));\n\t\tconst start = tailStart(combined, this.maxBytes, this.maxLines);\n\t\tthis.drop(combined.subarray(0, start));\n\t\tthis.bytes = combined.slice(start);\n\t}\n\n\tprivate drop(bytes: Uint8Array): void {\n\t\tthis.droppedBytes += bytes.length;\n\t\tthis.droppedLines += countNewlines(bytes);\n\t}\n\n\tget dropped(): number {\n\t\treturn this.droppedBytes;\n\t}\n\n\ttext(): string {\n\t\treturn new TextDecoder().decode(this.bytes);\n\t}\n}\n\nfunction tailStart(bytes: Uint8Array, maxBytes: number, maxLines: number): number {\n\tlet start = Math.max(0, bytes.length - maxBytes);\n\tlet excessLines = countNewlines(bytes.subarray(start)) - maxLines;\n\tfor (; start < bytes.length && excessLines > 0; start++) {\n\t\tif (bytes[start] === 0x0a) excessLines--;\n\t}\n\treturn start;\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n\tif (a.length === 0) return b.slice();\n\tif (b.length === 0) return a;\n\tconst out = new Uint8Array(a.length + b.length);\n\tout.set(a);\n\tout.set(b, a.length);\n\treturn out;\n}\n\nfunction countNewlines(bytes: Uint8Array): number {\n\tlet count = 0;\n\tfor (const byte of bytes) if (byte === 0x0a) count++;\n\treturn count;\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded byte collector. It retains a prefix or suffix constrained by both a byte
|
|
3
|
+
* budget and a newline budget, and accounts for every discarded byte/newline.
|
|
4
|
+
*/
|
|
5
|
+
export class Bounded {
|
|
6
|
+
bytes = new Uint8Array();
|
|
7
|
+
maxBytes;
|
|
8
|
+
maxLines;
|
|
9
|
+
retain;
|
|
10
|
+
droppedBytes = 0;
|
|
11
|
+
droppedLines = 0;
|
|
12
|
+
total = 0;
|
|
13
|
+
constructor(maxBytes, maxLines, retain) {
|
|
14
|
+
this.maxBytes = Math.max(0, maxBytes);
|
|
15
|
+
this.maxLines = Math.max(0, maxLines);
|
|
16
|
+
this.retain = retain;
|
|
17
|
+
}
|
|
18
|
+
push(chunk) {
|
|
19
|
+
this.total += chunk.length;
|
|
20
|
+
if (chunk.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
if (this.maxBytes === 0 || this.maxLines === 0) {
|
|
23
|
+
this.drop(chunk);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (this.retain === "head") {
|
|
27
|
+
this.pushHead(chunk);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.pushTail(chunk);
|
|
31
|
+
}
|
|
32
|
+
pushHead(chunk) {
|
|
33
|
+
const remainingBytes = this.maxBytes - this.bytes.length;
|
|
34
|
+
const remainingLines = this.maxLines - countNewlines(this.bytes);
|
|
35
|
+
if (remainingBytes <= 0 || remainingLines <= 0) {
|
|
36
|
+
this.drop(chunk);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let take = Math.min(chunk.length, remainingBytes);
|
|
40
|
+
let lines = 0;
|
|
41
|
+
for (let i = 0; i < take; i++) {
|
|
42
|
+
if (chunk[i] !== 0x0a)
|
|
43
|
+
continue;
|
|
44
|
+
lines++;
|
|
45
|
+
if (lines === remainingLines) {
|
|
46
|
+
take = i + 1;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.bytes = concat(this.bytes, chunk.subarray(0, take));
|
|
51
|
+
this.drop(chunk.subarray(take));
|
|
52
|
+
}
|
|
53
|
+
pushTail(chunk) {
|
|
54
|
+
const incomingStart = tailStart(chunk, this.maxBytes, this.maxLines);
|
|
55
|
+
this.drop(chunk.subarray(0, incomingStart));
|
|
56
|
+
const combined = concat(this.bytes, chunk.subarray(incomingStart));
|
|
57
|
+
const start = tailStart(combined, this.maxBytes, this.maxLines);
|
|
58
|
+
this.drop(combined.subarray(0, start));
|
|
59
|
+
this.bytes = combined.slice(start);
|
|
60
|
+
}
|
|
61
|
+
drop(bytes) {
|
|
62
|
+
this.droppedBytes += bytes.length;
|
|
63
|
+
this.droppedLines += countNewlines(bytes);
|
|
64
|
+
}
|
|
65
|
+
get dropped() {
|
|
66
|
+
return this.droppedBytes;
|
|
67
|
+
}
|
|
68
|
+
text() {
|
|
69
|
+
return new TextDecoder().decode(this.bytes);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function tailStart(bytes, maxBytes, maxLines) {
|
|
73
|
+
let start = Math.max(0, bytes.length - maxBytes);
|
|
74
|
+
let excessLines = countNewlines(bytes.subarray(start)) - maxLines;
|
|
75
|
+
for (; start < bytes.length && excessLines > 0; start++) {
|
|
76
|
+
if (bytes[start] === 0x0a)
|
|
77
|
+
excessLines--;
|
|
78
|
+
}
|
|
79
|
+
return start;
|
|
80
|
+
}
|
|
81
|
+
function concat(a, b) {
|
|
82
|
+
if (a.length === 0)
|
|
83
|
+
return b.slice();
|
|
84
|
+
if (b.length === 0)
|
|
85
|
+
return a;
|
|
86
|
+
const out = new Uint8Array(a.length + b.length);
|
|
87
|
+
out.set(a);
|
|
88
|
+
out.set(b, a.length);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function countNewlines(bytes) {
|
|
92
|
+
let count = 0;
|
|
93
|
+
for (const byte of bytes)
|
|
94
|
+
if (byte === 0x0a)
|
|
95
|
+
count++;
|
|
96
|
+
return count;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=bounded.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounded.js","sourceRoot":"","sources":["../../../src/harness/pico3/bounded.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bounded byte collector. It retains a prefix or suffix constrained by both a byte\n * budget and a newline budget, and accounts for every discarded byte/newline.\n */\nexport class Bounded {\n\tprivate bytes: Uint8Array<ArrayBufferLike> = new Uint8Array();\n\tprivate readonly maxBytes: number;\n\tprivate readonly maxLines: number;\n\tprivate readonly retain: \"head\" | \"tail\";\n\tdroppedBytes = 0;\n\tdroppedLines = 0;\n\ttotal = 0;\n\n\tconstructor(maxBytes: number, maxLines: number, retain: \"head\" | \"tail\") {\n\t\tthis.maxBytes = Math.max(0, maxBytes);\n\t\tthis.maxLines = Math.max(0, maxLines);\n\t\tthis.retain = retain;\n\t}\n\n\tpush(chunk: Uint8Array): void {\n\t\tthis.total += chunk.length;\n\t\tif (chunk.length === 0) return;\n\t\tif (this.maxBytes === 0 || this.maxLines === 0) {\n\t\t\tthis.drop(chunk);\n\t\t\treturn;\n\t\t}\n\t\tif (this.retain === \"head\") {\n\t\t\tthis.pushHead(chunk);\n\t\t\treturn;\n\t\t}\n\t\tthis.pushTail(chunk);\n\t}\n\n\tprivate pushHead(chunk: Uint8Array): void {\n\t\tconst remainingBytes = this.maxBytes - this.bytes.length;\n\t\tconst remainingLines = this.maxLines - countNewlines(this.bytes);\n\t\tif (remainingBytes <= 0 || remainingLines <= 0) {\n\t\t\tthis.drop(chunk);\n\t\t\treturn;\n\t\t}\n\t\tlet take = Math.min(chunk.length, remainingBytes);\n\t\tlet lines = 0;\n\t\tfor (let i = 0; i < take; i++) {\n\t\t\tif (chunk[i] !== 0x0a) continue;\n\t\t\tlines++;\n\t\t\tif (lines === remainingLines) {\n\t\t\t\ttake = i + 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tthis.bytes = concat(this.bytes, chunk.subarray(0, take));\n\t\tthis.drop(chunk.subarray(take));\n\t}\n\n\tprivate pushTail(chunk: Uint8Array): void {\n\t\tconst incomingStart = tailStart(chunk, this.maxBytes, this.maxLines);\n\t\tthis.drop(chunk.subarray(0, incomingStart));\n\t\tconst combined = concat(this.bytes, chunk.subarray(incomingStart));\n\t\tconst start = tailStart(combined, this.maxBytes, this.maxLines);\n\t\tthis.drop(combined.subarray(0, start));\n\t\tthis.bytes = combined.slice(start);\n\t}\n\n\tprivate drop(bytes: Uint8Array): void {\n\t\tthis.droppedBytes += bytes.length;\n\t\tthis.droppedLines += countNewlines(bytes);\n\t}\n\n\tget dropped(): number {\n\t\treturn this.droppedBytes;\n\t}\n\n\ttext(): string {\n\t\treturn new TextDecoder().decode(this.bytes);\n\t}\n}\n\nfunction tailStart(bytes: Uint8Array, maxBytes: number, maxLines: number): number {\n\tlet start = Math.max(0, bytes.length - maxBytes);\n\tlet excessLines = countNewlines(bytes.subarray(start)) - maxLines;\n\tfor (; start < bytes.length && excessLines > 0; start++) {\n\t\tif (bytes[start] === 0x0a) excessLines--;\n\t}\n\treturn start;\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n\tif (a.length === 0) return b.slice();\n\tif (b.length === 0) return a;\n\tconst out = new Uint8Array(a.length + b.length);\n\tout.set(a);\n\tout.set(b, a.length);\n\treturn out;\n}\n\nfunction countNewlines(bytes: Uint8Array): number {\n\tlet count = 0;\n\tfor (const byte of bytes) if (byte === 0x0a) count++;\n\treturn count;\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Context, type MutableReplicatedState, type ReplicatedState } from "@earendil-works/chord";
|
|
2
|
+
import type { ConversationHandle, Harness } from "./harness.ts";
|
|
3
|
+
import type { ConversationSpec, ConversationView, Entry, EntryScan, Id, JsonObject, NewEntry, SendInput, ViewEvent } from "./types.ts";
|
|
4
|
+
export type PublishedConversationView = ConversationView & {
|
|
5
|
+
commit: {
|
|
6
|
+
events: ViewEvent[];
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export interface PicoConversationService {
|
|
10
|
+
readonly view: ReplicatedState<PublishedConversationView>;
|
|
11
|
+
send(input: SendInput, ctx: Context): Promise<Id>;
|
|
12
|
+
write(entry: NewEntry, ctx: Context): Promise<Id>;
|
|
13
|
+
inputAbort(id: Id, ctx: Context): Promise<"aborted" | "already_placed" | "not_found">;
|
|
14
|
+
configSet(patch: JsonObject, ctx: Context): Promise<void>;
|
|
15
|
+
abort(ctx: Context): Promise<void>;
|
|
16
|
+
reset(handoff: string | null, ctx: Context): Promise<void>;
|
|
17
|
+
collapse(instructions: string | null, ctx: Context): Promise<Id>;
|
|
18
|
+
fork(at: Id | "start", spec: Omit<ConversationSpec, "parent">, ctx: Context): Promise<Id>;
|
|
19
|
+
entries(scan: EntryScan, ctx: Context): Promise<Entry[]>;
|
|
20
|
+
}
|
|
21
|
+
/** Local registration/control plane and remote keyed conversation contract used by Chord facets. */
|
|
22
|
+
export declare const PicoHarnessService: import("@earendil-works/chord").Service<Harness<[]>>;
|
|
23
|
+
export declare const PicoConversationService: import("@earendil-works/chord").Service<PicoConversationService>;
|
|
24
|
+
export declare function createPicoConversationService<Cfg extends object>(harness: Pick<Harness, "abortInput" | "entries">, conversation: ConversationHandle<Cfg>, view: ReplicatedState<PublishedConversationView>): PicoConversationService;
|
|
25
|
+
export interface ChordViewBridge {
|
|
26
|
+
readonly view: MutableReplicatedState<PublishedConversationView>;
|
|
27
|
+
readonly closed: boolean;
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
export interface ChordViewBridgeOptions {
|
|
31
|
+
capacity?: number;
|
|
32
|
+
/** Called after the raw watch is closed. The owner should close and respawn its keyed service instance. */
|
|
33
|
+
onFailure?: (error: Error) => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Adapt Pico's commit-granular envelopes to one Chord publication per commit.
|
|
37
|
+
* The raw listener only enqueues. Applying ops and publishing happen together,
|
|
38
|
+
* off the Session line, with no await between them.
|
|
39
|
+
*/
|
|
40
|
+
export declare function attachChordView(conversation: Pick<ConversationHandle, "watch">, createState: (initial: PublishedConversationView) => MutableReplicatedState<PublishedConversationView>, ctx: Context, opts?: ChordViewBridgeOptions): Promise<ChordViewBridge>;
|
|
41
|
+
//# sourceMappingURL=chord.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chord.d.ts","sourceRoot":"","sources":["../../../src/harness/pico3/chord.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAiB,KAAK,sBAAsB,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAEvH,OAAO,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EACX,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,EACL,SAAS,EAET,EAAE,EACF,UAAU,EACV,QAAQ,EACR,SAAS,EACT,SAAS,EACT,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,yBAAyB,GAAG,gBAAgB,GAAG;IAC1D,MAAM,EAAE;QAAE,MAAM,EAAE,SAAS,EAAE,CAAA;KAAE,CAAC;CAChC,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,yBAAyB,CAAC,CAAC;IAC1D,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IAClD,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IAClD,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,GAAG,gBAAgB,GAAG,WAAW,CAAC,CAAC;IACtF,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IACjE,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IAC1F,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;CACzD;AAED,oGAAoG;AACpG,eAAO,MAAM,kBAAkB,sDAAwD,CAAC;AACxF,eAAO,MAAM,uBAAuB,kEAA4D,CAAC;AAEjG,wBAAgB,6BAA6B,CAAC,GAAG,SAAS,MAAM,EAC/D,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,CAAC,EAChD,YAAY,EAAE,kBAAkB,CAAC,GAAG,CAAC,EACrC,IAAI,EAAE,eAAe,CAAC,yBAAyB,CAAC,GAC9C,uBAAuB,CAazB;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC,yBAAyB,CAAC,CAAC;IACjE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2GAA2G;IAC3G,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACpC,YAAY,EAAE,IAAI,CAAC,kBAAkB,EAAE,OAAO,CAAC,EAC/C,WAAW,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,sBAAsB,CAAC,yBAAyB,CAAC,EACtG,GAAG,EAAE,OAAO,EACZ,IAAI,GAAE,sBAA2B,GAC/B,OAAO,CAAC,eAAe,CAAC,CAI1B","sourcesContent":["import { type Context, defineService, type MutableReplicatedState, type ReplicatedState } from \"@earendil-works/chord\";\nimport type { Op, Path, Seg } from \"@earendil-works/chord/delta\";\nimport type { ConversationHandle, Harness } from \"./harness.ts\";\nimport type {\n\tConversationSpec,\n\tConversationView,\n\tEntry,\n\tEntryScan,\n\tEnvelope,\n\tId,\n\tJsonObject,\n\tNewEntry,\n\tSendInput,\n\tViewEvent,\n} from \"./types.ts\";\nimport { WATCH_CAPACITY, type Watch } from \"./view.ts\";\n\nexport type PublishedConversationView = ConversationView & {\n\tcommit: { events: ViewEvent[] };\n};\n\nexport interface PicoConversationService {\n\treadonly view: ReplicatedState<PublishedConversationView>;\n\tsend(input: SendInput, ctx: Context): Promise<Id>;\n\twrite(entry: NewEntry, ctx: Context): Promise<Id>;\n\tinputAbort(id: Id, ctx: Context): Promise<\"aborted\" | \"already_placed\" | \"not_found\">;\n\tconfigSet(patch: JsonObject, ctx: Context): Promise<void>;\n\tabort(ctx: Context): Promise<void>;\n\treset(handoff: string | null, ctx: Context): Promise<void>;\n\tcollapse(instructions: string | null, ctx: Context): Promise<Id>;\n\tfork(at: Id | \"start\", spec: Omit<ConversationSpec, \"parent\">, ctx: Context): Promise<Id>;\n\tentries(scan: EntryScan, ctx: Context): Promise<Entry[]>;\n}\n\n/** Local registration/control plane and remote keyed conversation contract used by Chord facets. */\nexport const PicoHarnessService = defineService<Harness>(\"pi.harness\", { local: true });\nexport const PicoConversationService = defineService<PicoConversationService>(\"pi.conversation\");\n\nexport function createPicoConversationService<Cfg extends object>(\n\tharness: Pick<Harness, \"abortInput\" | \"entries\">,\n\tconversation: ConversationHandle<Cfg>,\n\tview: ReplicatedState<PublishedConversationView>,\n): PicoConversationService {\n\treturn {\n\t\tview,\n\t\tsend: (input, ctx) => conversation.send(input, ctx).then((handle) => handle.id),\n\t\twrite: (entry, ctx) => conversation.write(entry, ctx),\n\t\tinputAbort: (id, ctx) => harness.abortInput(id, ctx, conversation.id),\n\t\tconfigSet: (patch, ctx) => conversation.config.set(patch as never, ctx),\n\t\tabort: (ctx) => conversation.abort(ctx),\n\t\treset: (handoff, ctx) => conversation.reset(handoff ?? undefined, ctx),\n\t\tcollapse: (instructions, ctx) => conversation.collapse(instructions ?? undefined, ctx),\n\t\tfork: (at, spec, ctx) => conversation.fork(at, spec, ctx).then((child) => child.id),\n\t\tentries: (scan, ctx) => harness.entries({ ...scan, conversationId: conversation.id }, ctx),\n\t};\n}\n\nexport interface ChordViewBridge {\n\treadonly view: MutableReplicatedState<PublishedConversationView>;\n\treadonly closed: boolean;\n\tclose(): void;\n}\n\nexport interface ChordViewBridgeOptions {\n\tcapacity?: number;\n\t/** Called after the raw watch is closed. The owner should close and respawn its keyed service instance. */\n\tonFailure?: (error: Error) => void;\n}\n\n/**\n * Adapt Pico's commit-granular envelopes to one Chord publication per commit.\n * The raw listener only enqueues. Applying ops and publishing happen together,\n * off the Session line, with no await between them.\n */\nexport async function attachChordView(\n\tconversation: Pick<ConversationHandle, \"watch\">,\n\tcreateState: (initial: PublishedConversationView) => MutableReplicatedState<PublishedConversationView>,\n\tctx: Context,\n\topts: ChordViewBridgeOptions = {},\n): Promise<ChordViewBridge> {\n\tconst watch = await conversation.watch(ctx);\n\tconst view = createState({ ...structuredClone(watch.view), commit: { events: [] } });\n\treturn bridgeWatch(watch, view, ctx, opts);\n}\n\nfunction bridgeWatch(\n\twatch: Watch,\n\tview: MutableReplicatedState<PublishedConversationView>,\n\tctx: Context,\n\topts: ChordViewBridgeOptions,\n): ChordViewBridge {\n\tconst capacity = opts.capacity ?? WATCH_CAPACITY;\n\tif (!Number.isSafeInteger(capacity) || capacity < 1)\n\t\tthrow new RangeError(\"Chord view queue capacity must be positive\");\n\tconst queue: Envelope[] = [];\n\tlet scheduled = false;\n\tlet closed = false;\n\tconst fail = (cause: unknown) => {\n\t\tif (closed) return;\n\t\tclosed = true;\n\t\tqueue.length = 0;\n\t\twatch.stop();\n\t\ttry {\n\t\t\topts.onFailure?.(cause instanceof Error ? cause : new Error(String(cause)));\n\t\t} catch {}\n\t};\n\tconst drain = () => {\n\t\tscheduled = false;\n\t\twhile (!closed && queue.length > 0) {\n\t\t\tconst envelope = queue.shift()!;\n\t\t\ttry {\n\t\t\t\tapplyTracked(view.state, envelope.ops);\n\t\t\t\tview.state.commit.events = structuredClone(envelope.events);\n\t\t\t\tview.publish(ctx);\n\t\t\t} catch (error) {\n\t\t\t\tfail(error);\n\t\t\t}\n\t\t}\n\t};\n\twatch.start((envelope) => {\n\t\tif (closed) return;\n\t\tif (queue.length >= capacity) {\n\t\t\tfail(new Error(`Pico-to-Chord view queue exceeded ${capacity} envelopes`));\n\t\t\treturn;\n\t\t}\n\t\tqueue.push(envelope);\n\t\tif (scheduled) return;\n\t\tscheduled = true;\n\t\tqueueMicrotask(drain);\n\t});\n\treturn {\n\t\tview,\n\t\tget closed() {\n\t\t\treturn closed;\n\t\t},\n\t\tclose() {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tqueue.length = 0;\n\t\t\twatch.stop();\n\t\t},\n\t};\n}\n\nfunction applyTracked(root: PublishedConversationView, ops: readonly Op[]): void {\n\tfor (const op of ops) {\n\t\tif (op[0] === \"r\") throw new Error(\"live Pico envelope unexpectedly replaced the view root\");\n\t\tconst path = op[1];\n\t\tif (op[0] === \"p\") {\n\t\t\tconst target = resolve(root, path);\n\t\t\tif (!Array.isArray(target)) throw new Error(`Pico splice path is not an array: ${path.join(\".\")}`);\n\t\t\ttarget.splice(op[2], op[3], ...structuredClone(op[4]));\n\t\t\tcontinue;\n\t\t}\n\t\tconst parent = resolve(root, path.slice(0, -1));\n\t\tif (parent === null || typeof parent !== \"object\")\n\t\t\tthrow new Error(`Pico operation parent is not an object: ${path.join(\".\")}`);\n\t\tconst key = path[path.length - 1]!;\n\t\tif (op[0] === \"d\") {\n\t\t\tif (Array.isArray(parent)) parent.splice(key as number, 1);\n\t\t\telse delete (parent as Record<PropertyKey, unknown>)[key];\n\t\t\tcontinue;\n\t\t}\n\t\tconst record = parent as Record<PropertyKey, unknown>;\n\t\tif (op[0] === \"s\") record[key] = structuredClone(op[2]);\n\t\telse if (op[0] === \"a\") record[key] = `${String(record[key])}${op[2]}`;\n\t\telse record[key] = String(record[key]).slice(op[2]);\n\t}\n}\n\nfunction resolve(root: unknown, path: Path): unknown {\n\tlet value = root;\n\tfor (const segment of path) {\n\t\tif (value === null || typeof value !== \"object\") return undefined;\n\t\tvalue = (value as Record<Seg, unknown>)[segment];\n\t}\n\treturn value;\n}\n"]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { defineService } from "@earendil-works/chord";
|
|
2
|
+
import { WATCH_CAPACITY } from "./view.js";
|
|
3
|
+
/** Local registration/control plane and remote keyed conversation contract used by Chord facets. */
|
|
4
|
+
export const PicoHarnessService = defineService("pi.harness", { local: true });
|
|
5
|
+
export const PicoConversationService = defineService("pi.conversation");
|
|
6
|
+
export function createPicoConversationService(harness, conversation, view) {
|
|
7
|
+
return {
|
|
8
|
+
view,
|
|
9
|
+
send: (input, ctx) => conversation.send(input, ctx).then((handle) => handle.id),
|
|
10
|
+
write: (entry, ctx) => conversation.write(entry, ctx),
|
|
11
|
+
inputAbort: (id, ctx) => harness.abortInput(id, ctx, conversation.id),
|
|
12
|
+
configSet: (patch, ctx) => conversation.config.set(patch, ctx),
|
|
13
|
+
abort: (ctx) => conversation.abort(ctx),
|
|
14
|
+
reset: (handoff, ctx) => conversation.reset(handoff ?? undefined, ctx),
|
|
15
|
+
collapse: (instructions, ctx) => conversation.collapse(instructions ?? undefined, ctx),
|
|
16
|
+
fork: (at, spec, ctx) => conversation.fork(at, spec, ctx).then((child) => child.id),
|
|
17
|
+
entries: (scan, ctx) => harness.entries({ ...scan, conversationId: conversation.id }, ctx),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Adapt Pico's commit-granular envelopes to one Chord publication per commit.
|
|
22
|
+
* The raw listener only enqueues. Applying ops and publishing happen together,
|
|
23
|
+
* off the Session line, with no await between them.
|
|
24
|
+
*/
|
|
25
|
+
export async function attachChordView(conversation, createState, ctx, opts = {}) {
|
|
26
|
+
const watch = await conversation.watch(ctx);
|
|
27
|
+
const view = createState({ ...structuredClone(watch.view), commit: { events: [] } });
|
|
28
|
+
return bridgeWatch(watch, view, ctx, opts);
|
|
29
|
+
}
|
|
30
|
+
function bridgeWatch(watch, view, ctx, opts) {
|
|
31
|
+
const capacity = opts.capacity ?? WATCH_CAPACITY;
|
|
32
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1)
|
|
33
|
+
throw new RangeError("Chord view queue capacity must be positive");
|
|
34
|
+
const queue = [];
|
|
35
|
+
let scheduled = false;
|
|
36
|
+
let closed = false;
|
|
37
|
+
const fail = (cause) => {
|
|
38
|
+
if (closed)
|
|
39
|
+
return;
|
|
40
|
+
closed = true;
|
|
41
|
+
queue.length = 0;
|
|
42
|
+
watch.stop();
|
|
43
|
+
try {
|
|
44
|
+
opts.onFailure?.(cause instanceof Error ? cause : new Error(String(cause)));
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
};
|
|
48
|
+
const drain = () => {
|
|
49
|
+
scheduled = false;
|
|
50
|
+
while (!closed && queue.length > 0) {
|
|
51
|
+
const envelope = queue.shift();
|
|
52
|
+
try {
|
|
53
|
+
applyTracked(view.state, envelope.ops);
|
|
54
|
+
view.state.commit.events = structuredClone(envelope.events);
|
|
55
|
+
view.publish(ctx);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
fail(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
watch.start((envelope) => {
|
|
63
|
+
if (closed)
|
|
64
|
+
return;
|
|
65
|
+
if (queue.length >= capacity) {
|
|
66
|
+
fail(new Error(`Pico-to-Chord view queue exceeded ${capacity} envelopes`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
queue.push(envelope);
|
|
70
|
+
if (scheduled)
|
|
71
|
+
return;
|
|
72
|
+
scheduled = true;
|
|
73
|
+
queueMicrotask(drain);
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
view,
|
|
77
|
+
get closed() {
|
|
78
|
+
return closed;
|
|
79
|
+
},
|
|
80
|
+
close() {
|
|
81
|
+
if (closed)
|
|
82
|
+
return;
|
|
83
|
+
closed = true;
|
|
84
|
+
queue.length = 0;
|
|
85
|
+
watch.stop();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function applyTracked(root, ops) {
|
|
90
|
+
for (const op of ops) {
|
|
91
|
+
if (op[0] === "r")
|
|
92
|
+
throw new Error("live Pico envelope unexpectedly replaced the view root");
|
|
93
|
+
const path = op[1];
|
|
94
|
+
if (op[0] === "p") {
|
|
95
|
+
const target = resolve(root, path);
|
|
96
|
+
if (!Array.isArray(target))
|
|
97
|
+
throw new Error(`Pico splice path is not an array: ${path.join(".")}`);
|
|
98
|
+
target.splice(op[2], op[3], ...structuredClone(op[4]));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const parent = resolve(root, path.slice(0, -1));
|
|
102
|
+
if (parent === null || typeof parent !== "object")
|
|
103
|
+
throw new Error(`Pico operation parent is not an object: ${path.join(".")}`);
|
|
104
|
+
const key = path[path.length - 1];
|
|
105
|
+
if (op[0] === "d") {
|
|
106
|
+
if (Array.isArray(parent))
|
|
107
|
+
parent.splice(key, 1);
|
|
108
|
+
else
|
|
109
|
+
delete parent[key];
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const record = parent;
|
|
113
|
+
if (op[0] === "s")
|
|
114
|
+
record[key] = structuredClone(op[2]);
|
|
115
|
+
else if (op[0] === "a")
|
|
116
|
+
record[key] = `${String(record[key])}${op[2]}`;
|
|
117
|
+
else
|
|
118
|
+
record[key] = String(record[key]).slice(op[2]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function resolve(root, path) {
|
|
122
|
+
let value = root;
|
|
123
|
+
for (const segment of path) {
|
|
124
|
+
if (value === null || typeof value !== "object")
|
|
125
|
+
return undefined;
|
|
126
|
+
value = value[segment];
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=chord.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chord.js","sourceRoot":"","sources":["../../../src/harness/pico3/chord.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Context, defineService, type MutableReplicatedState, type ReplicatedState } from \"@earendil-works/chord\";\nimport type { Op, Path, Seg } from \"@earendil-works/chord/delta\";\nimport type { ConversationHandle, Harness } from \"./harness.ts\";\nimport type {\n\tConversationSpec,\n\tConversationView,\n\tEntry,\n\tEntryScan,\n\tEnvelope,\n\tId,\n\tJsonObject,\n\tNewEntry,\n\tSendInput,\n\tViewEvent,\n} from \"./types.ts\";\nimport { WATCH_CAPACITY, type Watch } from \"./view.ts\";\n\nexport type PublishedConversationView = ConversationView & {\n\tcommit: { events: ViewEvent[] };\n};\n\nexport interface PicoConversationService {\n\treadonly view: ReplicatedState<PublishedConversationView>;\n\tsend(input: SendInput, ctx: Context): Promise<Id>;\n\twrite(entry: NewEntry, ctx: Context): Promise<Id>;\n\tinputAbort(id: Id, ctx: Context): Promise<\"aborted\" | \"already_placed\" | \"not_found\">;\n\tconfigSet(patch: JsonObject, ctx: Context): Promise<void>;\n\tabort(ctx: Context): Promise<void>;\n\treset(handoff: string | null, ctx: Context): Promise<void>;\n\tcollapse(instructions: string | null, ctx: Context): Promise<Id>;\n\tfork(at: Id | \"start\", spec: Omit<ConversationSpec, \"parent\">, ctx: Context): Promise<Id>;\n\tentries(scan: EntryScan, ctx: Context): Promise<Entry[]>;\n}\n\n/** Local registration/control plane and remote keyed conversation contract used by Chord facets. */\nexport const PicoHarnessService = defineService<Harness>(\"pi.harness\", { local: true });\nexport const PicoConversationService = defineService<PicoConversationService>(\"pi.conversation\");\n\nexport function createPicoConversationService<Cfg extends object>(\n\tharness: Pick<Harness, \"abortInput\" | \"entries\">,\n\tconversation: ConversationHandle<Cfg>,\n\tview: ReplicatedState<PublishedConversationView>,\n): PicoConversationService {\n\treturn {\n\t\tview,\n\t\tsend: (input, ctx) => conversation.send(input, ctx).then((handle) => handle.id),\n\t\twrite: (entry, ctx) => conversation.write(entry, ctx),\n\t\tinputAbort: (id, ctx) => harness.abortInput(id, ctx, conversation.id),\n\t\tconfigSet: (patch, ctx) => conversation.config.set(patch as never, ctx),\n\t\tabort: (ctx) => conversation.abort(ctx),\n\t\treset: (handoff, ctx) => conversation.reset(handoff ?? undefined, ctx),\n\t\tcollapse: (instructions, ctx) => conversation.collapse(instructions ?? undefined, ctx),\n\t\tfork: (at, spec, ctx) => conversation.fork(at, spec, ctx).then((child) => child.id),\n\t\tentries: (scan, ctx) => harness.entries({ ...scan, conversationId: conversation.id }, ctx),\n\t};\n}\n\nexport interface ChordViewBridge {\n\treadonly view: MutableReplicatedState<PublishedConversationView>;\n\treadonly closed: boolean;\n\tclose(): void;\n}\n\nexport interface ChordViewBridgeOptions {\n\tcapacity?: number;\n\t/** Called after the raw watch is closed. The owner should close and respawn its keyed service instance. */\n\tonFailure?: (error: Error) => void;\n}\n\n/**\n * Adapt Pico's commit-granular envelopes to one Chord publication per commit.\n * The raw listener only enqueues. Applying ops and publishing happen together,\n * off the Session line, with no await between them.\n */\nexport async function attachChordView(\n\tconversation: Pick<ConversationHandle, \"watch\">,\n\tcreateState: (initial: PublishedConversationView) => MutableReplicatedState<PublishedConversationView>,\n\tctx: Context,\n\topts: ChordViewBridgeOptions = {},\n): Promise<ChordViewBridge> {\n\tconst watch = await conversation.watch(ctx);\n\tconst view = createState({ ...structuredClone(watch.view), commit: { events: [] } });\n\treturn bridgeWatch(watch, view, ctx, opts);\n}\n\nfunction bridgeWatch(\n\twatch: Watch,\n\tview: MutableReplicatedState<PublishedConversationView>,\n\tctx: Context,\n\topts: ChordViewBridgeOptions,\n): ChordViewBridge {\n\tconst capacity = opts.capacity ?? WATCH_CAPACITY;\n\tif (!Number.isSafeInteger(capacity) || capacity < 1)\n\t\tthrow new RangeError(\"Chord view queue capacity must be positive\");\n\tconst queue: Envelope[] = [];\n\tlet scheduled = false;\n\tlet closed = false;\n\tconst fail = (cause: unknown) => {\n\t\tif (closed) return;\n\t\tclosed = true;\n\t\tqueue.length = 0;\n\t\twatch.stop();\n\t\ttry {\n\t\t\topts.onFailure?.(cause instanceof Error ? cause : new Error(String(cause)));\n\t\t} catch {}\n\t};\n\tconst drain = () => {\n\t\tscheduled = false;\n\t\twhile (!closed && queue.length > 0) {\n\t\t\tconst envelope = queue.shift()!;\n\t\t\ttry {\n\t\t\t\tapplyTracked(view.state, envelope.ops);\n\t\t\t\tview.state.commit.events = structuredClone(envelope.events);\n\t\t\t\tview.publish(ctx);\n\t\t\t} catch (error) {\n\t\t\t\tfail(error);\n\t\t\t}\n\t\t}\n\t};\n\twatch.start((envelope) => {\n\t\tif (closed) return;\n\t\tif (queue.length >= capacity) {\n\t\t\tfail(new Error(`Pico-to-Chord view queue exceeded ${capacity} envelopes`));\n\t\t\treturn;\n\t\t}\n\t\tqueue.push(envelope);\n\t\tif (scheduled) return;\n\t\tscheduled = true;\n\t\tqueueMicrotask(drain);\n\t});\n\treturn {\n\t\tview,\n\t\tget closed() {\n\t\t\treturn closed;\n\t\t},\n\t\tclose() {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tqueue.length = 0;\n\t\t\twatch.stop();\n\t\t},\n\t};\n}\n\nfunction applyTracked(root: PublishedConversationView, ops: readonly Op[]): void {\n\tfor (const op of ops) {\n\t\tif (op[0] === \"r\") throw new Error(\"live Pico envelope unexpectedly replaced the view root\");\n\t\tconst path = op[1];\n\t\tif (op[0] === \"p\") {\n\t\t\tconst target = resolve(root, path);\n\t\t\tif (!Array.isArray(target)) throw new Error(`Pico splice path is not an array: ${path.join(\".\")}`);\n\t\t\ttarget.splice(op[2], op[3], ...structuredClone(op[4]));\n\t\t\tcontinue;\n\t\t}\n\t\tconst parent = resolve(root, path.slice(0, -1));\n\t\tif (parent === null || typeof parent !== \"object\")\n\t\t\tthrow new Error(`Pico operation parent is not an object: ${path.join(\".\")}`);\n\t\tconst key = path[path.length - 1]!;\n\t\tif (op[0] === \"d\") {\n\t\t\tif (Array.isArray(parent)) parent.splice(key as number, 1);\n\t\t\telse delete (parent as Record<PropertyKey, unknown>)[key];\n\t\t\tcontinue;\n\t\t}\n\t\tconst record = parent as Record<PropertyKey, unknown>;\n\t\tif (op[0] === \"s\") record[key] = structuredClone(op[2]);\n\t\telse if (op[0] === \"a\") record[key] = `${String(record[key])}${op[2]}`;\n\t\telse record[key] = String(record[key]).slice(op[2]);\n\t}\n}\n\nfunction resolve(root: unknown, path: Path): unknown {\n\tlet value = root;\n\tfor (const segment of path) {\n\t\tif (value === null || typeof value !== \"object\") return undefined;\n\t\tvalue = (value as Record<Seg, unknown>)[segment];\n\t}\n\treturn value;\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/chord";
|
|
2
|
+
import type { ContextView, Id, Storage } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* pico §1.3, verbatim:
|
|
5
|
+
* H = newest fork-visible entry at or before T with a head
|
|
6
|
+
* from = H ? H.head : transcript start
|
|
7
|
+
* range = fork-visible entries from `from` through T
|
|
8
|
+
* edits = per target, newest edit in range wins
|
|
9
|
+
* entries = H ? [H, ...range without any head entries] : range
|
|
10
|
+
* model = concat(entries.map(e => edits[e.id] ? apply : e.model)), then reorder tool results
|
|
11
|
+
*
|
|
12
|
+
* Display-only entries (aborted/error assistants, pi.usage, model-less plugin entries) have no
|
|
13
|
+
* `model` and contribute nothing. Nothing here inspects an error string.
|
|
14
|
+
*/
|
|
15
|
+
export declare function deriveContext(storage: Storage, conversationId: Id, at: Id | undefined, ctx: Context): Promise<ContextView>;
|
|
16
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/harness/pico3/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAe,WAAW,EAAS,EAAE,EAAkB,OAAO,EAAiB,MAAM,YAAY,CAAC;AAE9G;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAClC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,EAAE,EAClB,EAAE,EAAE,EAAE,GAAG,SAAS,EAClB,GAAG,EAAE,OAAO,GACV,OAAO,CAAC,WAAW,CAAC,CA4CtB","sourcesContent":["import type { Context } from \"@earendil-works/chord\";\nimport type { ContextEdit, ContextView, Entry, Id, RequestMessage, Storage, StoredMessage } from \"./types.ts\";\n\n/**\n * pico §1.3, verbatim:\n * H = newest fork-visible entry at or before T with a head\n * from = H ? H.head : transcript start\n * range = fork-visible entries from `from` through T\n * edits = per target, newest edit in range wins\n * entries = H ? [H, ...range without any head entries] : range\n * model = concat(entries.map(e => edits[e.id] ? apply : e.model)), then reorder tool results\n *\n * Display-only entries (aborted/error assistants, pi.usage, model-less plugin entries) have no\n * `model` and contribute nothing. Nothing here inspects an error string.\n */\nexport async function deriveContext(\n\tstorage: Storage,\n\tconversationId: Id,\n\tat: Id | undefined,\n\tctx: Context,\n): Promise<ContextView> {\n\tconst [head] = await storage.scanEntries(\n\t\t{ conversationId, withHead: true, ...(at === undefined ? {} : { before: at + 1 }), limit: 1 },\n\t\tctx,\n\t);\n\tconst from = head?.head;\n\n\t// Walk newest-first until we pass `from`.\n\tconst range: Entry[] = [];\n\tlet before = at === undefined ? undefined : at + 1;\n\tfor (;;) {\n\t\tconst page = await storage.scanEntries(\n\t\t\t{ conversationId, ...(before === undefined ? {} : { before }), limit: 256 },\n\t\t\tctx,\n\t\t);\n\t\tlet done = page.length < 256;\n\t\tfor (const e of page) {\n\t\t\tif (from !== undefined && e.id < from) {\n\t\t\t\tdone = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trange.push(e);\n\t\t}\n\t\tif (done) break;\n\t\tbefore = page[page.length - 1]!.id;\n\t}\n\trange.reverse();\n\n\tconst edits = new Map<Id, ContextEdit>();\n\tfor (const e of range) for (const edit of e.edits ?? []) edits.set(edit.target, edit); // later wins by iteration order\n\n\tconst entries = head === undefined ? range : [head, ...range.filter((e) => e.head === undefined)];\n\n\tconst messages: RequestMessage[] = [];\n\tfor (const e of entries) {\n\t\tconst edit = edits.get(e.id);\n\t\tif (edit?.action === \"omit\") continue;\n\t\tif (edit?.action === \"replace\") {\n\t\t\tmessages.push(...(edit.messages ?? []));\n\t\t\tcontinue;\n\t\t}\n\t\tif (e.model) messages.push(...e.model);\n\t}\n\treturn { head, entries, messages: reorderToolResults(messages) };\n}\n\n/** Results are appended as tools finish; put them back in call order, synthesising a missing one after a fork cut. */\ntype StoredAssistant = Extract<StoredMessage, { role: \"assistant\" }>;\ntype StoredToolResult = Extract<StoredMessage, { role: \"toolResult\" }>;\ntype StoredToolCall = Extract<StoredAssistant[\"content\"][number], { type: \"toolCall\" }>;\nfunction reorderToolResults(messages: RequestMessage[]): RequestMessage[] {\n\tconst out: RequestMessage[] = [];\n\tfor (let i = 0; i < messages.length; i++) {\n\t\tconst m = messages[i]!;\n\t\tout.push(m);\n\t\tif (m.role !== \"assistant\") continue;\n\t\tconst calls = (m as StoredAssistant).content.filter((c): c is StoredToolCall => c.type === \"toolCall\");\n\t\tif (calls.length === 0) continue;\n\t\tconst results = new Map<string, StoredToolResult>();\n\t\tlet j = i + 1;\n\t\twhile (j < messages.length && messages[j]!.role === \"toolResult\") {\n\t\t\tconst r = messages[j] as StoredToolResult;\n\t\t\tresults.set(r.toolCallId, r);\n\t\t\tj++;\n\t\t}\n\t\tfor (const call of calls) {\n\t\t\tout.push(\n\t\t\t\tresults.get(call.id) ??\n\t\t\t\t\t({\n\t\t\t\t\t\trole: \"toolResult\",\n\t\t\t\t\t\ttoolCallId: call.id,\n\t\t\t\t\t\ttoolName: call.name,\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{ type: \"text\", text: \"Tool result unavailable: history ends before this call completed.\" },\n\t\t\t\t\t\t],\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\tdetails: { reason: \"missing_after_fork\" },\n\t\t\t\t\t\ttimestamp: (m as StoredAssistant).timestamp,\n\t\t\t\t\t} as StoredToolResult),\n\t\t\t);\n\t\t}\n\t\ti = j - 1;\n\t}\n\treturn out;\n}\n"]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pico §1.3, verbatim:
|
|
3
|
+
* H = newest fork-visible entry at or before T with a head
|
|
4
|
+
* from = H ? H.head : transcript start
|
|
5
|
+
* range = fork-visible entries from `from` through T
|
|
6
|
+
* edits = per target, newest edit in range wins
|
|
7
|
+
* entries = H ? [H, ...range without any head entries] : range
|
|
8
|
+
* model = concat(entries.map(e => edits[e.id] ? apply : e.model)), then reorder tool results
|
|
9
|
+
*
|
|
10
|
+
* Display-only entries (aborted/error assistants, pi.usage, model-less plugin entries) have no
|
|
11
|
+
* `model` and contribute nothing. Nothing here inspects an error string.
|
|
12
|
+
*/
|
|
13
|
+
export async function deriveContext(storage, conversationId, at, ctx) {
|
|
14
|
+
const [head] = await storage.scanEntries({ conversationId, withHead: true, ...(at === undefined ? {} : { before: at + 1 }), limit: 1 }, ctx);
|
|
15
|
+
const from = head?.head;
|
|
16
|
+
// Walk newest-first until we pass `from`.
|
|
17
|
+
const range = [];
|
|
18
|
+
let before = at === undefined ? undefined : at + 1;
|
|
19
|
+
for (;;) {
|
|
20
|
+
const page = await storage.scanEntries({ conversationId, ...(before === undefined ? {} : { before }), limit: 256 }, ctx);
|
|
21
|
+
let done = page.length < 256;
|
|
22
|
+
for (const e of page) {
|
|
23
|
+
if (from !== undefined && e.id < from) {
|
|
24
|
+
done = true;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
range.push(e);
|
|
28
|
+
}
|
|
29
|
+
if (done)
|
|
30
|
+
break;
|
|
31
|
+
before = page[page.length - 1].id;
|
|
32
|
+
}
|
|
33
|
+
range.reverse();
|
|
34
|
+
const edits = new Map();
|
|
35
|
+
for (const e of range)
|
|
36
|
+
for (const edit of e.edits ?? [])
|
|
37
|
+
edits.set(edit.target, edit); // later wins by iteration order
|
|
38
|
+
const entries = head === undefined ? range : [head, ...range.filter((e) => e.head === undefined)];
|
|
39
|
+
const messages = [];
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
const edit = edits.get(e.id);
|
|
42
|
+
if (edit?.action === "omit")
|
|
43
|
+
continue;
|
|
44
|
+
if (edit?.action === "replace") {
|
|
45
|
+
messages.push(...(edit.messages ?? []));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (e.model)
|
|
49
|
+
messages.push(...e.model);
|
|
50
|
+
}
|
|
51
|
+
return { head, entries, messages: reorderToolResults(messages) };
|
|
52
|
+
}
|
|
53
|
+
function reorderToolResults(messages) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (let i = 0; i < messages.length; i++) {
|
|
56
|
+
const m = messages[i];
|
|
57
|
+
out.push(m);
|
|
58
|
+
if (m.role !== "assistant")
|
|
59
|
+
continue;
|
|
60
|
+
const calls = m.content.filter((c) => c.type === "toolCall");
|
|
61
|
+
if (calls.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
const results = new Map();
|
|
64
|
+
let j = i + 1;
|
|
65
|
+
while (j < messages.length && messages[j].role === "toolResult") {
|
|
66
|
+
const r = messages[j];
|
|
67
|
+
results.set(r.toolCallId, r);
|
|
68
|
+
j++;
|
|
69
|
+
}
|
|
70
|
+
for (const call of calls) {
|
|
71
|
+
out.push(results.get(call.id) ??
|
|
72
|
+
{
|
|
73
|
+
role: "toolResult",
|
|
74
|
+
toolCallId: call.id,
|
|
75
|
+
toolName: call.name,
|
|
76
|
+
content: [
|
|
77
|
+
{ type: "text", text: "Tool result unavailable: history ends before this call completed." },
|
|
78
|
+
],
|
|
79
|
+
isError: true,
|
|
80
|
+
details: { reason: "missing_after_fork" },
|
|
81
|
+
timestamp: m.timestamp,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
i = j - 1;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/harness/pico3/context.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context } from \"@earendil-works/chord\";\nimport type { ContextEdit, ContextView, Entry, Id, RequestMessage, Storage, StoredMessage } from \"./types.ts\";\n\n/**\n * pico §1.3, verbatim:\n * H = newest fork-visible entry at or before T with a head\n * from = H ? H.head : transcript start\n * range = fork-visible entries from `from` through T\n * edits = per target, newest edit in range wins\n * entries = H ? [H, ...range without any head entries] : range\n * model = concat(entries.map(e => edits[e.id] ? apply : e.model)), then reorder tool results\n *\n * Display-only entries (aborted/error assistants, pi.usage, model-less plugin entries) have no\n * `model` and contribute nothing. Nothing here inspects an error string.\n */\nexport async function deriveContext(\n\tstorage: Storage,\n\tconversationId: Id,\n\tat: Id | undefined,\n\tctx: Context,\n): Promise<ContextView> {\n\tconst [head] = await storage.scanEntries(\n\t\t{ conversationId, withHead: true, ...(at === undefined ? {} : { before: at + 1 }), limit: 1 },\n\t\tctx,\n\t);\n\tconst from = head?.head;\n\n\t// Walk newest-first until we pass `from`.\n\tconst range: Entry[] = [];\n\tlet before = at === undefined ? undefined : at + 1;\n\tfor (;;) {\n\t\tconst page = await storage.scanEntries(\n\t\t\t{ conversationId, ...(before === undefined ? {} : { before }), limit: 256 },\n\t\t\tctx,\n\t\t);\n\t\tlet done = page.length < 256;\n\t\tfor (const e of page) {\n\t\t\tif (from !== undefined && e.id < from) {\n\t\t\t\tdone = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trange.push(e);\n\t\t}\n\t\tif (done) break;\n\t\tbefore = page[page.length - 1]!.id;\n\t}\n\trange.reverse();\n\n\tconst edits = new Map<Id, ContextEdit>();\n\tfor (const e of range) for (const edit of e.edits ?? []) edits.set(edit.target, edit); // later wins by iteration order\n\n\tconst entries = head === undefined ? range : [head, ...range.filter((e) => e.head === undefined)];\n\n\tconst messages: RequestMessage[] = [];\n\tfor (const e of entries) {\n\t\tconst edit = edits.get(e.id);\n\t\tif (edit?.action === \"omit\") continue;\n\t\tif (edit?.action === \"replace\") {\n\t\t\tmessages.push(...(edit.messages ?? []));\n\t\t\tcontinue;\n\t\t}\n\t\tif (e.model) messages.push(...e.model);\n\t}\n\treturn { head, entries, messages: reorderToolResults(messages) };\n}\n\n/** Results are appended as tools finish; put them back in call order, synthesising a missing one after a fork cut. */\ntype StoredAssistant = Extract<StoredMessage, { role: \"assistant\" }>;\ntype StoredToolResult = Extract<StoredMessage, { role: \"toolResult\" }>;\ntype StoredToolCall = Extract<StoredAssistant[\"content\"][number], { type: \"toolCall\" }>;\nfunction reorderToolResults(messages: RequestMessage[]): RequestMessage[] {\n\tconst out: RequestMessage[] = [];\n\tfor (let i = 0; i < messages.length; i++) {\n\t\tconst m = messages[i]!;\n\t\tout.push(m);\n\t\tif (m.role !== \"assistant\") continue;\n\t\tconst calls = (m as StoredAssistant).content.filter((c): c is StoredToolCall => c.type === \"toolCall\");\n\t\tif (calls.length === 0) continue;\n\t\tconst results = new Map<string, StoredToolResult>();\n\t\tlet j = i + 1;\n\t\twhile (j < messages.length && messages[j]!.role === \"toolResult\") {\n\t\t\tconst r = messages[j] as StoredToolResult;\n\t\t\tresults.set(r.toolCallId, r);\n\t\t\tj++;\n\t\t}\n\t\tfor (const call of calls) {\n\t\t\tout.push(\n\t\t\t\tresults.get(call.id) ??\n\t\t\t\t\t({\n\t\t\t\t\t\trole: \"toolResult\",\n\t\t\t\t\t\ttoolCallId: call.id,\n\t\t\t\t\t\ttoolName: call.name,\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{ type: \"text\", text: \"Tool result unavailable: history ends before this call completed.\" },\n\t\t\t\t\t\t],\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\tdetails: { reason: \"missing_after_fork\" },\n\t\t\t\t\t\ttimestamp: (m as StoredAssistant).timestamp,\n\t\t\t\t\t} as StoredToolResult),\n\t\t\t);\n\t\t}\n\t\ti = j - 1;\n\t}\n\treturn out;\n}\n"]}
|