@chroxify/eunoe 1.2.1 → 1.3.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 +143 -5
- package/dist/core/index.js +2235 -0
- package/dist/types/context/content.d.ts +31 -0
- package/dist/types/context/fold/evidence.d.ts +20 -0
- package/dist/types/context/fold/index.d.ts +11 -0
- package/dist/types/context/fold/strategies.d.ts +20 -0
- package/dist/types/context/fold/turns.d.ts +14 -0
- package/dist/types/context/prompts.d.ts +24 -0
- package/dist/types/context/recall/index.d.ts +44 -0
- package/dist/types/context/recall/rerank.d.ts +3 -0
- package/dist/types/core/compaction.d.ts +32 -0
- package/dist/types/core/config.d.ts +33 -0
- package/dist/types/core/estimate.d.ts +9 -0
- package/dist/types/core/fingerprint.d.ts +10 -0
- package/dist/types/core/fold.d.ts +18 -0
- package/dist/types/core/forward.d.ts +24 -0
- package/dist/types/core/handoff.d.ts +10 -0
- package/dist/types/core/index.d.ts +14 -0
- package/dist/types/core/recall.d.ts +8 -0
- package/dist/types/core/rewrite.d.ts +63 -0
- package/dist/types/core/state.d.ts +22 -0
- package/dist/types/core/subagents.d.ts +10 -0
- package/dist/types/core/transcript.d.ts +9 -0
- package/dist/types/core/usage.d.ts +7 -0
- package/dist/types/core/window.d.ts +5 -0
- package/dist/types/transcript/format.d.ts +16 -0
- package/dist/types/transcript/parse.d.ts +45 -0
- package/package.json +15 -2
- package/src/cli/commands/mode.ts +2 -1
- package/src/cli/commands/serve.ts +2 -0
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/proxy/stats.ts +1 -1
- package/src/cli/replay/index.ts +3 -3
- package/src/cli/sessions/index.ts +2 -2
- package/src/config/index.ts +4 -27
- package/src/context/content.ts +64 -1
- package/src/context/fold/strategies.ts +55 -10
- package/src/context/fold/turns.ts +22 -2
- package/src/context/prompts.ts +4 -3
- package/src/context/recall/index.ts +259 -35
- package/src/context/recall/rerank.ts +0 -8
- package/src/{proxy → core}/compaction.ts +74 -24
- package/src/core/config.ts +31 -0
- package/src/core/fold.ts +69 -0
- package/src/core/forward.ts +192 -0
- package/src/{proxy → core}/handoff.ts +23 -23
- package/src/core/index.ts +14 -0
- package/src/{proxy → core}/recall.ts +18 -20
- package/src/core/rewrite.ts +189 -0
- package/src/core/state.ts +40 -0
- package/src/core/subagents.ts +28 -0
- package/src/core/transcript.ts +44 -0
- package/src/{proxy → core}/usage.ts +34 -1
- package/src/core/window.ts +39 -0
- package/src/proxy/rewrite.ts +22 -139
- package/src/proxy/server.ts +24 -137
- package/src/proxy/sessions.ts +1 -1
- package/src/proxy/sources.ts +35 -0
- package/src/proxy/state.ts +32 -21
- package/src/proxy/warm.ts +68 -0
- package/src/proxy/window.ts +3 -33
- package/src/transcript/entries.ts +114 -0
- package/src/transcript/format.ts +1 -2
- package/src/transcript/messages.ts +14 -11
- package/src/transcript/render-worker.ts +24 -0
- package/src/transcript/render.ts +12 -3
- package/src/transcript/search.ts +46 -5
- package/src/transcript/subagents.ts +55 -34
- /package/src/{proxy → core}/estimate.ts +0 -0
- /package/src/{proxy → core}/fingerprint.ts +0 -0
|
@@ -0,0 +1,2235 @@
|
|
|
1
|
+
// src/context/content.ts
|
|
2
|
+
function contentBlocks(content) {
|
|
3
|
+
if (typeof content === "string")
|
|
4
|
+
return [{ type: "text", text: content }];
|
|
5
|
+
return Array.isArray(content) ? content : [];
|
|
6
|
+
}
|
|
7
|
+
function blocks(message) {
|
|
8
|
+
return contentBlocks(message.content);
|
|
9
|
+
}
|
|
10
|
+
function joinedText(list) {
|
|
11
|
+
return list.filter((b) => b.type === "text").map((b) => String(b.text ?? "")).join(`
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
function textOf(message) {
|
|
15
|
+
return joinedText(blocks(message));
|
|
16
|
+
}
|
|
17
|
+
function resultText(block) {
|
|
18
|
+
return typeof block.content === "string" ? block.content : joinedText(contentBlocks(block.content));
|
|
19
|
+
}
|
|
20
|
+
function withoutCacheControl(block) {
|
|
21
|
+
if (!("cache_control" in block))
|
|
22
|
+
return block;
|
|
23
|
+
const { cache_control: _, ...rest } = block;
|
|
24
|
+
return rest;
|
|
25
|
+
}
|
|
26
|
+
function clean(message) {
|
|
27
|
+
return { role: message.role, content: blocks(message).map(withoutCacheControl) };
|
|
28
|
+
}
|
|
29
|
+
var SHA256_ROUNDS = new Uint32Array([
|
|
30
|
+
1116352408,
|
|
31
|
+
1899447441,
|
|
32
|
+
3049323471,
|
|
33
|
+
3921009573,
|
|
34
|
+
961987163,
|
|
35
|
+
1508970993,
|
|
36
|
+
2453635748,
|
|
37
|
+
2870763221,
|
|
38
|
+
3624381080,
|
|
39
|
+
310598401,
|
|
40
|
+
607225278,
|
|
41
|
+
1426881987,
|
|
42
|
+
1925078388,
|
|
43
|
+
2162078206,
|
|
44
|
+
2614888103,
|
|
45
|
+
3248222580,
|
|
46
|
+
3835390401,
|
|
47
|
+
4022224774,
|
|
48
|
+
264347078,
|
|
49
|
+
604807628,
|
|
50
|
+
770255983,
|
|
51
|
+
1249150122,
|
|
52
|
+
1555081692,
|
|
53
|
+
1996064986,
|
|
54
|
+
2554220882,
|
|
55
|
+
2821834349,
|
|
56
|
+
2952996808,
|
|
57
|
+
3210313671,
|
|
58
|
+
3336571891,
|
|
59
|
+
3584528711,
|
|
60
|
+
113926993,
|
|
61
|
+
338241895,
|
|
62
|
+
666307205,
|
|
63
|
+
773529912,
|
|
64
|
+
1294757372,
|
|
65
|
+
1396182291,
|
|
66
|
+
1695183700,
|
|
67
|
+
1986661051,
|
|
68
|
+
2177026350,
|
|
69
|
+
2456956037,
|
|
70
|
+
2730485921,
|
|
71
|
+
2820302411,
|
|
72
|
+
3259730800,
|
|
73
|
+
3345764771,
|
|
74
|
+
3516065817,
|
|
75
|
+
3600352804,
|
|
76
|
+
4094571909,
|
|
77
|
+
275423344,
|
|
78
|
+
430227734,
|
|
79
|
+
506948616,
|
|
80
|
+
659060556,
|
|
81
|
+
883997877,
|
|
82
|
+
958139571,
|
|
83
|
+
1322822218,
|
|
84
|
+
1537002063,
|
|
85
|
+
1747873779,
|
|
86
|
+
1955562222,
|
|
87
|
+
2024104815,
|
|
88
|
+
2227730452,
|
|
89
|
+
2361852424,
|
|
90
|
+
2428436474,
|
|
91
|
+
2756734187,
|
|
92
|
+
3204031479,
|
|
93
|
+
3329325298
|
|
94
|
+
]);
|
|
95
|
+
var utf8 = new TextEncoder;
|
|
96
|
+
var rotate = (x, n) => x >>> n | x << 32 - n;
|
|
97
|
+
function sha256(text) {
|
|
98
|
+
const data = utf8.encode(text);
|
|
99
|
+
const size = data.length + 72 >> 6 << 6;
|
|
100
|
+
const bytes = new Uint8Array(size);
|
|
101
|
+
bytes.set(data);
|
|
102
|
+
bytes[data.length] = 128;
|
|
103
|
+
const view = new DataView(bytes.buffer);
|
|
104
|
+
view.setUint32(size - 8, Math.floor(data.length / 536870912));
|
|
105
|
+
view.setUint32(size - 4, data.length << 3 >>> 0);
|
|
106
|
+
const h = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
|
|
107
|
+
const w = new Uint32Array(64);
|
|
108
|
+
for (let offset = 0;offset < size; offset += 64) {
|
|
109
|
+
for (let i = 0;i < 16; i += 1)
|
|
110
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
111
|
+
for (let i = 16;i < 64; i += 1)
|
|
112
|
+
w[i] = w[i - 16] + (rotate(w[i - 15], 7) ^ rotate(w[i - 15], 18) ^ w[i - 15] >>> 3) + w[i - 7] + (rotate(w[i - 2], 17) ^ rotate(w[i - 2], 19) ^ w[i - 2] >>> 10) | 0;
|
|
113
|
+
let [a, b, c, d, e, f, g, k] = h;
|
|
114
|
+
for (let i = 0;i < 64; i += 1) {
|
|
115
|
+
const t1 = k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + (e & f ^ ~e & g) + SHA256_ROUNDS[i] + w[i] | 0;
|
|
116
|
+
const t2 = (rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
|
|
117
|
+
k = g;
|
|
118
|
+
g = f;
|
|
119
|
+
f = e;
|
|
120
|
+
e = d + t1 | 0;
|
|
121
|
+
d = c;
|
|
122
|
+
c = b;
|
|
123
|
+
b = a;
|
|
124
|
+
a = t1 + t2 | 0;
|
|
125
|
+
}
|
|
126
|
+
h[0] += a;
|
|
127
|
+
h[1] += b;
|
|
128
|
+
h[2] += c;
|
|
129
|
+
h[3] += d;
|
|
130
|
+
h[4] += e;
|
|
131
|
+
h[5] += f;
|
|
132
|
+
h[6] += g;
|
|
133
|
+
h[7] += k;
|
|
134
|
+
}
|
|
135
|
+
return Array.from(h, (x) => x.toString(16).padStart(8, "0")).join("");
|
|
136
|
+
}
|
|
137
|
+
function shortHash(json, chars) {
|
|
138
|
+
return sha256(json).slice(0, chars);
|
|
139
|
+
}
|
|
140
|
+
function quickHash(text) {
|
|
141
|
+
let a = 3735928559 ^ text.length;
|
|
142
|
+
let b = 1103547991 ^ text.length;
|
|
143
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
144
|
+
const code = text.charCodeAt(i);
|
|
145
|
+
a = Math.imul(a ^ code, 2654435761);
|
|
146
|
+
b = Math.imul(b ^ code, 1597334677);
|
|
147
|
+
}
|
|
148
|
+
const high = Math.imul(a ^ a >>> 16, 2246822507) ^ Math.imul(b ^ b >>> 13, 3266489909);
|
|
149
|
+
const low = Math.imul(b ^ b >>> 16, 2246822507) ^ Math.imul(high ^ high >>> 13, 3266489909);
|
|
150
|
+
return `${(low >>> 0).toString(36)}${(high >>> 0).toString(36)}`;
|
|
151
|
+
}
|
|
152
|
+
function truncate(text, max) {
|
|
153
|
+
const cut = text.slice(0, max);
|
|
154
|
+
return /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
155
|
+
}
|
|
156
|
+
function clip(value, max) {
|
|
157
|
+
const text = String(value).replace(/\s+/g, " ").trim();
|
|
158
|
+
return text.length > max ? `${truncate(text, max)}…` : text;
|
|
159
|
+
}
|
|
160
|
+
function wellFormed(_key, value) {
|
|
161
|
+
return typeof value === "string" ? value.toWellFormed() : value;
|
|
162
|
+
}
|
|
163
|
+
function describeToolCall(block) {
|
|
164
|
+
const input = block.input ?? {};
|
|
165
|
+
if (typeof input.command === "string")
|
|
166
|
+
return `${block.name} \`${clip(input.command, 100)}\``;
|
|
167
|
+
if (typeof input.file_path === "string")
|
|
168
|
+
return `${block.name} ${input.file_path}`;
|
|
169
|
+
if (typeof input.pattern === "string")
|
|
170
|
+
return `${block.name} "${clip(input.pattern, 60)}"`;
|
|
171
|
+
return `${block.name} ${clip(JSON.stringify(input), 60)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/context/prompts.ts
|
|
175
|
+
var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
176
|
+
var SYSTEM_PROMPT = [
|
|
177
|
+
"# Earlier sessions in this project",
|
|
178
|
+
"",
|
|
179
|
+
"eunoe keeps the full record of this project's earlier Claude Code sessions, and the user expects you to use it the way a colleague who was there would. When they point you at one, skipping it is the failure to avoid: you will redo finished work, break rules the user already set, and ask for things they already told you.",
|
|
180
|
+
"",
|
|
181
|
+
"## When",
|
|
182
|
+
"",
|
|
183
|
+
'A message carrying `{ee:continue:<id>}` already has that session attached to it as a `<handoff>` block: read it as part of the message, nothing to run. Use the commands below when the user names a session without the marker (`eunoe load <id>`), or when they refer to earlier work you have no context for: a plan, a todo, "the thing we talked about", "where we left off". `eunoe sessions <key words>` lists the closest earlier sessions, newest first, each with its first and last request; if one clearly continues into this message, load it, otherwise ask which they mean. Do not go looking for work that is clearly new and unrelated.',
|
|
184
|
+
"",
|
|
185
|
+
"## How",
|
|
186
|
+
"",
|
|
187
|
+
"1. `eunoe load <id>` writes that whole session to a file and prints its path. Read the file with the Read tool from the first line to the last, in as many Read calls as its length needs, before you act. Do not grep, head, tail or sed it, and do not stop partway: the rules, values and open todos you need are spread across all of it.",
|
|
188
|
+
"2. Only then start the work. Follow what the record establishes: the user's rules and preferences, decisions made, what was finished and what was left for later. Its values (ids, versions, names, commands) are exact.",
|
|
189
|
+
"",
|
|
190
|
+
"`eunoe peek <id> <words>` returns just the excerpts of a session that match, for a single fact such as an error message or a value. It is not a substitute for loading when the message continues that session's work.",
|
|
191
|
+
"",
|
|
192
|
+
"Loading or peeking needs no permission and no announcement: run it and carry on with the request."
|
|
193
|
+
].join(`
|
|
194
|
+
`);
|
|
195
|
+
var HANDOFF_PREAMBLE = "What follows is carried over from an earlier session on this project so you can pick the work up without being told again: the user's messages, the key output lines and the agent's replies, in order. Treat the rules and preferences in it as still in force, and the values in it (ids, versions, names, commands) as exact. Its full tool output is not here but stays reachable two ways: when a later message touches it, the matching excerpts are attached to that message as recalled context, labelled with this session's title; and for a specific value you can run `eunoe peek <id> <words>` yourself.";
|
|
196
|
+
function handoffBlock(label, text) {
|
|
197
|
+
return `<handoff from="${label}">
|
|
198
|
+
${HANDOFF_PREAMBLE}
|
|
199
|
+
|
|
200
|
+
${text}
|
|
201
|
+
</handoff>`;
|
|
202
|
+
}
|
|
203
|
+
function recalledContext(entries, fromEarlierSessions) {
|
|
204
|
+
const where = fromEarlierSessions ? "earlier in this session or in an earlier session on this project" : "earlier in this session";
|
|
205
|
+
return `<recalled-context>
|
|
206
|
+
Excerpts from ${where} that match this message: tool output that is no longer in your context, quoted exactly, oldest first. Where the same thing appears twice, the later turn is current. Use them if they answer what was asked; ignore them otherwise.
|
|
207
|
+
|
|
208
|
+
${entries.join(`
|
|
209
|
+
`)}
|
|
210
|
+
</recalled-context>`;
|
|
211
|
+
}
|
|
212
|
+
function rerankQuestion(query) {
|
|
213
|
+
return `Which excerpt contains the exact answer to the developer's question about this coding session: "${query}"?`;
|
|
214
|
+
}
|
|
215
|
+
function toolEvidence(entries) {
|
|
216
|
+
return `<tool-evidence>
|
|
217
|
+
Key lines of command output from this turn, kept verbatim (the full output is in the transcript):
|
|
218
|
+
${entries.join(`
|
|
219
|
+
`)}
|
|
220
|
+
</tool-evidence>`;
|
|
221
|
+
}
|
|
222
|
+
function interruptedReply(lastWords, calls) {
|
|
223
|
+
const shown = calls.slice(-20);
|
|
224
|
+
return [
|
|
225
|
+
"[This turn was interrupted before a final reply.]",
|
|
226
|
+
lastWords ? `Last thing you said: ${lastWords.length > 600 ? `${truncate(lastWords, 600)}…` : lastWords}` : "",
|
|
227
|
+
calls.length > 0 ? `Tool calls (${calls.length}${calls.length > shown.length ? `, last ${shown.length} shown` : ""}): ${shown.join("; ")}` : "",
|
|
228
|
+
"Their output is in the full transcript."
|
|
229
|
+
].filter(Boolean).join(`
|
|
230
|
+
`);
|
|
231
|
+
}
|
|
232
|
+
function clearedOutput(dir) {
|
|
233
|
+
return `[Output cleared to keep the context within its limit. It is in the full transcript${dir ? ` at ${dir}` : ""}: search there for this call.]`;
|
|
234
|
+
}
|
|
235
|
+
function clearedInput(kept, cleared) {
|
|
236
|
+
return `${kept}… [${cleared} chars cleared; in the transcript]`;
|
|
237
|
+
}
|
|
238
|
+
function compactionNotice({ omitted, kept, pinned = 0, guide }) {
|
|
239
|
+
return [
|
|
240
|
+
"<context-compacted>",
|
|
241
|
+
"Your context was compacted here, so tool calls and their output from before this point were removed.",
|
|
242
|
+
omitted > 0 ? `${omitted} of the oldest turns didn't fit and were removed entirely. The ${plural(kept, "turn")} after them are shown as the user's message and your final reply.` : kept > 0 ? "Every earlier turn is shown below as the user's message and your final reply." : "",
|
|
243
|
+
pinned > 0 ? `${pinned === 1 ? "One earlier turn was" : `${pinned} earlier turns were`} kept whole, with their tool calls and output, because they matter to what comes next.` : "",
|
|
244
|
+
"The turn that was in progress when the context filled up follows in full. Continue from it.",
|
|
245
|
+
"Instructions and preferences the user gave in earlier turns still apply: how they want things done, and what to do or not do on your own. Look back over their messages before you act.",
|
|
246
|
+
"For an exact detail from before this point (an ID, a path, a command, an error, a value, what a tool printed), look it up instead of going from memory. A lookup is cheap; a wrong detail is not. Never say you don't know about something earlier in this session without searching for it first, and if what you remember contradicts what the user says, search before telling them they are wrong.",
|
|
247
|
+
guide ?? "",
|
|
248
|
+
"</context-compacted>"
|
|
249
|
+
].filter(Boolean).join(`
|
|
250
|
+
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
function truncationNotice({ omitted, kept, guide }) {
|
|
254
|
+
return [
|
|
255
|
+
"<context-truncated>",
|
|
256
|
+
`${omitted} earlier turn${omitted === 1 ? " was" : "s were"} dropped to fit the context window. What follows is the most recent ${plural(kept, "turn")}, unchanged.`,
|
|
257
|
+
"Instructions and preferences the user gave in earlier turns still apply.",
|
|
258
|
+
guide ?? "",
|
|
259
|
+
"</context-truncated>"
|
|
260
|
+
].filter(Boolean).join(`
|
|
261
|
+
|
|
262
|
+
`);
|
|
263
|
+
}
|
|
264
|
+
function trimInstructions(guide) {
|
|
265
|
+
return [
|
|
266
|
+
"# Context between turns",
|
|
267
|
+
"",
|
|
268
|
+
"This session trims its context between turns: of every earlier turn you keep only the user's message (with anything that arrived with it) and your final message. Everything in between is gone when the next turn starts: tool calls and their results, files you read, command output, your thinking, and anything you wrote along the way. Within a turn, nothing is trimmed.",
|
|
269
|
+
"",
|
|
270
|
+
"So your final message is the only memory you carry forward. End each turn with a final message that lets you pick the work back up cold:",
|
|
271
|
+
"- What changed: files with their paths, branches, commits, PRs, anything created, moved or deleted.",
|
|
272
|
+
"- What you found: facts you would otherwise have to dig up again. Where code lives, how something works, the cause of a bug, exact error text, IDs, URLs, the numbers that matter.",
|
|
273
|
+
"- Decisions and their reasons, including approaches you tried and ruled out.",
|
|
274
|
+
"- Where things stand: what is done, what is not, what you verified and how, and what is waiting on the user.",
|
|
275
|
+
"",
|
|
276
|
+
'It is still your reply to the user, so write it for them, but leave out nothing you will need. State things outright instead of pointing back at tool output: "as shown above" means nothing next turn. One exact line (a path, a command, a value) beats a paragraph describing it. A turn that ends on a question still needs the state written down. A trivial turn (a quick answer, small talk) needs no state.',
|
|
277
|
+
"",
|
|
278
|
+
"Instructions and preferences the user gave in earlier turns still apply: how they want things done, and what to do or not do on your own.",
|
|
279
|
+
...guide ? [
|
|
280
|
+
"",
|
|
281
|
+
"Nothing is lost. What you see of earlier turns is only their outcome. For an exact detail from an earlier turn (an ID, a path, a command you ran, what it printed, a file's earlier content, an error), look it up in the full transcript instead of going from memory or redoing the work. A lookup is cheap; a wrong detail is not.",
|
|
282
|
+
"",
|
|
283
|
+
guide,
|
|
284
|
+
"",
|
|
285
|
+
"What you read from it stays in context for this turn only. If you will need it again later, put it in your final message."
|
|
286
|
+
] : []
|
|
287
|
+
].join(`
|
|
288
|
+
`);
|
|
289
|
+
}
|
|
290
|
+
function searchOnlyInstructions(guide) {
|
|
291
|
+
return [
|
|
292
|
+
"# The full transcript",
|
|
293
|
+
"",
|
|
294
|
+
"Earlier parts of this session may be summarised or missing from your context. The complete record is on disk, and for any exact detail from earlier (an ID, a path, a command you ran, what it printed, an error, a value) you should look it up there rather than answer from memory. A lookup is cheap; a wrong detail is not. Never say you don't know about something earlier in this session without searching for it first.",
|
|
295
|
+
"",
|
|
296
|
+
guide
|
|
297
|
+
].join(`
|
|
298
|
+
`);
|
|
299
|
+
}
|
|
300
|
+
function searchGuide(ref) {
|
|
301
|
+
if (ref.mode === "jsonl") {
|
|
302
|
+
return [
|
|
303
|
+
`The full transcript is Claude Code's session file: ${ref.jsonl}`,
|
|
304
|
+
"It is JSONL, one entry per line. Your tool calls are `tool_use` blocks in `assistant` entries; their output is in the `tool_result` blocks of the `user` entries that follow. It is large: search it narrowly and never read it whole. For example:",
|
|
305
|
+
` grep -n 'distinctive text' ${ref.jsonl} | cut -c1-400`,
|
|
306
|
+
` jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use") | "\\(.name) \\(.input | tostring | .[0:200])"' ${ref.jsonl}`
|
|
307
|
+
].join(`
|
|
308
|
+
`);
|
|
309
|
+
}
|
|
310
|
+
const layout = ref.mode === "xml" ? [
|
|
311
|
+
`The full transcript is a folder with one XML file per turn: ${ref.dir}`,
|
|
312
|
+
"- `index.md`: one line per turn, what the user asked and how the turn ended. Skim it to find the turn you need.",
|
|
313
|
+
"- `turn-0001.xml`, …: one turn each, `<turn n>` with `<user>` (the message), `<said>` (what you wrote while working), `<tool n name>` holding `<call>`, `<input>` and `<output>`, and `<final_reply>`.",
|
|
314
|
+
"- `turn-0001-subagent-1.xml`, …: the whole run of a subagent launched in that turn, same layout. The turn itself only has the subagent's report; what the subagent saw and ran is here."
|
|
315
|
+
] : [
|
|
316
|
+
`The full transcript is a folder of markdown files: ${ref.dir}`,
|
|
317
|
+
"- `index.md`: one line per turn, what the user asked and how the turn ended. Skim it to find the turn you need.",
|
|
318
|
+
"- `turn-0001.md`, …: one turn each. `## User` (the message), `### [tool 3/12] Bash: …` for every tool call with `[output of tool 3]` below it, and `## Final reply`.",
|
|
319
|
+
"- `turn-0001-subagent-1.md`, …: the whole run of a subagent launched in that turn, same layout. The turn itself only has the subagent's report; what the subagent saw and ran is here."
|
|
320
|
+
];
|
|
321
|
+
const habits = "Search for more than one phrasing (the exact term, a synonym, a file or function name, an error string) before you conclude something isn't there. Read the whole turn file around a hit: the answer is often a few calls before or after it.";
|
|
322
|
+
if (ref.mode === "qmd") {
|
|
323
|
+
return [
|
|
324
|
+
...layout,
|
|
325
|
+
"It is indexed with qmd. Run qmd from inside that folder:",
|
|
326
|
+
` cd ${ref.dir} && qmd query "a natural-language question" # semantic + keyword, best when you don't know the exact words (5–20s)`,
|
|
327
|
+
` cd ${ref.dir} && qmd search "exact words" # keyword only, instant`,
|
|
328
|
+
` then Read ${ref.dir}/turn-NNNN.md (or qmd get turn-NNNN.md)`,
|
|
329
|
+
"If qmd isn't ready yet (the index builds in the background), grep the folder instead.",
|
|
330
|
+
habits
|
|
331
|
+
].join(`
|
|
332
|
+
`);
|
|
333
|
+
}
|
|
334
|
+
const ext = ref.mode === "xml" ? "xml" : "md";
|
|
335
|
+
return [
|
|
336
|
+
...layout,
|
|
337
|
+
"To find something:",
|
|
338
|
+
` grep -ril 'distinctive words' ${ref.dir} # which turns mention it`,
|
|
339
|
+
` grep -n -i 'distinctive words' ${ref.dir}/*.${ext} | cut -c1-300`,
|
|
340
|
+
` then Read ${ref.dir}/turn-NNNN.${ext}`,
|
|
341
|
+
habits
|
|
342
|
+
].join(`
|
|
343
|
+
`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/context/recall/rerank.ts
|
|
347
|
+
var TIMEOUT_MS = 4000;
|
|
348
|
+
function rerankState(chunks, candidates) {
|
|
349
|
+
return candidates.map((index, n) => {
|
|
350
|
+
const chunk = chunks[index];
|
|
351
|
+
return `### ${n}
|
|
352
|
+
[turn ${chunk.turn}] ${chunk.call}
|
|
353
|
+
${chunk.lines.map((l) => l.slice(0, 240)).join(`
|
|
354
|
+
`)}`;
|
|
355
|
+
}).join(`
|
|
356
|
+
|
|
357
|
+
`);
|
|
358
|
+
}
|
|
359
|
+
function orderByProbability(candidates, probabilities) {
|
|
360
|
+
return candidates.map((_, n) => n).sort((a, b) => (probabilities[String(b)] ?? 0) - (probabilities[String(a)] ?? 0) || a - b).map((n) => candidates[n]);
|
|
361
|
+
}
|
|
362
|
+
function failureReason(error) {
|
|
363
|
+
if (!(error instanceof Error))
|
|
364
|
+
return String(error);
|
|
365
|
+
return error.name === "TimeoutError" ? `timeout after ${TIMEOUT_MS}ms` : error.message;
|
|
366
|
+
}
|
|
367
|
+
function jevReranker(model, key, endpoint = "https://api.typesafe.ai/v1/systemone", onError) {
|
|
368
|
+
return async (query, chunks, candidates) => {
|
|
369
|
+
if (candidates.length < 2)
|
|
370
|
+
return candidates;
|
|
371
|
+
try {
|
|
372
|
+
const response = await fetch(endpoint, {
|
|
373
|
+
method: "POST",
|
|
374
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
375
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
376
|
+
body: JSON.stringify({
|
|
377
|
+
model,
|
|
378
|
+
state: rerankState(chunks, candidates),
|
|
379
|
+
questions: {
|
|
380
|
+
where: {
|
|
381
|
+
type: "choice",
|
|
382
|
+
instructions: rerankQuestion(query),
|
|
383
|
+
criteria: Object.fromEntries(candidates.map((_, n) => [String(n), null]))
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
});
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
const body = await response.text().catch(() => "");
|
|
390
|
+
onError?.(`${response.status} ${/error_type"\s*:\s*"([^"]+)/.exec(body)?.[1] ?? body.slice(0, 60)}`.trim());
|
|
391
|
+
return candidates;
|
|
392
|
+
}
|
|
393
|
+
const probabilities = (await response.json()).answers?.where?.probabilities;
|
|
394
|
+
return probabilities && typeof probabilities === "object" ? orderByProbability(candidates, probabilities) : candidates;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
onError?.(failureReason(error));
|
|
397
|
+
return candidates;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
// src/core/config.ts
|
|
402
|
+
var CORE_DEFAULTS = {
|
|
403
|
+
mode: "default",
|
|
404
|
+
compactAt: 0.9,
|
|
405
|
+
keepTurns: "all",
|
|
406
|
+
keep: 3,
|
|
407
|
+
budget: 1e5,
|
|
408
|
+
turnCap: 1e5
|
|
409
|
+
};
|
|
410
|
+
// src/context/fold/evidence.ts
|
|
411
|
+
var REREADABLE_TOOLS = new Set(["Read", "Grep", "Glob", "LS", "NotebookRead", "WebFetch", "WebSearch", "TodoWrite", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
412
|
+
var ERROR_LINE = /\b(?:error|errors|fail|fails|failed|failure|fatal|exception|panic|denied|refused|timed? ?out|traceback|cannot|unable to)\b/i;
|
|
413
|
+
var ERROR_CODE = /\b[A-Z][A-Z0-9]*_[A-Z0-9_]{2,}\b|\bSIG[A-Z]{2,}\d*\b|\b[A-Z]{2,}\d{2,}\b|\bE(?:NOENT|ACCES|PERM|EXIST|CONN(?:REFUSED|RESET|ABORTED)|PIPE|ADDRINUSE|TIMEDOUT|NOTFOUND|INVAL|BUSY|MFILE|AGAIN|HOSTUNREACH|NOTDIR|ISDIR|BADF|NOSPC|AI_[A-Z]+)\b/;
|
|
414
|
+
var VALUE_TOKEN = /\b[a-z]{2,}[_-](?=[a-z0-9]*\d)[a-z0-9]{6,}\b|\b(?=[0-9a-f]*\d)(?=[0-9a-f]*[a-f])[0-9a-f]{7,}\b|https?:\/\/[^\s)"']+|\b\d{4,}\b|\b\d{6,}_[a-z0-9_]{3,}\b|\bv?\d+\.\d+\.\d+\b|\b\d+(?:\.\d+)?\s?(?:ms|s|[KMGT]i?B|kB|%)(?![a-z])|\bsha\d*:[0-9a-f]+|@[a-z][\w-]+|--[a-z][\w-]*=\S+|\b\d+ (?:pass(?:ed|ing)?|fail(?:ed|ing)?|tests?|files? changed|insertions?|deletions?|rows?|errors?|warnings?)\b|[$€£]\d+(?:[.,]\d+)?|\b(?:status|HTTP\/\d(?:\.\d)?)\s*[:=]?\s*[45]\d{2}\b|\bexit(?: code| status)?\s*[:=]?\s*(?!0\b)\d{1,3}\b/i;
|
|
415
|
+
var ID_TOKEN = /\b[a-z]{2,}[_-](?=[a-z0-9]*\d)[a-z0-9]{6,}\b|\b(?=[0-9a-f]*\d)(?=[0-9a-f]*[a-f])[0-9a-f]{7,}\b|https?:\/\/[^\s)"']+|\b\d{6,}_[a-z0-9_]{3,}\b|\bv?\d+\.\d+\.\d+\b|\bsha\d*:[0-9a-f]+|@[a-z][\w-]+|--[a-z][\w-]*=\S+/i;
|
|
416
|
+
var KEY_VALUE = /^[\w .\-/()]{2,48}[:=]\s*\S/;
|
|
417
|
+
var VALUE_LABEL = /\b(?:ids?|hash|sha(?:256)?|checksum|trace|deploy(?:ment)?|preview|url|version|rows|p9[59]|exit(?: code)?|signal|request[_-]?id|job|migration|applied|bundle|build|status|took|updated|created)\b/i;
|
|
418
|
+
function resolved(options) {
|
|
419
|
+
return {
|
|
420
|
+
linesPerCall: options.linesPerCall ?? 12,
|
|
421
|
+
lineChars: options.lineChars ?? 220,
|
|
422
|
+
turnChars: options.turnChars ?? 4000,
|
|
423
|
+
scanLines: options.scanLines ?? 1e6,
|
|
424
|
+
minScore: options.minScore ?? 2,
|
|
425
|
+
ranked: options.ranked ?? true,
|
|
426
|
+
labels: options.labels ?? true,
|
|
427
|
+
callFloor: options.callFloor ?? 2
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
var isError = (line) => ERROR_LINE.test(line) || ERROR_CODE.test(line);
|
|
431
|
+
function isSalient(line) {
|
|
432
|
+
return isError(line) || VALUE_TOKEN.test(line);
|
|
433
|
+
}
|
|
434
|
+
function scoreLines(text, options) {
|
|
435
|
+
const { scanLines, lineChars, minScore, labels } = resolved(options);
|
|
436
|
+
const scored = text.split(`
|
|
437
|
+
`).slice(0, scanLines).flatMap((raw, index) => {
|
|
438
|
+
const line = raw.trim();
|
|
439
|
+
if (line.length < 4)
|
|
440
|
+
return [];
|
|
441
|
+
const error = isError(line);
|
|
442
|
+
const value = VALUE_TOKEN.test(line);
|
|
443
|
+
if (!error && !value)
|
|
444
|
+
return [];
|
|
445
|
+
const score = (error ? 3 : 0) + (value ? ID_TOKEN.test(line) ? 3 : 2 : 0) + (KEY_VALUE.test(line) ? 1 : 0) + (line.length < 120 ? 1 : 0) + (labels && VALUE_LABEL.test(line) ? 1 : 0);
|
|
446
|
+
return score >= minScore ? [{ index, line: clip(line, lineChars), score }] : [];
|
|
447
|
+
});
|
|
448
|
+
const seen = new Set;
|
|
449
|
+
return scored.sort((a, b) => b.score - a.score || a.index - b.index).filter((s) => !seen.has(s.line) && seen.add(s.line));
|
|
450
|
+
}
|
|
451
|
+
function salientLines(text, options = {}) {
|
|
452
|
+
return scoreLines(text, options).slice(0, resolved(options).linesPerCall).sort((a, b) => a.index - b.index).map((s) => s.line);
|
|
453
|
+
}
|
|
454
|
+
function toolPairs(turn) {
|
|
455
|
+
const results = new Map;
|
|
456
|
+
for (const message of turn) {
|
|
457
|
+
if (message.role !== "user")
|
|
458
|
+
continue;
|
|
459
|
+
for (const block of blocks(message))
|
|
460
|
+
if (block.type === "tool_result")
|
|
461
|
+
results.set(block.tool_use_id, block);
|
|
462
|
+
}
|
|
463
|
+
return turn.filter((m) => m.role === "assistant").flatMap((m) => blocks(m).filter((b) => b.type === "tool_use")).flatMap((call) => {
|
|
464
|
+
const result = results.get(call.id);
|
|
465
|
+
return result ? [{ call, result }] : [];
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function render(entries) {
|
|
469
|
+
const shown = entries.filter((e) => e.lines.length > 0).map((e) => `- ${e.head}:
|
|
470
|
+
${e.lines.map((l) => ` ${l}`).join(`
|
|
471
|
+
`)}`);
|
|
472
|
+
return shown.length > 0 ? toolEvidence(shown) : null;
|
|
473
|
+
}
|
|
474
|
+
function inCallOrder(heads, pairs, options) {
|
|
475
|
+
const { turnChars } = resolved(options);
|
|
476
|
+
const entries = [];
|
|
477
|
+
let used = 0;
|
|
478
|
+
pairs.forEach(({ result }, i) => {
|
|
479
|
+
const lines = salientLines(resultText(result), options);
|
|
480
|
+
if (lines.length === 0)
|
|
481
|
+
return;
|
|
482
|
+
const size = heads[i].length + 4 + lines.reduce((s, l) => s + l.length + 5, 0);
|
|
483
|
+
if (used + size > turnChars)
|
|
484
|
+
return;
|
|
485
|
+
entries.push({ head: heads[i], lines });
|
|
486
|
+
used += size;
|
|
487
|
+
});
|
|
488
|
+
return entries;
|
|
489
|
+
}
|
|
490
|
+
function byScore(heads, pairs, options) {
|
|
491
|
+
const { turnChars, linesPerCall, callFloor } = resolved(options);
|
|
492
|
+
const perCall = pairs.map(({ result }) => scoreLines(resultText(result), options));
|
|
493
|
+
const chosen = new Map;
|
|
494
|
+
let used = 0;
|
|
495
|
+
const take = (call, line) => {
|
|
496
|
+
const bucket = chosen.get(call) ?? [];
|
|
497
|
+
const cost = line.line.length + 5 + (bucket.length > 0 ? 0 : heads[call].length + 4);
|
|
498
|
+
if (bucket.length >= linesPerCall || used + cost > turnChars)
|
|
499
|
+
return;
|
|
500
|
+
bucket.push(line);
|
|
501
|
+
chosen.set(call, bucket);
|
|
502
|
+
used += cost;
|
|
503
|
+
};
|
|
504
|
+
for (const [call, lines] of perCall.entries())
|
|
505
|
+
for (const line of lines.slice(0, callFloor))
|
|
506
|
+
take(call, line);
|
|
507
|
+
const rest = perCall.flatMap((lines, call) => lines.slice(callFloor).map((line) => ({ line, call }))).sort((a, b) => b.line.score - a.line.score || a.call - b.call || a.line.index - b.line.index);
|
|
508
|
+
for (const { line, call } of rest)
|
|
509
|
+
take(call, line);
|
|
510
|
+
return heads.map((head, i) => ({ head, lines: (chosen.get(i) ?? []).sort((a, b) => a.index - b.index).map((s) => s.line) }));
|
|
511
|
+
}
|
|
512
|
+
function turnEvidence(turn, options = {}) {
|
|
513
|
+
const pairs = toolPairs(turn).filter(({ call }) => options.allTools || !REREADABLE_TOOLS.has(call.name));
|
|
514
|
+
const heads = pairs.map(({ call, result }) => `${describeToolCall(call)}${result.is_error ? " (failed)" : ""}`);
|
|
515
|
+
return render(resolved(options).ranked ? byScore(heads, pairs, options) : inCallOrder(heads, pairs, options));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/context/fold/turns.ts
|
|
519
|
+
var IMAGE_NOTE = /^\s*\[Image: original \d+x\d+, displayed at \d+x\d+\.[^\]]*\]\s*$/;
|
|
520
|
+
var STALE_NOTE = /changed on disk since you last read it|was modified, either by the user or by a linter/;
|
|
521
|
+
var isStaleNote = (text) => STALE_NOTE.test(String(text ?? ""));
|
|
522
|
+
var isImageNote = (content) => content.length > 0 && content.every((block) => block.type === "text" && IMAGE_NOTE.test(String(block.text ?? "")));
|
|
523
|
+
function isPrompt(message) {
|
|
524
|
+
if (message.role !== "user")
|
|
525
|
+
return false;
|
|
526
|
+
const content = blocks(message);
|
|
527
|
+
if (isImageNote(content))
|
|
528
|
+
return false;
|
|
529
|
+
return !content.some((block) => block.type === "tool_result") && content.some((block) => block.type === "text" || block.type === "image" || block.type === "document");
|
|
530
|
+
}
|
|
531
|
+
function splitTurns(messages) {
|
|
532
|
+
const preamble = [];
|
|
533
|
+
const turns = [];
|
|
534
|
+
for (const message of messages) {
|
|
535
|
+
const current = turns.at(-1);
|
|
536
|
+
if (isPrompt(message) && !current?.every((m) => m.role === "user"))
|
|
537
|
+
turns.push([message]);
|
|
538
|
+
else if (current)
|
|
539
|
+
current.push(message);
|
|
540
|
+
else
|
|
541
|
+
preamble.push(message);
|
|
542
|
+
}
|
|
543
|
+
return { preamble, turns };
|
|
544
|
+
}
|
|
545
|
+
function turnFingerprint(turn) {
|
|
546
|
+
return shortHash(JSON.stringify(blocks(turn[0]).map(withoutCacheControl)), 24);
|
|
547
|
+
}
|
|
548
|
+
function isHarness(message) {
|
|
549
|
+
return message.role === "system";
|
|
550
|
+
}
|
|
551
|
+
function hasContent(message) {
|
|
552
|
+
return blocks(message).some((block) => block.type !== "text" || String(block.text ?? "").trim());
|
|
553
|
+
}
|
|
554
|
+
var hasText = (block) => block.type === "text" && String(block.text ?? "").trim() !== "";
|
|
555
|
+
function mergedHarness(turn) {
|
|
556
|
+
const seen = new Set;
|
|
557
|
+
const fresh = (block) => {
|
|
558
|
+
if (block.type !== "text")
|
|
559
|
+
return true;
|
|
560
|
+
const text = String(block.text ?? "");
|
|
561
|
+
if (isStaleNote(text) || seen.has(text))
|
|
562
|
+
return false;
|
|
563
|
+
seen.add(text);
|
|
564
|
+
return true;
|
|
565
|
+
};
|
|
566
|
+
const content = turn.filter((m) => isHarness(m) && hasContent(m)).flatMap((m) => blocks(m).map(withoutCacheControl)).filter(fresh);
|
|
567
|
+
return content.length > 0 ? [{ role: "system", content }] : [];
|
|
568
|
+
}
|
|
569
|
+
function interrupted(turn) {
|
|
570
|
+
const assistant = turn.filter((m) => m.role === "assistant");
|
|
571
|
+
const lastWords = assistant.map((m) => textOf(m).trim()).filter(Boolean).at(-1);
|
|
572
|
+
const calls = assistant.flatMap((m) => blocks(m).filter((b) => b.type === "tool_use")).map(describeToolCall);
|
|
573
|
+
return interruptedReply(lastWords, calls);
|
|
574
|
+
}
|
|
575
|
+
function carriedText(messages) {
|
|
576
|
+
return messages.filter((m) => m.role === "user").flatMap((m) => blocks(m).filter((b) => hasText(b) && !IMAGE_NOTE.test(String(b.text)) && !/^\s*(?:<system-reminder>[\s\S]*?<\/system-reminder>\s*)+$/.test(String(b.text)))).map((b) => ({ type: "text", text: b.text }));
|
|
577
|
+
}
|
|
578
|
+
function reduceTurn(turn, options = {}) {
|
|
579
|
+
const firstReply = turn.findIndex((m) => m.role === "assistant");
|
|
580
|
+
const opening = (firstReply === -1 ? turn : turn.slice(0, firstReply)).filter((m) => m.role === "user").map(clean);
|
|
581
|
+
const carried = carriedText(firstReply === -1 ? [] : turn.slice(firstReply));
|
|
582
|
+
if (carried.length > 0)
|
|
583
|
+
opening.push({ role: "user", content: carried });
|
|
584
|
+
const evidence = options.evidence ? turnEvidence(turn) : null;
|
|
585
|
+
if (evidence)
|
|
586
|
+
opening.push({ role: "user", content: [{ type: "text", text: evidence }] });
|
|
587
|
+
opening.push(...mergedHarness(turn));
|
|
588
|
+
const last = turn.findLast((m) => m.role !== "system");
|
|
589
|
+
const content = last?.role === "assistant" ? blocks(last) : [];
|
|
590
|
+
const text = content.filter(hasText);
|
|
591
|
+
const final = text.length > 0 && !content.some((b) => b.type === "tool_use") ? text.map((b) => ({ type: "text", text: b.text })) : [{ type: "text", text: interrupted(turn) }];
|
|
592
|
+
return [...opening, { role: "assistant", content: final }];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/context/fold/strategies.ts
|
|
596
|
+
var pinnedBy = ({ pinned }) => (turn) => pinned !== undefined && pinned.size > 0 && pinned.has(turnFingerprint(turn));
|
|
597
|
+
var countPinned = (turns, pinned) => pinned.size > 0 ? turns.filter((turn) => pinned.has(turnFingerprint(turn))).length : 0;
|
|
598
|
+
function trimMessages(messages, options = {}) {
|
|
599
|
+
const { preamble, turns } = splitTurns(messages);
|
|
600
|
+
if (turns.length < 2)
|
|
601
|
+
return messages;
|
|
602
|
+
return [...preamble, ...turns.slice(0, -1).flatMap((turn) => reduceTurn(turn, options)), ...turns.at(-1) ?? []];
|
|
603
|
+
}
|
|
604
|
+
var CARRIED_CHARS = 80000;
|
|
605
|
+
function carriedHarness(removed) {
|
|
606
|
+
const texts = removed.filter((m) => isHarness(m) && hasContent(m)).flatMap((m) => blocks(m).filter((b) => b.type === "text")).map((b) => String(b.text ?? "")).filter((text) => text.trim() && !isStaleNote(text));
|
|
607
|
+
const kept = [];
|
|
608
|
+
const seen = new Set;
|
|
609
|
+
let used = 0;
|
|
610
|
+
for (const text of texts.toReversed()) {
|
|
611
|
+
if (seen.has(text) || used + text.length > CARRIED_CHARS)
|
|
612
|
+
continue;
|
|
613
|
+
seen.add(text);
|
|
614
|
+
kept.push(text);
|
|
615
|
+
used += text.length;
|
|
616
|
+
}
|
|
617
|
+
return kept.toReversed().map((text) => ({ type: "text", text: `<system-reminder>
|
|
618
|
+
${text}
|
|
619
|
+
</system-reminder>` }));
|
|
620
|
+
}
|
|
621
|
+
var foldsOpening = (opening, keptFrom) => keptFrom === 1 && opening.length > 1;
|
|
622
|
+
function compactMessages(messages, cutIndex, options) {
|
|
623
|
+
const { preamble, turns } = splitTurns(messages);
|
|
624
|
+
if (cutIndex <= 0 || cutIndex >= turns.length)
|
|
625
|
+
return messages;
|
|
626
|
+
const reduce = options.reduce ?? reduceTurn;
|
|
627
|
+
const isPinned = pinnedBy(options);
|
|
628
|
+
const keptFrom = Math.max(1, cutIndex - options.keepTurns);
|
|
629
|
+
const [first, ...rest] = turns[0];
|
|
630
|
+
const openingPinned = isPinned(turns[0]);
|
|
631
|
+
const openingFolded = !openingPinned && foldsOpening(turns[0], keptFrom);
|
|
632
|
+
const dropped = turns.slice(1, keptFrom);
|
|
633
|
+
const whole = dropped.filter(isPinned);
|
|
634
|
+
const pinned = whole.length + turns.slice(keptFrom, cutIndex).filter(isPinned).length + (openingPinned ? 1 : 0);
|
|
635
|
+
const kept = cutIndex - keptFrom + (openingFolded ? 1 : 0);
|
|
636
|
+
const notice = compactionNotice({ omitted: dropped.length - whole.length + (options.omittedBefore ?? 0), kept, pinned, guide: options.guide });
|
|
637
|
+
const removed = [...openingPinned || openingFolded ? [] : rest, ...dropped.filter((turn) => !isPinned(turn)).flat()];
|
|
638
|
+
const shown = (turn) => isPinned(turn) ? turn.map(clean) : reduce(turn, options);
|
|
639
|
+
return [
|
|
640
|
+
...preamble,
|
|
641
|
+
first,
|
|
642
|
+
...openingPinned ? rest.map(clean) : [],
|
|
643
|
+
{ role: "user", content: [{ type: "text", text: notice }, ...carriedHarness(removed)] },
|
|
644
|
+
...openingFolded ? reduce(turns[0], options).slice(1) : [],
|
|
645
|
+
...whole.flatMap((turn) => turn.map(clean)),
|
|
646
|
+
...turns.slice(keptFrom, cutIndex).flatMap(shown),
|
|
647
|
+
...turns.slice(cutIndex).flat()
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
function capTurns(messages, options) {
|
|
651
|
+
const { capChars } = options;
|
|
652
|
+
if (!capChars)
|
|
653
|
+
return messages;
|
|
654
|
+
const { preamble, turns } = splitTurns(messages);
|
|
655
|
+
if (turns.length < 3)
|
|
656
|
+
return messages;
|
|
657
|
+
const live = turns.length - 1;
|
|
658
|
+
const isPinned = pinnedBy(options);
|
|
659
|
+
const over = (turn, i) => i > 0 && i < live && !isPinned(turn) && JSON.stringify(turn).length > capChars;
|
|
660
|
+
if (!turns.some(over))
|
|
661
|
+
return messages;
|
|
662
|
+
const reduce = options.reduce ?? reduceTurn;
|
|
663
|
+
return [...preamble, ...turns.flatMap((turn, i) => over(turn, i) ? reduce(turn, options) : turn)];
|
|
664
|
+
}
|
|
665
|
+
function tailMessages(messages, from, guide) {
|
|
666
|
+
const { preamble, turns } = splitTurns(messages);
|
|
667
|
+
if (turns.length === 0 || from <= 1)
|
|
668
|
+
return messages;
|
|
669
|
+
const notice = truncationNotice({ omitted: from - 1, kept: turns.length - from, guide });
|
|
670
|
+
return [...preamble, turns[0][0], { role: "user", content: [{ type: "text", text: notice }] }, ...turns.slice(from).flat()];
|
|
671
|
+
}
|
|
672
|
+
function countToolResults(messages, from) {
|
|
673
|
+
return messages.slice(from).filter((m) => m.role === "user").reduce((total, m) => total + blocks(m).filter((b) => b.type === "tool_result").length, 0);
|
|
674
|
+
}
|
|
675
|
+
function shortenInput(input) {
|
|
676
|
+
if (typeof input === "string")
|
|
677
|
+
return input.length > 2000 ? clearedInput(truncate(input, 500), input.length - 500) : input;
|
|
678
|
+
if (Array.isArray(input))
|
|
679
|
+
return input.map(shortenInput);
|
|
680
|
+
if (input && typeof input === "object")
|
|
681
|
+
return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, shortenInput(v)]));
|
|
682
|
+
return input;
|
|
683
|
+
}
|
|
684
|
+
function clearToolResults(messages, from, count, placeholder) {
|
|
685
|
+
let left = count;
|
|
686
|
+
const cleared = new Set;
|
|
687
|
+
const clearResult = (b) => {
|
|
688
|
+
if (b.type !== "tool_result" || left <= 0)
|
|
689
|
+
return b;
|
|
690
|
+
left -= 1;
|
|
691
|
+
cleared.add(b.tool_use_id);
|
|
692
|
+
return { type: "tool_result", tool_use_id: b.tool_use_id, ...b.is_error ? { is_error: true } : {}, content: placeholder };
|
|
693
|
+
};
|
|
694
|
+
const withResultsCleared = messages.map((message, index) => {
|
|
695
|
+
if (left <= 0 || index < from || message.role !== "user" || typeof message.content === "string")
|
|
696
|
+
return message;
|
|
697
|
+
if (!message.content.some((b) => b.type === "tool_result"))
|
|
698
|
+
return message;
|
|
699
|
+
return { ...message, content: message.content.map(clearResult) };
|
|
700
|
+
});
|
|
701
|
+
const isClearedCall = (b) => b.type === "tool_use" && cleared.has(b.id);
|
|
702
|
+
return withResultsCleared.map((message) => {
|
|
703
|
+
if (message.role !== "assistant" || typeof message.content === "string" || !message.content.some(isClearedCall))
|
|
704
|
+
return message;
|
|
705
|
+
return { ...message, content: message.content.map((b) => isClearedCall(b) ? { ...b, input: shortenInput(b.input) } : b) };
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function appendSystem(system, text) {
|
|
709
|
+
const list = typeof system === "string" ? [{ type: "text", text: system }] : [...system ?? []];
|
|
710
|
+
const lastCached = list.findLastIndex((b) => b.cache_control);
|
|
711
|
+
const cacheControl = lastCached >= 0 ? list[lastCached].cache_control : undefined;
|
|
712
|
+
if (lastCached >= 0)
|
|
713
|
+
list[lastCached] = withoutCacheControl(list[lastCached]);
|
|
714
|
+
list.push({ type: "text", text, ...cacheControl ? { cache_control: cacheControl } : {} });
|
|
715
|
+
return list;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/context/recall/index.ts
|
|
719
|
+
var SHOWN_LINES = 8;
|
|
720
|
+
var BM25_K1 = 1.2;
|
|
721
|
+
var BM25_B = 0.75;
|
|
722
|
+
var STOPWORDS = new Set("the and for with that this what which when where who how was were did does done from into onto have has had you your our out not but are can could would should will just then than them they there their its it's about after before again also any each other some such only own same very more most over under while because been being into".split(" "));
|
|
723
|
+
function terms(text) {
|
|
724
|
+
return (text.toLowerCase().match(/[a-z0-9_@$][a-z0-9_.:/@$-]*[a-z0-9_]|[a-z0-9]/g) ?? []).flatMap((term) => /[._:/-]/.test(term) ? [term, ...term.split(/[._:/-]+/)] : [term]).filter((term) => term.length >= 3 && !STOPWORDS.has(term));
|
|
725
|
+
}
|
|
726
|
+
function windows(lines, size) {
|
|
727
|
+
const out = [];
|
|
728
|
+
for (let start = 0;start < lines.length; start += size)
|
|
729
|
+
out.push(lines.slice(start, start + size));
|
|
730
|
+
return out;
|
|
731
|
+
}
|
|
732
|
+
var CHUNK_CACHE_TERMS = 8000000;
|
|
733
|
+
var chunked = new Map;
|
|
734
|
+
var chunkedTerms = 0;
|
|
735
|
+
var termCount = (chunks) => chunks.reduce((sum, chunk) => sum + chunk.terms.length, 0);
|
|
736
|
+
function forget(key) {
|
|
737
|
+
const known = chunked.get(key);
|
|
738
|
+
if (!known)
|
|
739
|
+
return;
|
|
740
|
+
chunked.delete(key);
|
|
741
|
+
chunkedTerms -= termCount(known.chunks);
|
|
742
|
+
}
|
|
743
|
+
function chunksOf(key, text, head, turn, build) {
|
|
744
|
+
if (key === null)
|
|
745
|
+
return build();
|
|
746
|
+
const known = chunked.get(key);
|
|
747
|
+
if (known && known.text === text && known.head === head) {
|
|
748
|
+
chunked.delete(key);
|
|
749
|
+
chunked.set(key, known);
|
|
750
|
+
if (known.turn !== turn) {
|
|
751
|
+
known.turn = turn;
|
|
752
|
+
known.chunks = known.chunks.map((chunk) => ({ ...chunk, turn }));
|
|
753
|
+
}
|
|
754
|
+
return known.chunks;
|
|
755
|
+
}
|
|
756
|
+
forget(key);
|
|
757
|
+
const chunks = build();
|
|
758
|
+
chunked.set(key, { text, head, turn, chunks });
|
|
759
|
+
chunkedTerms += termCount(chunks);
|
|
760
|
+
for (const oldest of chunked.keys()) {
|
|
761
|
+
if (chunkedTerms <= CHUNK_CACHE_TERMS || oldest === key)
|
|
762
|
+
break;
|
|
763
|
+
forget(oldest);
|
|
764
|
+
}
|
|
765
|
+
return chunks;
|
|
766
|
+
}
|
|
767
|
+
function recallChunks(messages, keptIds, options = {}) {
|
|
768
|
+
const size = options.windowLines ?? 6;
|
|
769
|
+
const weight = Math.max(1, options.callWeight ?? 1);
|
|
770
|
+
const chunks = [];
|
|
771
|
+
splitTurns(messages).turns.slice(0, -1).forEach((turn, turnIndex) => {
|
|
772
|
+
for (const { call, result } of toolPairs(turn)) {
|
|
773
|
+
const text = resultText(result);
|
|
774
|
+
if (keptIds.get(call.id) === text || !text.trim())
|
|
775
|
+
continue;
|
|
776
|
+
const head = describeToolCall(call);
|
|
777
|
+
const key = typeof call.id === "string" && call.id ? `${size}|${weight}|${call.id}` : null;
|
|
778
|
+
const built = chunksOf(key, text, head, turnIndex + 1, () => {
|
|
779
|
+
const headTerms = Array.from({ length: weight }, () => terms(head)).flat();
|
|
780
|
+
return windows(text.split(`
|
|
781
|
+
`), size).map((window) => ({ turn: turnIndex + 1, call: head, lines: window, terms: [...headTerms, ...terms(window.join(`
|
|
782
|
+
`))] }));
|
|
783
|
+
});
|
|
784
|
+
for (const chunk of built)
|
|
785
|
+
chunks.push(chunk);
|
|
786
|
+
}
|
|
787
|
+
if (!(options.narrative ?? true))
|
|
788
|
+
return;
|
|
789
|
+
const said = turn.slice(1, -1).filter((m) => m.role === "assistant").flatMap((m) => blocks(m).filter((b) => b.type === "text").map((b) => String(b.text ?? "").trim())).filter(Boolean).join(`
|
|
790
|
+
`);
|
|
791
|
+
if (!said)
|
|
792
|
+
return;
|
|
793
|
+
const built = chunksOf(`${size}|said|${turnIndex}|${said}`, said, "agent", turnIndex + 1, () => windows(said.split(`
|
|
794
|
+
`), size).map((window) => ({ turn: turnIndex + 1, call: "agent", lines: window, terms: terms(window.join(`
|
|
795
|
+
`)) })));
|
|
796
|
+
for (const chunk of built)
|
|
797
|
+
chunks.push(chunk);
|
|
798
|
+
});
|
|
799
|
+
return chunks;
|
|
800
|
+
}
|
|
801
|
+
var INDEXED_FROM = 2000;
|
|
802
|
+
var KEPT_INDEXES = 6;
|
|
803
|
+
var SAMPLED = 32;
|
|
804
|
+
var indexes = [];
|
|
805
|
+
function addToIndex(index, chunk) {
|
|
806
|
+
index.members.add(chunk);
|
|
807
|
+
const counts = new Map;
|
|
808
|
+
for (const term of chunk.terms)
|
|
809
|
+
counts.set(term, (counts.get(term) ?? 0) + 1);
|
|
810
|
+
for (const [term, count] of counts) {
|
|
811
|
+
const postings = index.postings.get(term);
|
|
812
|
+
if (postings) {
|
|
813
|
+
postings.chunks.push(chunk);
|
|
814
|
+
postings.counts.push(count);
|
|
815
|
+
} else
|
|
816
|
+
index.postings.set(term, { chunks: [chunk], counts: [count] });
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function overlap(index, chunks) {
|
|
820
|
+
const step = Math.max(1, Math.floor(chunks.length / SAMPLED));
|
|
821
|
+
let found = 0;
|
|
822
|
+
let sampled = 0;
|
|
823
|
+
for (let i = 0;i < chunks.length; i += step) {
|
|
824
|
+
sampled += 1;
|
|
825
|
+
if (index.members.has(chunks[i]))
|
|
826
|
+
found += 1;
|
|
827
|
+
}
|
|
828
|
+
return found / Math.max(1, sampled);
|
|
829
|
+
}
|
|
830
|
+
function indexFor(chunks) {
|
|
831
|
+
let best = null;
|
|
832
|
+
let bestOverlap = 0.5;
|
|
833
|
+
for (const index of indexes) {
|
|
834
|
+
const shared = overlap(index, chunks);
|
|
835
|
+
if (shared >= bestOverlap) {
|
|
836
|
+
best = index;
|
|
837
|
+
bestOverlap = shared;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
let present = 0;
|
|
841
|
+
if (best) {
|
|
842
|
+
for (const chunk of chunks)
|
|
843
|
+
if (best.members.has(chunk))
|
|
844
|
+
present += 1;
|
|
845
|
+
}
|
|
846
|
+
if (!best || best.members.size - present > chunks.length / 2) {
|
|
847
|
+
if (best)
|
|
848
|
+
indexes.splice(indexes.indexOf(best), 1);
|
|
849
|
+
best = { members: new Set, postings: new Map };
|
|
850
|
+
present = 0;
|
|
851
|
+
} else
|
|
852
|
+
indexes.splice(indexes.indexOf(best), 1);
|
|
853
|
+
indexes.push(best);
|
|
854
|
+
if (indexes.length > KEPT_INDEXES)
|
|
855
|
+
indexes.shift();
|
|
856
|
+
if (present < chunks.length) {
|
|
857
|
+
for (const chunk of chunks)
|
|
858
|
+
if (!best.members.has(chunk))
|
|
859
|
+
addToIndex(best, chunk);
|
|
860
|
+
}
|
|
861
|
+
return best;
|
|
862
|
+
}
|
|
863
|
+
function indexedAll(queries, chunks, minTerms) {
|
|
864
|
+
const position = new Map;
|
|
865
|
+
let total = 0;
|
|
866
|
+
chunks.forEach((chunk, i) => {
|
|
867
|
+
position.set(chunk, i);
|
|
868
|
+
total += chunk.terms.length;
|
|
869
|
+
});
|
|
870
|
+
if (position.size !== chunks.length)
|
|
871
|
+
return null;
|
|
872
|
+
const index = indexFor(chunks);
|
|
873
|
+
const average = total / Math.max(1, chunks.length);
|
|
874
|
+
const hitsByTerm = new Map;
|
|
875
|
+
const hitsOf = (term) => {
|
|
876
|
+
const known = hitsByTerm.get(term);
|
|
877
|
+
if (known)
|
|
878
|
+
return known;
|
|
879
|
+
const hits = [];
|
|
880
|
+
const postings = index.postings.get(term);
|
|
881
|
+
if (postings)
|
|
882
|
+
postings.chunks.forEach((chunk, k) => {
|
|
883
|
+
const at = position.get(chunk);
|
|
884
|
+
if (at !== undefined)
|
|
885
|
+
hits.push([at, postings.counts[k]]);
|
|
886
|
+
});
|
|
887
|
+
hitsByTerm.set(term, hits);
|
|
888
|
+
return hits;
|
|
889
|
+
};
|
|
890
|
+
return queries.map((query) => {
|
|
891
|
+
const lists = [...new Set(query)].map(hitsOf);
|
|
892
|
+
const needed = Math.max(1, Math.min(minTerms, lists.filter((hits) => hits.length > 0).length));
|
|
893
|
+
const found = new Map;
|
|
894
|
+
lists.forEach((hits, k) => {
|
|
895
|
+
for (const [at, f] of hits) {
|
|
896
|
+
const mine = found.get(at);
|
|
897
|
+
if (mine)
|
|
898
|
+
mine.push([k, f]);
|
|
899
|
+
else
|
|
900
|
+
found.set(at, [[k, f]]);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
const scored = [];
|
|
904
|
+
for (const at of [...found.keys()].sort((a, b) => a - b)) {
|
|
905
|
+
const mine = found.get(at);
|
|
906
|
+
if (mine.length < needed)
|
|
907
|
+
continue;
|
|
908
|
+
const length = chunks[at].terms.length;
|
|
909
|
+
let score = 0;
|
|
910
|
+
for (const [k, f] of mine) {
|
|
911
|
+
const n = lists[k].length;
|
|
912
|
+
const idf = Math.log(1 + (chunks.length - n + 0.5) / (n + 0.5));
|
|
913
|
+
score += idf * (f * (BM25_K1 + 1)) / (f + BM25_K1 * (1 - BM25_B + BM25_B * length / average));
|
|
914
|
+
}
|
|
915
|
+
if (score > 0)
|
|
916
|
+
scored.push({ index: at, score, matched: mine.length });
|
|
917
|
+
}
|
|
918
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
function rankAll(queries, chunks, minTerms = 2) {
|
|
922
|
+
return chunks.length >= INDEXED_FROM && indexedAll(queries, chunks, minTerms) || scanAll(queries, chunks, minTerms);
|
|
923
|
+
}
|
|
924
|
+
function scanAll(queries, chunks, minTerms) {
|
|
925
|
+
const ids = new Map;
|
|
926
|
+
for (const query of queries)
|
|
927
|
+
for (const term of query)
|
|
928
|
+
if (!ids.has(term))
|
|
929
|
+
ids.set(term, ids.size);
|
|
930
|
+
const df = new Int32Array(ids.size);
|
|
931
|
+
const counts = new Int32Array(ids.size);
|
|
932
|
+
const touched = [];
|
|
933
|
+
const hits = [];
|
|
934
|
+
let total = 0;
|
|
935
|
+
chunks.forEach((chunk, index) => {
|
|
936
|
+
total += chunk.terms.length;
|
|
937
|
+
touched.length = 0;
|
|
938
|
+
for (const term of chunk.terms) {
|
|
939
|
+
const id = ids.get(term);
|
|
940
|
+
if (id === undefined)
|
|
941
|
+
continue;
|
|
942
|
+
if (counts[id] === 0)
|
|
943
|
+
touched.push(id);
|
|
944
|
+
counts[id] += 1;
|
|
945
|
+
}
|
|
946
|
+
if (touched.length === 0)
|
|
947
|
+
return;
|
|
948
|
+
const found = touched.map((id) => {
|
|
949
|
+
const f = counts[id];
|
|
950
|
+
counts[id] = 0;
|
|
951
|
+
df[id] += 1;
|
|
952
|
+
return [id, f];
|
|
953
|
+
});
|
|
954
|
+
hits.push({ index, found });
|
|
955
|
+
});
|
|
956
|
+
const average = total / Math.max(1, chunks.length);
|
|
957
|
+
return queries.map((query) => {
|
|
958
|
+
const asked = [...new Set(query)].map((term) => ids.get(term) ?? -1);
|
|
959
|
+
const position = new Int32Array(ids.size).fill(-1);
|
|
960
|
+
asked.forEach((id, i) => {
|
|
961
|
+
position[id] = i;
|
|
962
|
+
});
|
|
963
|
+
const needed = Math.max(1, Math.min(minTerms, asked.filter((id) => df[id] > 0).length));
|
|
964
|
+
const scored = [];
|
|
965
|
+
for (const { index, found } of hits) {
|
|
966
|
+
const mine = found.filter(([id]) => position[id] >= 0);
|
|
967
|
+
if (mine.length < needed)
|
|
968
|
+
continue;
|
|
969
|
+
if (mine.length > 1)
|
|
970
|
+
mine.sort((a, b) => position[a[0]] - position[b[0]]);
|
|
971
|
+
const length = chunks[index].terms.length;
|
|
972
|
+
let score = 0;
|
|
973
|
+
for (const [id, f] of mine) {
|
|
974
|
+
const n = df[id];
|
|
975
|
+
const idf = Math.log(1 + (chunks.length - n + 0.5) / (n + 0.5));
|
|
976
|
+
score += idf * (f * (BM25_K1 + 1)) / (f + BM25_K1 * (1 - BM25_B + BM25_B * length / average));
|
|
977
|
+
}
|
|
978
|
+
if (score > 0)
|
|
979
|
+
scored.push({ index, score, matched: mine.length });
|
|
980
|
+
}
|
|
981
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function excerpt(chunk, query, salient) {
|
|
985
|
+
const limit = salient ? SHOWN_LINES + 2 : SHOWN_LINES;
|
|
986
|
+
const shown = new Set;
|
|
987
|
+
chunk.lines.forEach((line, i) => {
|
|
988
|
+
if (!terms(line).some((t) => query.has(t)))
|
|
989
|
+
return;
|
|
990
|
+
for (const j of [i - 1, i, i + 1])
|
|
991
|
+
if (j >= 0 && j < chunk.lines.length)
|
|
992
|
+
shown.add(j);
|
|
993
|
+
});
|
|
994
|
+
if (salient) {
|
|
995
|
+
for (let i = 0;i < chunk.lines.length && shown.size < limit; i += 1)
|
|
996
|
+
if (isSalient(chunk.lines[i]))
|
|
997
|
+
shown.add(i);
|
|
998
|
+
}
|
|
999
|
+
const ordered = [...shown].sort((a, b) => a - b).slice(0, limit);
|
|
1000
|
+
return (ordered.length > 0 ? ordered : [0]).map((i) => clip(chunk.lines[i], 240)).filter(Boolean);
|
|
1001
|
+
}
|
|
1002
|
+
function queriesOf(prompt) {
|
|
1003
|
+
const lines = prompt.split(`
|
|
1004
|
+
`).map((l) => l.trim()).filter((l) => terms(l).length >= 2);
|
|
1005
|
+
return lines.length > 1 ? lines : [prompt];
|
|
1006
|
+
}
|
|
1007
|
+
function merged(hits) {
|
|
1008
|
+
const picked = new Map;
|
|
1009
|
+
for (const { query, indices } of hits) {
|
|
1010
|
+
for (const index of indices) {
|
|
1011
|
+
const matched = picked.get(index) ?? new Set;
|
|
1012
|
+
for (const term of query)
|
|
1013
|
+
matched.add(term);
|
|
1014
|
+
picked.set(index, matched);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return [...picked.entries()].map(([index, query]) => ({ index, query }));
|
|
1018
|
+
}
|
|
1019
|
+
function rankedLines(prompt, chunks, options, width) {
|
|
1020
|
+
const lines = queriesOf(prompt);
|
|
1021
|
+
const queries = lines.map(terms);
|
|
1022
|
+
const results = rankAll(queries, chunks, options.minTerms);
|
|
1023
|
+
return lines.map((line, i) => ({ line, query: queries[i], indices: results[i].slice(0, width).map((hit) => hit.index) }));
|
|
1024
|
+
}
|
|
1025
|
+
function pickRecall(prompt, chunks, options) {
|
|
1026
|
+
return merged(rankedLines(prompt, chunks, options, options.perQuery ?? 12));
|
|
1027
|
+
}
|
|
1028
|
+
var identityReranker = async (_query, _chunks, candidates) => candidates;
|
|
1029
|
+
async function pickRecallWith(prompt, chunks, rerank, options = {}) {
|
|
1030
|
+
const hits = [];
|
|
1031
|
+
for (const { line, query, indices } of rankedLines(prompt, chunks, options, options.candidates ?? 30)) {
|
|
1032
|
+
const reordered = await rerank(line, chunks, indices);
|
|
1033
|
+
hits.push({ query, indices: reordered.slice(0, options.perQuery ?? 12) });
|
|
1034
|
+
}
|
|
1035
|
+
return merged(hits);
|
|
1036
|
+
}
|
|
1037
|
+
function renderRecall(prompt, chunks, picks, options = {}) {
|
|
1038
|
+
if (picks.length === 0)
|
|
1039
|
+
return null;
|
|
1040
|
+
const budget = queriesOf(prompt).length > 1 ? options.maxCharsMulti ?? 30000 : options.maxChars ?? 18000;
|
|
1041
|
+
const entries = [];
|
|
1042
|
+
let used = 0;
|
|
1043
|
+
for (const { index, query } of [...picks].sort((a, b) => chunks[a.index].turn - chunks[b.index].turn || a.index - b.index)) {
|
|
1044
|
+
const chunk = chunks[index];
|
|
1045
|
+
const where = chunk.from ? `${chunk.from}, turn ${chunk.turn}` : `turn ${chunk.turn}`;
|
|
1046
|
+
const lines = excerpt(chunk, query, options.salientExcerpt ?? true).map((l) => ` ${l}`);
|
|
1047
|
+
const entry = `[${where}] ${chunk.call}:
|
|
1048
|
+
${lines.join(`
|
|
1049
|
+
`)}`;
|
|
1050
|
+
if (used + entry.length > budget)
|
|
1051
|
+
continue;
|
|
1052
|
+
entries.push(entry);
|
|
1053
|
+
used += entry.length;
|
|
1054
|
+
}
|
|
1055
|
+
if (entries.length === 0)
|
|
1056
|
+
return null;
|
|
1057
|
+
return recalledContext(entries, picks.some((p) => Boolean(chunks[p.index].from)));
|
|
1058
|
+
}
|
|
1059
|
+
function recallFor(prompt, chunks, options = {}) {
|
|
1060
|
+
if (chunks.length === 0)
|
|
1061
|
+
return null;
|
|
1062
|
+
return renderRecall(prompt, chunks, pickRecall(prompt, chunks, options), options);
|
|
1063
|
+
}
|
|
1064
|
+
async function recallForWith(prompt, chunks, rerank, options = {}) {
|
|
1065
|
+
if (chunks.length === 0)
|
|
1066
|
+
return null;
|
|
1067
|
+
return renderRecall(prompt, chunks, await pickRecallWith(prompt, chunks, rerank, options), options);
|
|
1068
|
+
}
|
|
1069
|
+
function livePrompt(messages) {
|
|
1070
|
+
const { preamble, turns } = splitTurns(messages);
|
|
1071
|
+
const live = turns.at(-1);
|
|
1072
|
+
if (!live || !isPrompt(live[0]))
|
|
1073
|
+
return null;
|
|
1074
|
+
const index = preamble.length + turns.slice(0, -1).reduce((s, t) => s + t.length, 0);
|
|
1075
|
+
const text = textOf(live[0]).replace(/<(system-reminder|system-message|recalled-context|tool-evidence|handoff)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, "").trim();
|
|
1076
|
+
return { index, text };
|
|
1077
|
+
}
|
|
1078
|
+
function withRecall(messages, fingerprint, recalls) {
|
|
1079
|
+
if (Object.keys(recalls).length === 0)
|
|
1080
|
+
return messages;
|
|
1081
|
+
return messages.map((message) => {
|
|
1082
|
+
if (!isPrompt(message))
|
|
1083
|
+
return message;
|
|
1084
|
+
const text = recalls[fingerprint(message)];
|
|
1085
|
+
return text ? { ...message, content: [...blocks(message), { type: "text", text }] } : message;
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/core/estimate.ts
|
|
1090
|
+
var DEFAULT_RATIO = 1 / 3.6;
|
|
1091
|
+
var MEDIA_CHARS = 5800;
|
|
1092
|
+
function sizeOf(body) {
|
|
1093
|
+
let media = 0;
|
|
1094
|
+
const json = JSON.stringify({ system: body.system, tools: body.tools, messages: body.messages }, (key, value) => {
|
|
1095
|
+
if (key !== "data" || typeof value !== "string" || value.length <= 2000)
|
|
1096
|
+
return value;
|
|
1097
|
+
media += 1;
|
|
1098
|
+
return "";
|
|
1099
|
+
});
|
|
1100
|
+
return json.length + media * MEDIA_CHARS;
|
|
1101
|
+
}
|
|
1102
|
+
function estimator(body, ratio) {
|
|
1103
|
+
return {
|
|
1104
|
+
of: (messages) => Math.round(sizeOf({ ...body, messages }) * ratio),
|
|
1105
|
+
fixed: Math.round(sizeOf({ ...body, messages: [] }) * ratio)
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function targetSize(size, window) {
|
|
1109
|
+
return size.fixed + 0.3 * Math.max(0, window - size.fixed);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/core/state.ts
|
|
1113
|
+
function memoryStore(initial = {}) {
|
|
1114
|
+
const threads = new Map(Object.entries(initial).map(([key, state]) => [key, structuredClone(state)]));
|
|
1115
|
+
return {
|
|
1116
|
+
read: (key) => structuredClone(threads.get(key) ?? {}),
|
|
1117
|
+
save: (key, state) => {
|
|
1118
|
+
threads.set(key, structuredClone(state));
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function resetCut(state) {
|
|
1123
|
+
delete state.cut;
|
|
1124
|
+
delete state.keep;
|
|
1125
|
+
delete state.cleared;
|
|
1126
|
+
delete state.stuck;
|
|
1127
|
+
delete state.host;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/core/compaction.ts
|
|
1131
|
+
var FOLDED_TURNS = 4096;
|
|
1132
|
+
var folded = new Map;
|
|
1133
|
+
function reduceRemembered(turn, options) {
|
|
1134
|
+
const key = `${options.evidence ? 1 : 0}:${quickHash(JSON.stringify(turn))}`;
|
|
1135
|
+
const known = folded.get(key);
|
|
1136
|
+
if (known) {
|
|
1137
|
+
folded.delete(key);
|
|
1138
|
+
folded.set(key, known);
|
|
1139
|
+
return known;
|
|
1140
|
+
}
|
|
1141
|
+
const reduced = reduceTurn(turn, options);
|
|
1142
|
+
folded.set(key, reduced);
|
|
1143
|
+
if (folded.size > FOLDED_TURNS)
|
|
1144
|
+
folded.delete(folded.keys().next().value);
|
|
1145
|
+
return reduced;
|
|
1146
|
+
}
|
|
1147
|
+
var PROTECTED_OUTPUTS = 5;
|
|
1148
|
+
var STUCK_STEP = 0.1;
|
|
1149
|
+
function settledCut(turns, index) {
|
|
1150
|
+
let cut = index;
|
|
1151
|
+
while (cut > 1 && JSON.stringify(turns[cut][0].content).includes("[Request interrupted"))
|
|
1152
|
+
cut -= 1;
|
|
1153
|
+
return cut;
|
|
1154
|
+
}
|
|
1155
|
+
function resolveCut(turns, state, config, rollingKeep) {
|
|
1156
|
+
let cutIndex = state.cut ? turns.findIndex((turn) => turnFingerprint(turn) === state.cut) : -1;
|
|
1157
|
+
let stateChanged = false;
|
|
1158
|
+
if (state.cut && cutIndex === -1) {
|
|
1159
|
+
resetCut(state);
|
|
1160
|
+
stateChanged = true;
|
|
1161
|
+
}
|
|
1162
|
+
if (cutIndex <= 0 && config.eval?.forceCut && turns.length >= 3) {
|
|
1163
|
+
cutIndex = settledCut(turns, Math.max(1, turns.length - 1 - (rollingKeep ?? 1)));
|
|
1164
|
+
state.cut = turnFingerprint(turns[cutIndex]);
|
|
1165
|
+
stateChanged = true;
|
|
1166
|
+
}
|
|
1167
|
+
return { cutIndex, stateChanged };
|
|
1168
|
+
}
|
|
1169
|
+
function shrinkOnce(state, cutIndex, configuredKeep, liveResults) {
|
|
1170
|
+
const keep = Math.min(state.keep ?? configuredKeep, cutIndex - 1);
|
|
1171
|
+
if (cutIndex > 0 && keep > 0) {
|
|
1172
|
+
state.keep = keep - 1;
|
|
1173
|
+
return `keep=${state.keep}`;
|
|
1174
|
+
}
|
|
1175
|
+
const remaining = liveResults() - (state.cleared ?? 0) - PROTECTED_OUTPUTS;
|
|
1176
|
+
if (remaining <= 0)
|
|
1177
|
+
return "stuck";
|
|
1178
|
+
state.cleared = (state.cleared ?? 0) + Math.ceil(remaining / 2);
|
|
1179
|
+
return `cleared=${state.cleared}`;
|
|
1180
|
+
}
|
|
1181
|
+
function dueRoll(turns, cutIndex, rolling, size) {
|
|
1182
|
+
const next = settledCut(turns, turns.length - 1 - rolling.keep);
|
|
1183
|
+
const backlog = size.of(turns.slice(Math.max(cutIndex, 0), Math.max(next, 0)).flat()) - size.fixed;
|
|
1184
|
+
return next >= 1 && next > cutIndex && backlog > rolling.budget ? next : null;
|
|
1185
|
+
}
|
|
1186
|
+
function shrinkToTarget(over, step, actions, rebuild) {
|
|
1187
|
+
while (over()) {
|
|
1188
|
+
const action = step();
|
|
1189
|
+
actions.push(action);
|
|
1190
|
+
if (action === "stuck")
|
|
1191
|
+
return true;
|
|
1192
|
+
rebuild();
|
|
1193
|
+
}
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
var holdsStuck = (state, estimate, window) => window !== null && state.stuck !== undefined && estimate <= state.stuck + STUCK_STEP * window;
|
|
1197
|
+
var headLength = (preamble, first, state) => preamble.length + 1 + (countPinned([first], new Set(state.pins)) > 0 ? first.length : 1);
|
|
1198
|
+
function stuckSizes(messages, headEnd, liveFrom, size) {
|
|
1199
|
+
const of = (list) => size.of(list) - size.fixed;
|
|
1200
|
+
return { fixed: size.fixed, head: of(messages.slice(0, headEnd)), folded: of(messages.slice(headEnd, liveFrom)), live: of(messages.slice(liveFrom)) };
|
|
1201
|
+
}
|
|
1202
|
+
function cutNote(state, turns, cutIndex, configuredKeep, actions) {
|
|
1203
|
+
const pins = new Set(state.pins);
|
|
1204
|
+
const keptFrom = cutIndex > 0 ? Math.max(1, cutIndex - (state.keep ?? configuredKeep)) : 0;
|
|
1205
|
+
const omitted = cutIndex > 0 ? keptFrom - 1 - countPinned(turns.slice(1, keptFrom), pins) : 0;
|
|
1206
|
+
return {
|
|
1207
|
+
action: cutIndex > 0 ? "compact" : "full",
|
|
1208
|
+
compacted: actions.includes("cut") || actions.includes("roll"),
|
|
1209
|
+
actions: actions.length > 0 ? actions : undefined,
|
|
1210
|
+
cutTurn: cutIndex > 0 ? cutIndex : null,
|
|
1211
|
+
omitted,
|
|
1212
|
+
folded: cutIndex > 0 ? cutIndex - keptFrom - countPinned(turns.slice(keptFrom, cutIndex), pins) + (foldsOpening(turns[0], keptFrom) && countPinned([turns[0]], pins) === 0 ? 1 : 0) : 0,
|
|
1213
|
+
pinned: cutIndex > 0 ? countPinned(turns.slice(0, cutIndex), pins) : 0,
|
|
1214
|
+
host: state.host,
|
|
1215
|
+
keep: state.keep,
|
|
1216
|
+
cleared: state.cleared
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function compact({ body, config, state, window, guide, transcriptDir, mayCompact, omittedBefore = 0, decorate = (m) => m, reduce }) {
|
|
1220
|
+
const { preamble, turns } = splitTurns(body.messages);
|
|
1221
|
+
const rolling = config.eval?.rolling ?? (config.mode === "default" ? { keep: config.keep, budget: config.budget } : undefined);
|
|
1222
|
+
const resolved = resolveCut(turns, state, config, rolling?.keep);
|
|
1223
|
+
let cutIndex = resolved.cutIndex;
|
|
1224
|
+
let stateChanged = resolved.stateChanged;
|
|
1225
|
+
const configuredKeep = config.keepTurns === "all" ? Number.MAX_SAFE_INTEGER : config.keepTurns;
|
|
1226
|
+
const ratio = state.ratio ?? DEFAULT_RATIO;
|
|
1227
|
+
const options = { guide, omittedBefore, evidence: config.eval?.evidence ?? true, reduce: reduce ?? reduceRemembered, capChars: config.turnCap === "off" ? undefined : config.turnCap / ratio, pinned: new Set(state.pins) };
|
|
1228
|
+
const liveStart = (list) => list.length - splitTurns(list).turns.slice(-(turns.length - Math.max(cutIndex, 0))).flat().length;
|
|
1229
|
+
const compacted = () => capTurns(cutIndex > 0 ? compactMessages(body.messages, cutIndex, { ...options, keepTurns: state.keep ?? configuredKeep }) : body.messages, { ...options, keepTurns: 0 });
|
|
1230
|
+
const build = () => {
|
|
1231
|
+
const messages = compacted();
|
|
1232
|
+
return state.cleared ? clearToolResults(messages, liveStart(messages), state.cleared, clearedOutput(transcriptDir ?? null)) : messages;
|
|
1233
|
+
};
|
|
1234
|
+
const liveResults = () => {
|
|
1235
|
+
const uncleared = compacted();
|
|
1236
|
+
return countToolResults(uncleared, liveStart(uncleared));
|
|
1237
|
+
};
|
|
1238
|
+
const size = estimator(body, ratio);
|
|
1239
|
+
const limit = window ? config.compactAt * window : Number.POSITIVE_INFINITY;
|
|
1240
|
+
const target = window ? targetSize(size, window) : Number.POSITIVE_INFINITY;
|
|
1241
|
+
let messages = build();
|
|
1242
|
+
let estimate = size.of(decorate(messages));
|
|
1243
|
+
const actions = [];
|
|
1244
|
+
const rebuild = () => {
|
|
1245
|
+
messages = build();
|
|
1246
|
+
estimate = size.of(decorate(messages));
|
|
1247
|
+
};
|
|
1248
|
+
const moveCut = (index, action) => {
|
|
1249
|
+
cutIndex = index;
|
|
1250
|
+
resetCut(state);
|
|
1251
|
+
state.cut = turnFingerprint(turns[index]);
|
|
1252
|
+
actions.push(action);
|
|
1253
|
+
rebuild();
|
|
1254
|
+
};
|
|
1255
|
+
const roll = mayCompact && rolling ? dueRoll(turns, cutIndex, rolling, size) : null;
|
|
1256
|
+
if (roll !== null) {
|
|
1257
|
+
moveCut(roll, "roll");
|
|
1258
|
+
stateChanged = true;
|
|
1259
|
+
}
|
|
1260
|
+
if (mayCompact && estimate > limit) {
|
|
1261
|
+
const current = turns.length - 1;
|
|
1262
|
+
if (current > 0 && current > cutIndex)
|
|
1263
|
+
moveCut(current, "cut");
|
|
1264
|
+
if (!holdsStuck(state, estimate, window)) {
|
|
1265
|
+
const stuck = shrinkToTarget(() => estimate > target, () => shrinkOnce(state, cutIndex, configuredKeep, liveResults), actions, rebuild);
|
|
1266
|
+
if (stuck)
|
|
1267
|
+
state.stuck = estimate;
|
|
1268
|
+
else
|
|
1269
|
+
delete state.stuck;
|
|
1270
|
+
}
|
|
1271
|
+
stateChanged = true;
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
messages,
|
|
1275
|
+
stateChanged,
|
|
1276
|
+
note: {
|
|
1277
|
+
...cutNote(state, turns, cutIndex, configuredKeep, actions),
|
|
1278
|
+
stuck: state.stuck === undefined ? undefined : stuckSizes(messages, headLength(preamble, turns[0], state), liveStart(messages), size),
|
|
1279
|
+
turns: turns.length,
|
|
1280
|
+
messagesIn: body.messages.length,
|
|
1281
|
+
messagesOut: messages.length,
|
|
1282
|
+
estimate,
|
|
1283
|
+
window
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/core/fingerprint.ts
|
|
1289
|
+
var isInjected = (text) => /^<(?:recalled-context|handoff)[\s>]/.test(String(text ?? ""));
|
|
1290
|
+
function messageFingerprint(message) {
|
|
1291
|
+
const own = blocks(message).filter((b) => !(b.type === "text" && isInjected(b.text))).map(withoutCacheControl);
|
|
1292
|
+
return shortHash(JSON.stringify(own), 24);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/context/fold/index.ts
|
|
1296
|
+
var FOLD_CAP_CHARS = 400000;
|
|
1297
|
+
var spoken = (message) => textOf(message).replace(/<(system-reminder|system-message|recalled-context|handoff)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, "").trim();
|
|
1298
|
+
var isEvidence = (text) => text.startsWith("<tool-evidence>");
|
|
1299
|
+
function foldedTurns(messages) {
|
|
1300
|
+
return splitTurns(messages).turns.filter((turn) => isPrompt(turn[0])).map((turn) => {
|
|
1301
|
+
const reduced = reduceTurn(turn, { evidence: true });
|
|
1302
|
+
const users = reduced.filter((m) => m.role === "user").map(spoken);
|
|
1303
|
+
return {
|
|
1304
|
+
user: users.filter((t) => t && !isEvidence(t)).join(`
|
|
1305
|
+
|
|
1306
|
+
`),
|
|
1307
|
+
evidence: users.filter(isEvidence).join(`
|
|
1308
|
+
`),
|
|
1309
|
+
reply: reduced.filter((m) => m.role === "assistant").map(spoken).join(`
|
|
1310
|
+
`)
|
|
1311
|
+
};
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
function renderTurn(turn, index, full) {
|
|
1315
|
+
const parts = [`### Turn ${index + 1}`, `**You:** ${turn.user}`];
|
|
1316
|
+
if (full && turn.evidence)
|
|
1317
|
+
parts.push(turn.evidence);
|
|
1318
|
+
if (full && turn.reply)
|
|
1319
|
+
parts.push(`**Agent:** ${turn.reply}`);
|
|
1320
|
+
return parts.join(`
|
|
1321
|
+
|
|
1322
|
+
`);
|
|
1323
|
+
}
|
|
1324
|
+
function foldedView(messages, cap = Number.POSITIVE_INFINITY) {
|
|
1325
|
+
const turns = foldedTurns(messages);
|
|
1326
|
+
const full = new Set;
|
|
1327
|
+
let used = 0;
|
|
1328
|
+
for (let i = turns.length - 1;i >= 0; i -= 1) {
|
|
1329
|
+
const size = renderTurn(turns[i], i, true).length;
|
|
1330
|
+
if (used + size > cap)
|
|
1331
|
+
break;
|
|
1332
|
+
full.add(i);
|
|
1333
|
+
used += size;
|
|
1334
|
+
}
|
|
1335
|
+
return turns.map((turn, i) => renderTurn(turn, i, full.has(i))).join(`
|
|
1336
|
+
|
|
1337
|
+
`);
|
|
1338
|
+
}
|
|
1339
|
+
function sourceChunks(messages, from, users = false) {
|
|
1340
|
+
const chunks = recallChunks([...messages, { role: "user", content: "" }], new Map).map((chunk) => ({ ...chunk, from }));
|
|
1341
|
+
if (!users)
|
|
1342
|
+
return chunks;
|
|
1343
|
+
const said = splitTurns(messages).turns.filter((turn) => isPrompt(turn[0])).map((turn, i) => ({ turn: i + 1, text: spoken(turn[0]) })).filter((t) => t.text);
|
|
1344
|
+
return [...said.map((t) => ({ turn: t.turn, from, call: "you", lines: t.text.split(`
|
|
1345
|
+
`), terms: terms(t.text) })), ...chunks];
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/core/handoff.ts
|
|
1349
|
+
var chunksOf2 = new WeakMap;
|
|
1350
|
+
var continueToken = (id) => `{ee:continue:${id}}`;
|
|
1351
|
+
function continueIdOf(message) {
|
|
1352
|
+
if (!isPrompt(message))
|
|
1353
|
+
return null;
|
|
1354
|
+
const own = blocks(message).filter((b) => b.type === "text" && !isInjected(b.text)).map((b) => String(b.text ?? "")).join(`
|
|
1355
|
+
`);
|
|
1356
|
+
return own.match(/\{ee:continue:([A-Za-z0-9-]{8,})\}/)?.[1] ?? null;
|
|
1357
|
+
}
|
|
1358
|
+
function activeHandoffs(handoffs = {}) {
|
|
1359
|
+
return Object.fromEntries(Object.entries(handoffs).filter((entry) => Boolean(entry[1])));
|
|
1360
|
+
}
|
|
1361
|
+
function resolveHandoff(id, sources, state) {
|
|
1362
|
+
const found = sources.findHandoff?.(id) ?? null;
|
|
1363
|
+
if (!found || !sources.readHandoff)
|
|
1364
|
+
return null;
|
|
1365
|
+
const already = Object.values(state.handoffs ?? {}).some((h) => h?.source === found.source);
|
|
1366
|
+
return already ? null : { label: found.label, source: found.source, text: foldedView(sources.readHandoff(found.source), FOLD_CAP_CHARS) };
|
|
1367
|
+
}
|
|
1368
|
+
function attachHandoffs(messages, sources, state) {
|
|
1369
|
+
let changed = false;
|
|
1370
|
+
for (const message of messages) {
|
|
1371
|
+
const id = continueIdOf(message);
|
|
1372
|
+
if (!id)
|
|
1373
|
+
continue;
|
|
1374
|
+
const key = messageFingerprint(message);
|
|
1375
|
+
if (state.handoffs && key in state.handoffs)
|
|
1376
|
+
continue;
|
|
1377
|
+
const handoff = resolveHandoff(id, sources, state);
|
|
1378
|
+
state.handoffs ??= {};
|
|
1379
|
+
state.handoffs[key] = handoff;
|
|
1380
|
+
changed = true;
|
|
1381
|
+
}
|
|
1382
|
+
return changed;
|
|
1383
|
+
}
|
|
1384
|
+
function withHandoffs(messages, handoffs) {
|
|
1385
|
+
if (Object.keys(handoffs).length === 0)
|
|
1386
|
+
return messages;
|
|
1387
|
+
return messages.map((message) => {
|
|
1388
|
+
if (!isPrompt(message))
|
|
1389
|
+
return message;
|
|
1390
|
+
const handoff = handoffs[messageFingerprint(message)];
|
|
1391
|
+
return handoff ? { ...message, content: [{ type: "text", text: handoffBlock(handoff.label, handoff.text) }, ...blocks(message)] } : message;
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
function handoffChunks(handoffs, sources) {
|
|
1395
|
+
const read = sources.readHandoff;
|
|
1396
|
+
if (!read)
|
|
1397
|
+
return [];
|
|
1398
|
+
const labels = new Map;
|
|
1399
|
+
for (const handoff of Object.values(handoffs))
|
|
1400
|
+
if (!labels.has(handoff.source))
|
|
1401
|
+
labels.set(handoff.source, handoff.label);
|
|
1402
|
+
return [...labels].flatMap(([source, label]) => {
|
|
1403
|
+
const messages = read(source);
|
|
1404
|
+
const known = chunksOf2.get(messages);
|
|
1405
|
+
if (known?.label === label)
|
|
1406
|
+
return known.chunks;
|
|
1407
|
+
const chunks = sourceChunks(messages, label, true);
|
|
1408
|
+
chunksOf2.set(messages, { label, chunks });
|
|
1409
|
+
return chunks;
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/core/recall.ts
|
|
1414
|
+
var closingTurn = { role: "user", content: "" };
|
|
1415
|
+
var wantsRecall = (input) => (input.config.eval?.recall ?? true) && input.config.mode !== "off";
|
|
1416
|
+
var activeRecalls = (recalls = {}) => Object.fromEntries(Object.entries(recalls).filter(([, text]) => text));
|
|
1417
|
+
var touched = (input, result) => result.threadKey !== null && !(result.body === input.body && result.note.action === "pass");
|
|
1418
|
+
function droppedChunks(input, result, threadKey) {
|
|
1419
|
+
const sources = input.sources ?? {};
|
|
1420
|
+
const kept = new Map;
|
|
1421
|
+
for (const message of result.body.messages)
|
|
1422
|
+
for (const b of blocks(message))
|
|
1423
|
+
if (b.type === "tool_result")
|
|
1424
|
+
kept.set(b.tool_use_id, resultText(b));
|
|
1425
|
+
const own = recallChunks(input.body.messages, kept);
|
|
1426
|
+
const session = own.length > 0 || !sources.readHistory ? own : recallChunks([...sources.readHistory(), closingTurn], kept);
|
|
1427
|
+
const withSubagents = sources.listSubagentChunks ? [...session, ...sources.listSubagentChunks()] : session;
|
|
1428
|
+
return [...handoffChunks(activeHandoffs(input.store.read(threadKey).handoffs), sources), ...withSubagents];
|
|
1429
|
+
}
|
|
1430
|
+
function recallTarget(original) {
|
|
1431
|
+
const live = livePrompt(original.messages);
|
|
1432
|
+
return live ? { key: messageFingerprint(original.messages[live.index]), text: live.text } : null;
|
|
1433
|
+
}
|
|
1434
|
+
function applyRecall(input, result) {
|
|
1435
|
+
if (!touched(input, result) || !result.threadKey)
|
|
1436
|
+
return result;
|
|
1437
|
+
const state = input.store.read(result.threadKey);
|
|
1438
|
+
state.recalls ??= {};
|
|
1439
|
+
const target = recallTarget(input.body);
|
|
1440
|
+
let note = result.note;
|
|
1441
|
+
if (target && !(target.key in state.recalls)) {
|
|
1442
|
+
state.recalls[target.key] = recallFor(target.text, droppedChunks(input, result, result.threadKey), input.config.eval?.recallOptions) ?? "";
|
|
1443
|
+
input.store.save(result.threadKey, state);
|
|
1444
|
+
note = { ...note, recalled: state.recalls[target.key].length };
|
|
1445
|
+
}
|
|
1446
|
+
const messages = withRecall(result.body.messages, messageFingerprint, activeRecalls(state.recalls));
|
|
1447
|
+
return messages === result.body.messages ? { ...result, note } : { ...result, body: { ...result.body, messages }, note };
|
|
1448
|
+
}
|
|
1449
|
+
async function prefetchFor(input, result, rerank) {
|
|
1450
|
+
if (!touched(input, result) || !result.threadKey)
|
|
1451
|
+
return null;
|
|
1452
|
+
const target = recallTarget(input.body);
|
|
1453
|
+
if (!target)
|
|
1454
|
+
return null;
|
|
1455
|
+
const state = input.store.read(result.threadKey);
|
|
1456
|
+
state.recalls ??= {};
|
|
1457
|
+
if (target.key in state.recalls)
|
|
1458
|
+
return null;
|
|
1459
|
+
const options = { candidates: input.config.eval?.rerank?.candidates, ...input.config.eval?.recallOptions };
|
|
1460
|
+
state.recalls[target.key] = await recallForWith(target.text, droppedChunks(input, result, result.threadKey), rerank, options) ?? "";
|
|
1461
|
+
input.store.save(result.threadKey, state);
|
|
1462
|
+
return state.recalls[target.key].length;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/core/rewrite.ts
|
|
1466
|
+
var pass = (body) => ({ body, threadKey: null, note: { action: "pass" } });
|
|
1467
|
+
var isAgentThread = (body) => Array.isArray(body.tools) && body.tools.length > 0;
|
|
1468
|
+
var withSystemPrompt = (system, extra) => appendSystem(extra ? appendSystem(system, extra) : system, SYSTEM_PROMPT);
|
|
1469
|
+
var ratioOf = (input, threadKey) => input.store.read(threadKey).ratio ?? DEFAULT_RATIO;
|
|
1470
|
+
function threadKeyOf(threadId, messages) {
|
|
1471
|
+
const first = splitTurns(messages).turns[0];
|
|
1472
|
+
return first ? `${threadId}-${turnFingerprint(first)}` : null;
|
|
1473
|
+
}
|
|
1474
|
+
function evalStrategy(input, turns, threadKey, guide) {
|
|
1475
|
+
const { body, config } = input;
|
|
1476
|
+
const strategy = config.eval?.strategy;
|
|
1477
|
+
if (strategy !== "guide" && strategy !== "trim" && strategy !== "tail")
|
|
1478
|
+
return null;
|
|
1479
|
+
if (!isAgentThread(body))
|
|
1480
|
+
return pass(body);
|
|
1481
|
+
const counts = { turns: turns.length, messagesIn: body.messages.length };
|
|
1482
|
+
if (strategy === "guide") {
|
|
1483
|
+
return { body: { ...body, system: withSystemPrompt(body.system, guide ? searchOnlyInstructions(guide) : null) }, threadKey, note: { action: "guide", ...counts } };
|
|
1484
|
+
}
|
|
1485
|
+
if (strategy === "trim") {
|
|
1486
|
+
const messages = trimMessages(body.messages, { evidence: config.eval?.evidence ?? true });
|
|
1487
|
+
return { body: { ...body, system: withSystemPrompt(body.system, trimInstructions(guide)), messages }, threadKey, note: { action: "trim", ...counts, messagesOut: messages.length } };
|
|
1488
|
+
}
|
|
1489
|
+
const based = { ...body, system: withSystemPrompt(body.system) };
|
|
1490
|
+
const size = estimator(based, ratioOf(input, threadKey));
|
|
1491
|
+
let from = 1;
|
|
1492
|
+
if (input.window) {
|
|
1493
|
+
const target = targetSize(size, input.window);
|
|
1494
|
+
from = turns.length - 1;
|
|
1495
|
+
while (from > 1 && size.of(tailMessages(body.messages, from - 1, guide)) <= target)
|
|
1496
|
+
from -= 1;
|
|
1497
|
+
}
|
|
1498
|
+
const messages = tailMessages(body.messages, from, guide);
|
|
1499
|
+
return { body: { ...based, messages }, threadKey, note: { action: "tail", keptTurns: turns.length - from, ...counts, messagesOut: messages.length, estimate: size.of(messages) } };
|
|
1500
|
+
}
|
|
1501
|
+
function strategize(input) {
|
|
1502
|
+
const { body, threadId, config, store } = input;
|
|
1503
|
+
const sources = input.sources ?? {};
|
|
1504
|
+
if (!threadId || !Array.isArray(body.messages) || body.messages.length === 0)
|
|
1505
|
+
return pass(body);
|
|
1506
|
+
const threadKey = threadKeyOf(threadId, body.messages);
|
|
1507
|
+
if (!threadKey)
|
|
1508
|
+
return pass(body);
|
|
1509
|
+
const off = config.mode === "off";
|
|
1510
|
+
const state = store.read(threadKey);
|
|
1511
|
+
if (off && !state.host)
|
|
1512
|
+
return pass(body);
|
|
1513
|
+
const { turns } = splitTurns(body.messages);
|
|
1514
|
+
const guide = sources.guide ?? null;
|
|
1515
|
+
if (!off && attachHandoffs(body.messages, sources, state))
|
|
1516
|
+
store.save(threadKey, state);
|
|
1517
|
+
const forced = off ? null : evalStrategy(input, turns, threadKey, guide);
|
|
1518
|
+
if (forced)
|
|
1519
|
+
return forced;
|
|
1520
|
+
const based = isAgentThread(body) ? { ...body, system: withSystemPrompt(body.system) } : body;
|
|
1521
|
+
if (body.messages.length < 2)
|
|
1522
|
+
return { body: based, threadKey, note: { action: "full", turns: turns.length } };
|
|
1523
|
+
const handoffs = activeHandoffs(state.handoffs);
|
|
1524
|
+
const recalls = wantsRecall(input) ? activeRecalls(state.recalls) : {};
|
|
1525
|
+
const result = compact({
|
|
1526
|
+
body: based,
|
|
1527
|
+
config,
|
|
1528
|
+
state,
|
|
1529
|
+
window: input.window,
|
|
1530
|
+
guide,
|
|
1531
|
+
transcriptDir: sources.transcriptDir ?? null,
|
|
1532
|
+
mayCompact: input.mayCompact && !off,
|
|
1533
|
+
omittedBefore: input.omittedBefore,
|
|
1534
|
+
decorate: (messages) => withRecall(withHandoffs(messages, handoffs), messageFingerprint, recalls)
|
|
1535
|
+
});
|
|
1536
|
+
if (result.stateChanged)
|
|
1537
|
+
store.save(threadKey, state);
|
|
1538
|
+
return { body: result.messages === body.messages ? based : { ...based, messages: result.messages }, threadKey, note: result.note };
|
|
1539
|
+
}
|
|
1540
|
+
function withAttachedHandoffs(input, result) {
|
|
1541
|
+
if (!result.threadKey)
|
|
1542
|
+
return result;
|
|
1543
|
+
const handoffs = activeHandoffs(input.store.read(result.threadKey).handoffs);
|
|
1544
|
+
const messages = withHandoffs(result.body.messages, handoffs);
|
|
1545
|
+
if (messages === result.body.messages)
|
|
1546
|
+
return result;
|
|
1547
|
+
const attached = Object.values(handoffs);
|
|
1548
|
+
const note = { ...result.note, handoff: attached.reduce((sum, h) => sum + h.text.length, 0), handoffFrom: attached.map((h) => h.label).join(", ") };
|
|
1549
|
+
return { ...result, body: { ...result.body, messages }, note };
|
|
1550
|
+
}
|
|
1551
|
+
function once(input) {
|
|
1552
|
+
const result = withAttachedHandoffs(input, strategize(input));
|
|
1553
|
+
return wantsRecall(input) ? applyRecall(input, result) : result;
|
|
1554
|
+
}
|
|
1555
|
+
function sentEstimate(input, result) {
|
|
1556
|
+
return Math.round(sizeOf(result.body) * (result.threadKey ? ratioOf(input, result.threadKey) : DEFAULT_RATIO));
|
|
1557
|
+
}
|
|
1558
|
+
function overLimit(input, result) {
|
|
1559
|
+
if (!input.window || !result.threadKey || input.mayCompact === false || result.note.action === "pass")
|
|
1560
|
+
return false;
|
|
1561
|
+
return sentEstimate(input, result) > input.config.compactAt * input.window;
|
|
1562
|
+
}
|
|
1563
|
+
function carried(from, to) {
|
|
1564
|
+
const actions = [...from.note.actions ?? [], ...to.note.actions ?? []];
|
|
1565
|
+
if (!from.note.compacted && actions.length === 0)
|
|
1566
|
+
return to;
|
|
1567
|
+
return { ...to, note: { ...to.note, compacted: Boolean(to.note.compacted || from.note.compacted), actions } };
|
|
1568
|
+
}
|
|
1569
|
+
function rewrite(input, before) {
|
|
1570
|
+
const result = before ? carried(before, once(input)) : once(input);
|
|
1571
|
+
if (!overLimit(input, result))
|
|
1572
|
+
return result;
|
|
1573
|
+
const recut = carried(result, once(input));
|
|
1574
|
+
if (!overLimit(input, recut))
|
|
1575
|
+
return { ...recut, note: { ...recut.note, fit: "recut" } };
|
|
1576
|
+
const bare = carried(recut, once({ ...input, config: { ...input.config, eval: { ...input.config.eval, recall: false } } }));
|
|
1577
|
+
return { ...bare, note: { ...bare.note, fit: overLimit(input, bare) ? "over" : "bare", fitEstimate: sentEstimate(input, bare) } };
|
|
1578
|
+
}
|
|
1579
|
+
async function rewriteAsync(input, rerank = identityReranker) {
|
|
1580
|
+
if (!wantsRecall(input) || rerank === identityReranker)
|
|
1581
|
+
return rewrite(input);
|
|
1582
|
+
const first = strategize(input);
|
|
1583
|
+
const recalled = await prefetchFor(input, first, rerank);
|
|
1584
|
+
const final = rewrite(input, first);
|
|
1585
|
+
return recalled === null ? final : { ...final, note: { ...final.note, recalled } };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/core/fold.ts
|
|
1589
|
+
function located({ store, threadId, messages }) {
|
|
1590
|
+
const threadKey = threadKeyOf(threadId, messages);
|
|
1591
|
+
if (!threadKey)
|
|
1592
|
+
throw new RangeError("eunoe can only fold a thread that has at least one user turn");
|
|
1593
|
+
return { threadKey, turns: splitTurns(messages).turns, state: store.read(threadKey) };
|
|
1594
|
+
}
|
|
1595
|
+
function checkedTurn(turn, turns, from) {
|
|
1596
|
+
if (turns.length <= from)
|
|
1597
|
+
throw new RangeError(`this thread has ${turns.length} turn${turns.length === 1 ? "" : "s"}, so there is no turn ${turn} to fold at`);
|
|
1598
|
+
if (!Number.isInteger(turn) || turn < from || turn >= turns.length)
|
|
1599
|
+
throw new RangeError(`turn ${turn} is outside turns ${from}..${turns.length - 1}`);
|
|
1600
|
+
return turn;
|
|
1601
|
+
}
|
|
1602
|
+
function folded2(threadKey, turns, state) {
|
|
1603
|
+
const cutTurn = state.cut ? turns.findIndex((turn) => turnFingerprint(turn) === state.cut) : -1;
|
|
1604
|
+
const pins = new Set(state.pins);
|
|
1605
|
+
return { threadKey, cutTurn: cutTurn > 0 ? cutTurn : null, pinned: turns.flatMap((turn, i) => pins.has(turnFingerprint(turn)) ? [i] : []) };
|
|
1606
|
+
}
|
|
1607
|
+
function withPins(state, turns, indices, pinned) {
|
|
1608
|
+
const pins = new Set(state.pins);
|
|
1609
|
+
for (const index of indices) {
|
|
1610
|
+
const fingerprint = turnFingerprint(turns[checkedTurn(index, turns, 0)]);
|
|
1611
|
+
if (pinned)
|
|
1612
|
+
pins.add(fingerprint);
|
|
1613
|
+
else
|
|
1614
|
+
pins.delete(fingerprint);
|
|
1615
|
+
}
|
|
1616
|
+
if (pins.size > 0)
|
|
1617
|
+
state.pins = [...pins].sort();
|
|
1618
|
+
else
|
|
1619
|
+
delete state.pins;
|
|
1620
|
+
}
|
|
1621
|
+
function foldThread(target, { at = "latest", pin = [] } = {}) {
|
|
1622
|
+
const { threadKey, turns, state } = located(target);
|
|
1623
|
+
const cut = checkedTurn(at === "latest" ? turns.length - 1 : at, turns, 1);
|
|
1624
|
+
withPins(state, turns, pin, true);
|
|
1625
|
+
resetCut(state);
|
|
1626
|
+
state.cut = turnFingerprint(turns[cut]);
|
|
1627
|
+
state.keep = cut - 1;
|
|
1628
|
+
state.host = true;
|
|
1629
|
+
target.store.save(threadKey, state);
|
|
1630
|
+
return folded2(threadKey, turns, state);
|
|
1631
|
+
}
|
|
1632
|
+
function pinTurns(target, indices, pinned = true) {
|
|
1633
|
+
const { threadKey, turns, state } = located(target);
|
|
1634
|
+
withPins(state, turns, indices, pinned);
|
|
1635
|
+
target.store.save(threadKey, state);
|
|
1636
|
+
return folded2(threadKey, turns, state);
|
|
1637
|
+
}
|
|
1638
|
+
// src/core/usage.ts
|
|
1639
|
+
function usageIn(line) {
|
|
1640
|
+
if (!line.startsWith("data:") || !line.includes("message_start"))
|
|
1641
|
+
return null;
|
|
1642
|
+
try {
|
|
1643
|
+
return JSON.parse(line.slice(5)).message?.usage ?? null;
|
|
1644
|
+
} catch {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function trailingUsage(buffer) {
|
|
1649
|
+
if (!buffer.trim().startsWith("{"))
|
|
1650
|
+
return null;
|
|
1651
|
+
try {
|
|
1652
|
+
const json = JSON.parse(buffer);
|
|
1653
|
+
if (json.usage)
|
|
1654
|
+
return json.usage;
|
|
1655
|
+
return typeof json.input_tokens === "number" ? { input_tokens: json.input_tokens } : null;
|
|
1656
|
+
} catch {
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function scanEvents(buffer) {
|
|
1661
|
+
let rest = buffer;
|
|
1662
|
+
for (let newline = rest.indexOf(`
|
|
1663
|
+
`);newline !== -1; newline = rest.indexOf(`
|
|
1664
|
+
`)) {
|
|
1665
|
+
const usage = usageIn(rest.slice(0, newline).trim());
|
|
1666
|
+
rest = rest.slice(newline + 1);
|
|
1667
|
+
if (usage)
|
|
1668
|
+
return { usage, rest };
|
|
1669
|
+
}
|
|
1670
|
+
return { usage: null, rest };
|
|
1671
|
+
}
|
|
1672
|
+
async function readUsage(stream) {
|
|
1673
|
+
const reader = stream.pipeThrough(new TextDecoderStream).getReader();
|
|
1674
|
+
let buffer = "";
|
|
1675
|
+
let usage = null;
|
|
1676
|
+
while (true) {
|
|
1677
|
+
const { value, done } = await reader.read();
|
|
1678
|
+
if (done)
|
|
1679
|
+
return usage ?? trailingUsage(buffer);
|
|
1680
|
+
if (usage)
|
|
1681
|
+
continue;
|
|
1682
|
+
const scanned = scanEvents(buffer + value);
|
|
1683
|
+
usage = scanned.usage;
|
|
1684
|
+
buffer = scanned.rest.length > 4000000 ? "" : scanned.rest;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function watchUsage(response, onUsage) {
|
|
1688
|
+
if (!response.body)
|
|
1689
|
+
return response;
|
|
1690
|
+
const [toClient, toUs] = response.body.tee();
|
|
1691
|
+
readUsage(toUs).then((usage) => {
|
|
1692
|
+
if (usage)
|
|
1693
|
+
onUsage(usage);
|
|
1694
|
+
}).catch(() => {
|
|
1695
|
+
return;
|
|
1696
|
+
});
|
|
1697
|
+
return new Response(toClient, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
1698
|
+
}
|
|
1699
|
+
function inputTokens(usage) {
|
|
1700
|
+
return (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
1701
|
+
}
|
|
1702
|
+
var OVERFLOW_MARGIN = 1.1;
|
|
1703
|
+
var charsOf = (sent) => typeof sent === "number" ? sent : sizeOf(sent);
|
|
1704
|
+
function overflowTokens(errorText) {
|
|
1705
|
+
const tokens = Number(errorText.match(/prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)/i)?.[1]);
|
|
1706
|
+
return Number.isFinite(tokens) && tokens > 0 ? tokens : null;
|
|
1707
|
+
}
|
|
1708
|
+
function recordUsage(store, threadKey, sent, usage) {
|
|
1709
|
+
const input = inputTokens(usage);
|
|
1710
|
+
const chars = charsOf(sent);
|
|
1711
|
+
if (input <= 0 || chars <= 0)
|
|
1712
|
+
return false;
|
|
1713
|
+
const state = store.read(threadKey);
|
|
1714
|
+
state.ratio = input / chars;
|
|
1715
|
+
store.save(threadKey, state);
|
|
1716
|
+
return true;
|
|
1717
|
+
}
|
|
1718
|
+
function recordOverflow(store, threadKey, sent, errorText) {
|
|
1719
|
+
const tokens = overflowTokens(errorText);
|
|
1720
|
+
const chars = charsOf(sent);
|
|
1721
|
+
if (!tokens || chars <= 0)
|
|
1722
|
+
return null;
|
|
1723
|
+
const state = store.read(threadKey);
|
|
1724
|
+
state.ratio = Math.max(state.ratio ?? 0, tokens / chars) * OVERFLOW_MARGIN;
|
|
1725
|
+
store.save(threadKey, state);
|
|
1726
|
+
return tokens;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/core/window.ts
|
|
1730
|
+
var LOOKUP_TIMEOUT_MS = 5000;
|
|
1731
|
+
function lookupHeaders(requestHeaders) {
|
|
1732
|
+
const headers = new Headers;
|
|
1733
|
+
for (const name of ["authorization", "x-api-key", "anthropic-version", "anthropic-beta"]) {
|
|
1734
|
+
const value = requestHeaders.get(name);
|
|
1735
|
+
if (value)
|
|
1736
|
+
headers.set(name, value);
|
|
1737
|
+
}
|
|
1738
|
+
if (!headers.has("anthropic-version"))
|
|
1739
|
+
headers.set("anthropic-version", "2023-06-01");
|
|
1740
|
+
return headers;
|
|
1741
|
+
}
|
|
1742
|
+
function createWindowLookup({ known = new Map, onLearn } = {}) {
|
|
1743
|
+
const lookups = new Map;
|
|
1744
|
+
const lookup = async (id, requestHeaders, upstream) => {
|
|
1745
|
+
const response = await fetch(`${upstream}/v1/models/${encodeURIComponent(id)}`, { headers: lookupHeaders(requestHeaders), signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) });
|
|
1746
|
+
if (!response.ok)
|
|
1747
|
+
return null;
|
|
1748
|
+
const info = await response.json();
|
|
1749
|
+
if (typeof info.max_input_tokens !== "number" || info.max_input_tokens <= 0)
|
|
1750
|
+
return null;
|
|
1751
|
+
known.set(id, info.max_input_tokens);
|
|
1752
|
+
onLearn?.(known);
|
|
1753
|
+
return info.max_input_tokens;
|
|
1754
|
+
};
|
|
1755
|
+
return (model, requestHeaders, upstream = "https://api.anthropic.com") => {
|
|
1756
|
+
const id = String(model ?? "").replace(/\[[^\]]*\]$/, "");
|
|
1757
|
+
if (!id)
|
|
1758
|
+
return Promise.resolve(null);
|
|
1759
|
+
const window = known.get(id);
|
|
1760
|
+
if (window)
|
|
1761
|
+
return Promise.resolve(window);
|
|
1762
|
+
const inFlight = lookups.get(id);
|
|
1763
|
+
if (inFlight)
|
|
1764
|
+
return inFlight;
|
|
1765
|
+
const pending = lookup(id, requestHeaders, upstream).catch(() => null).finally(() => lookups.delete(id));
|
|
1766
|
+
lookups.set(id, pending);
|
|
1767
|
+
return pending;
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// src/core/forward.ts
|
|
1772
|
+
var COUNT_TOKENS_ATTEMPTS = 3;
|
|
1773
|
+
var DROPPED_REQUEST_HEADERS = ["host", "content-length", "accept-encoding"];
|
|
1774
|
+
var clipped = (error) => error.slice(0, 500);
|
|
1775
|
+
function claudeCodeSessionId(headers, body) {
|
|
1776
|
+
const header = headers.get("x-claude-code-session-id");
|
|
1777
|
+
if (header)
|
|
1778
|
+
return header;
|
|
1779
|
+
try {
|
|
1780
|
+
return JSON.parse(body.metadata?.user_id ?? "{}").session_id ?? null;
|
|
1781
|
+
} catch {
|
|
1782
|
+
return null;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
var defaultThreadId = (request, body) => request.headers.get("x-thread-id") ?? claudeCodeSessionId(request.headers, body);
|
|
1786
|
+
function forwardableHeaders(headers) {
|
|
1787
|
+
const out = new Headers(headers);
|
|
1788
|
+
out.delete("content-encoding");
|
|
1789
|
+
out.delete("content-length");
|
|
1790
|
+
return out;
|
|
1791
|
+
}
|
|
1792
|
+
function defaultPrepare(options, upstream) {
|
|
1793
|
+
const threadIdOf = options.threadIdOf ?? defaultThreadId;
|
|
1794
|
+
const given = options.window;
|
|
1795
|
+
const lookup = typeof given === "function" ? given : createWindowLookup();
|
|
1796
|
+
const windowFor = typeof given === "number" || given === null ? () => Promise.resolve(given) : (model, headers) => lookup(model, headers, upstream);
|
|
1797
|
+
return async (request, body, mayCompact) => {
|
|
1798
|
+
const threadId = threadIdOf(request, body);
|
|
1799
|
+
const window = await windowFor(body.model, request.headers);
|
|
1800
|
+
const sources = threadId ? options.sources?.(threadId, request, body) : undefined;
|
|
1801
|
+
return rewriteAsync({ body, threadId, config: options.config ?? CORE_DEFAULTS, store: options.store, sources, window, mayCompact }, options.reranker);
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
var passthrough = (bodyText) => ({ note: { action: "pass" }, threadKey: null, bodyText, sentSize: 0, givenSize: 0, fixedSize: 0 });
|
|
1805
|
+
async function outgoing(prepare, request, givenText, mayCompact) {
|
|
1806
|
+
try {
|
|
1807
|
+
const given = JSON.parse(givenText);
|
|
1808
|
+
const result = await prepare(request, given, mayCompact);
|
|
1809
|
+
const sentSize = sizeOf(result.body);
|
|
1810
|
+
return {
|
|
1811
|
+
note: result.note,
|
|
1812
|
+
threadKey: result.threadKey,
|
|
1813
|
+
bodyText: result.body !== given ? JSON.stringify(result.body, wellFormed) : givenText,
|
|
1814
|
+
sentSize,
|
|
1815
|
+
givenSize: result.body === given ? sentSize : sizeOf(given),
|
|
1816
|
+
fixedSize: sizeOf({ ...result.body, messages: [] })
|
|
1817
|
+
};
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
return { ...passthrough(givenText), note: { action: "error-passthrough", error: String(error) } };
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
function createSender(request, target, headers, timeout, stall) {
|
|
1823
|
+
return async (body) => {
|
|
1824
|
+
if (timeout === null)
|
|
1825
|
+
return fetch(target, { method: request.method, headers, body });
|
|
1826
|
+
for (;; ) {
|
|
1827
|
+
try {
|
|
1828
|
+
return await fetch(target, { method: request.method, headers, body, signal: AbortSignal.timeout(timeout) });
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
const timedOut = error instanceof Error && error.name === "TimeoutError";
|
|
1831
|
+
if (!timedOut || stall.count + 1 >= COUNT_TOKENS_ATTEMPTS)
|
|
1832
|
+
throw error;
|
|
1833
|
+
stall.count += 1;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function delivered(answer, out, metered, path, log, store) {
|
|
1839
|
+
const { note, threadKey, sentSize, givenSize, fixedSize } = out;
|
|
1840
|
+
const response = new Response(answer.body, { status: answer.status, statusText: answer.statusText, headers: forwardableHeaders(answer.headers) });
|
|
1841
|
+
if (note.action === "pass" || !metered) {
|
|
1842
|
+
if (note.action !== "pass" || Object.keys(note).length > 1)
|
|
1843
|
+
log({ path, status: answer.status, ...note });
|
|
1844
|
+
return response;
|
|
1845
|
+
}
|
|
1846
|
+
return watchUsage(response, (usage) => {
|
|
1847
|
+
log({
|
|
1848
|
+
path,
|
|
1849
|
+
status: answer.status,
|
|
1850
|
+
thread: threadKey,
|
|
1851
|
+
...note,
|
|
1852
|
+
input: inputTokens(usage),
|
|
1853
|
+
cacheRead: usage.cache_read_input_tokens ?? 0,
|
|
1854
|
+
cacheWrite: usage.cache_creation_input_tokens ?? 0,
|
|
1855
|
+
uncached: usage.input_tokens ?? 0,
|
|
1856
|
+
charsIn: givenSize,
|
|
1857
|
+
charsOut: sentSize,
|
|
1858
|
+
charsFixed: fixedSize
|
|
1859
|
+
});
|
|
1860
|
+
if (threadKey)
|
|
1861
|
+
recordUsage(store, threadKey, sentSize, usage);
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
function createForwarder(options) {
|
|
1865
|
+
const upstream = options.upstream ?? "https://api.anthropic.com";
|
|
1866
|
+
const prepare = options.prepare ?? defaultPrepare(options, upstream);
|
|
1867
|
+
const timeout = options.countTokensTimeoutMs ?? 1e4;
|
|
1868
|
+
async function route(request, began) {
|
|
1869
|
+
const stall = { count: 0 };
|
|
1870
|
+
let prep = 0;
|
|
1871
|
+
let upload = 0;
|
|
1872
|
+
const log = (entry) => options.record?.({ ...entry, ms: Date.now() - began, prep, upload, ...stall.count > 0 && { stalled: stall.count } });
|
|
1873
|
+
const url = new URL(request.url);
|
|
1874
|
+
const headers = new Headers(request.headers);
|
|
1875
|
+
for (const name of DROPPED_REQUEST_HEADERS)
|
|
1876
|
+
headers.delete(name);
|
|
1877
|
+
let givenText;
|
|
1878
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
1879
|
+
givenText = await request.text();
|
|
1880
|
+
upload = Date.now() - began;
|
|
1881
|
+
const isMessages = request.method === "POST" && url.pathname.startsWith("/v1/messages");
|
|
1882
|
+
const isCountTokens = url.pathname.includes("count_tokens");
|
|
1883
|
+
const build = () => givenText && isMessages ? outgoing(prepare, request, givenText, !isCountTokens) : Promise.resolve(passthrough(givenText));
|
|
1884
|
+
const send = createSender(request, upstream + url.pathname + url.search, headers, isCountTokens ? timeout : null, stall);
|
|
1885
|
+
let out = await build();
|
|
1886
|
+
prep = Date.now() - began - upload;
|
|
1887
|
+
let answer = await send(out.bodyText);
|
|
1888
|
+
const errorResponse = (text) => new Response(text, { status: answer.status, statusText: answer.statusText, headers: forwardableHeaders(answer.headers) });
|
|
1889
|
+
if (answer.status >= 400 && out.note.action !== "pass") {
|
|
1890
|
+
const errorText = await answer.text();
|
|
1891
|
+
const logged = { path: url.pathname, status: answer.status, thread: out.threadKey, ...out.note };
|
|
1892
|
+
const overflow = out.threadKey && !isCountTokens ? recordOverflow(options.store, out.threadKey, out.sentSize, errorText) : null;
|
|
1893
|
+
if (!overflow) {
|
|
1894
|
+
log({ ...logged, error: clipped(errorText) });
|
|
1895
|
+
return errorResponse(errorText);
|
|
1896
|
+
}
|
|
1897
|
+
log({ ...logged, overflow, error: clipped(errorText) });
|
|
1898
|
+
out = await build();
|
|
1899
|
+
out = { ...out, note: { ...out.note, healed: overflow } };
|
|
1900
|
+
answer = await send(out.bodyText);
|
|
1901
|
+
if (answer.status >= 400) {
|
|
1902
|
+
const retryError = await answer.text();
|
|
1903
|
+
log({ path: url.pathname, status: answer.status, thread: out.threadKey, ...out.note, error: clipped(retryError) });
|
|
1904
|
+
return errorResponse(retryError);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return delivered(answer, out, isMessages && !isCountTokens, url.pathname, log, options.store);
|
|
1908
|
+
}
|
|
1909
|
+
return async (request) => {
|
|
1910
|
+
const began = Date.now();
|
|
1911
|
+
try {
|
|
1912
|
+
return await route(request, began);
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
options.record?.({ path: new URL(request.url).pathname, status: 502, action: "upstream-unreachable", error: clipped(String(error)), ms: Date.now() - began });
|
|
1915
|
+
return Response.json({ type: "error", error: { type: "api_error", message: `eunoe could not reach the API: ${String(error)}` } }, { status: 502 });
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
// src/core/subagents.ts
|
|
1920
|
+
var closingTurn2 = { role: "user", content: "" };
|
|
1921
|
+
function launchTurnOf(parentMessages, toolUseId) {
|
|
1922
|
+
if (!toolUseId)
|
|
1923
|
+
return 0;
|
|
1924
|
+
const index = splitTurns(parentMessages).turns.findIndex((turn) => turn.some((message) => message.role === "assistant" && blocks(message).some((b) => b.type === "tool_use" && b.id === toolUseId)));
|
|
1925
|
+
return index + 1;
|
|
1926
|
+
}
|
|
1927
|
+
var subagentLabel = (description) => `subagent "${description}"`;
|
|
1928
|
+
function subagentRecall(messages, description, parentTurn) {
|
|
1929
|
+
const from = subagentLabel(description);
|
|
1930
|
+
return recallChunks([...messages, closingTurn2], new Map).map((chunk) => ({ ...chunk, from, turn: parentTurn }));
|
|
1931
|
+
}
|
|
1932
|
+
function listSubagentRecall(parentMessages, subagents) {
|
|
1933
|
+
return subagents.flatMap((subagent) => subagentRecall(subagent.messages, subagent.description, launchTurnOf(parentMessages, subagent.toolUseId)));
|
|
1934
|
+
}
|
|
1935
|
+
// src/transcript/format.ts
|
|
1936
|
+
var extension = (format) => format === "xml" ? "xml" : "md";
|
|
1937
|
+
var padded = (n) => String(n).padStart(4, "0");
|
|
1938
|
+
var calls = (count) => `${count} tool call${count === 1 ? "" : "s"}`;
|
|
1939
|
+
function turnFile(turn, format) {
|
|
1940
|
+
return `turn-${padded(turn.number)}.${extension(format)}`;
|
|
1941
|
+
}
|
|
1942
|
+
function subagentFile(parentTurn, k, format) {
|
|
1943
|
+
return `turn-${padded(parentTurn)}-subagent-${k}.${extension(format)}`;
|
|
1944
|
+
}
|
|
1945
|
+
function toolMarkdown(part, total) {
|
|
1946
|
+
return [
|
|
1947
|
+
`### [tool ${part.n}/${total}] ${part.name}: ${part.call}`,
|
|
1948
|
+
part.record ? `Full record of this subagent run: ${part.record}` : "",
|
|
1949
|
+
part.body,
|
|
1950
|
+
part.output ? `[output of tool ${part.n}${part.error ? ", error" : ""}]
|
|
1951
|
+
\`\`\`
|
|
1952
|
+
${part.output}
|
|
1953
|
+
\`\`\`` : `[tool ${part.n}: no output]`
|
|
1954
|
+
].filter(Boolean).join(`
|
|
1955
|
+
|
|
1956
|
+
`);
|
|
1957
|
+
}
|
|
1958
|
+
function workOf(turn) {
|
|
1959
|
+
const work = turn.parts.map((part) => part.kind === "say" ? `> ${part.text.replace(/\n/g, `
|
|
1960
|
+
> `)}` : toolMarkdown(part, turn.calls));
|
|
1961
|
+
return work.join(`
|
|
1962
|
+
|
|
1963
|
+
`) || "_No tool calls._";
|
|
1964
|
+
}
|
|
1965
|
+
function renderMarkdown(turn) {
|
|
1966
|
+
return [
|
|
1967
|
+
`# Turn ${turn.number}${turn.time ? ` · ${turn.time}` : ""}`,
|
|
1968
|
+
"",
|
|
1969
|
+
"## User",
|
|
1970
|
+
"",
|
|
1971
|
+
turn.prompt,
|
|
1972
|
+
"",
|
|
1973
|
+
`## Work (${calls(turn.calls)})`,
|
|
1974
|
+
"",
|
|
1975
|
+
workOf(turn),
|
|
1976
|
+
"",
|
|
1977
|
+
"## Final reply",
|
|
1978
|
+
"",
|
|
1979
|
+
turn.final || "_No final reply: the turn was interrupted or is still running._",
|
|
1980
|
+
""
|
|
1981
|
+
].join(`
|
|
1982
|
+
`);
|
|
1983
|
+
}
|
|
1984
|
+
function attr(value) {
|
|
1985
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/\n/g, " ");
|
|
1986
|
+
}
|
|
1987
|
+
function toolXml(part, total) {
|
|
1988
|
+
return [
|
|
1989
|
+
`<tool n="${part.n}" of="${total}" name="${attr(part.name)}">`,
|
|
1990
|
+
`<call>${part.call.replace(/^`|`$/g, "")}</call>`,
|
|
1991
|
+
part.record ? `<record>${part.record}</record>` : "",
|
|
1992
|
+
part.body ? `<input>
|
|
1993
|
+
${part.body}
|
|
1994
|
+
</input>` : "",
|
|
1995
|
+
part.output ? `<output${part.error ? ' error="true"' : ""}>
|
|
1996
|
+
${part.output}
|
|
1997
|
+
</output>` : "<output/>",
|
|
1998
|
+
"</tool>"
|
|
1999
|
+
].filter(Boolean).join(`
|
|
2000
|
+
`);
|
|
2001
|
+
}
|
|
2002
|
+
function renderXml(turn) {
|
|
2003
|
+
return [
|
|
2004
|
+
`<turn n="${turn.number}"${turn.time ? ` time="${turn.time}"` : ""} tool_calls="${turn.calls}">`,
|
|
2005
|
+
`<user>
|
|
2006
|
+
${turn.prompt}
|
|
2007
|
+
</user>`,
|
|
2008
|
+
...turn.parts.map((part) => part.kind === "say" ? `<said>
|
|
2009
|
+
${part.text}
|
|
2010
|
+
</said>` : toolXml(part, turn.calls)),
|
|
2011
|
+
turn.final ? `<final_reply>
|
|
2012
|
+
${turn.final}
|
|
2013
|
+
</final_reply>` : `<final_reply interrupted="true"/>`,
|
|
2014
|
+
"</turn>",
|
|
2015
|
+
""
|
|
2016
|
+
].join(`
|
|
2017
|
+
`);
|
|
2018
|
+
}
|
|
2019
|
+
var finalOf = (final) => final ? clip(final, 160) : "_(no final reply)_";
|
|
2020
|
+
var turnLine = (turn, format) => `- **${turnFile(turn, format)}** (${turn.time}, ${turn.calls} calls): ${clip(turn.prompt, 140)} → ${finalOf(turn.final)}`;
|
|
2021
|
+
var recordLine = (record) => {
|
|
2022
|
+
const total = record.turns.reduce((sum, turn) => sum + turn.calls, 0);
|
|
2023
|
+
return ` - **${record.file}**: subagent "${clip(record.run.description, 140)}" (${total} calls) → ${finalOf(record.turns.at(-1)?.final)}`;
|
|
2024
|
+
};
|
|
2025
|
+
function renderIndex(turns, format, records = []) {
|
|
2026
|
+
const ext = extension(format);
|
|
2027
|
+
const subagentNote = records.length > 0 ? ` Indented lines are subagents the agent launched in that turn; turn-NNNN-subagent-K.${ext} holds the subagent's whole run, which the turn itself only has the report of.` : "";
|
|
2028
|
+
return [
|
|
2029
|
+
"# Session index",
|
|
2030
|
+
"",
|
|
2031
|
+
`One line per turn: what the user asked → how the turn ended. Open turn-NNNN.${ext} for everything that happened in it.${subagentNote}`,
|
|
2032
|
+
"",
|
|
2033
|
+
...turns.flatMap((turn) => [turnLine(turn, format), ...records.filter((record) => record.parentTurn === turn.number).map(recordLine)]),
|
|
2034
|
+
...records.filter((record) => record.parentTurn === 0).map(recordLine),
|
|
2035
|
+
""
|
|
2036
|
+
].join(`
|
|
2037
|
+
`);
|
|
2038
|
+
}
|
|
2039
|
+
function renderSubagentMarkdown(record) {
|
|
2040
|
+
const parent = record.parentTurn || "?";
|
|
2041
|
+
const body = record.turns.flatMap((turn, i) => [
|
|
2042
|
+
record.turns.length > 1 ? `## Message ${i + 1}` : "## Task",
|
|
2043
|
+
"",
|
|
2044
|
+
turn.prompt,
|
|
2045
|
+
"",
|
|
2046
|
+
`## Work (${calls(turn.calls)})`,
|
|
2047
|
+
"",
|
|
2048
|
+
workOf(turn),
|
|
2049
|
+
"",
|
|
2050
|
+
"## Final reply",
|
|
2051
|
+
"",
|
|
2052
|
+
turn.final || "_No final reply: the run was interrupted or is still going._",
|
|
2053
|
+
""
|
|
2054
|
+
]);
|
|
2055
|
+
return [`# Subagent of turn ${parent} · "${record.run.description}"`, `This is the full record of a subagent launched from turn ${parent}; that turn only holds its final report.`, "", ...body].join(`
|
|
2056
|
+
`);
|
|
2057
|
+
}
|
|
2058
|
+
function renderSubagentXml(record) {
|
|
2059
|
+
return [`<subagent parent_turn="${record.parentTurn}" description="${attr(record.run.description)}">`, ...record.turns.map(renderXml), "</subagent>", ""].join(`
|
|
2060
|
+
`);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/transcript/parse.ts
|
|
2064
|
+
var OUTPUT_CAP = 40000;
|
|
2065
|
+
function promptText(content) {
|
|
2066
|
+
return content.filter((block) => block.type === "text").map((block) => block.text ?? "").join(`
|
|
2067
|
+
`).replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").replace(/<system-message>[\s\S]*?<\/system-message>/g, "").trim();
|
|
2068
|
+
}
|
|
2069
|
+
function isPrompt2(entry) {
|
|
2070
|
+
if (entry.type !== "user" || entry.isMeta || entry.isCompactSummary)
|
|
2071
|
+
return false;
|
|
2072
|
+
const content = contentBlocks(entry.message?.content);
|
|
2073
|
+
if (content.some((block) => block.type === "tool_result"))
|
|
2074
|
+
return false;
|
|
2075
|
+
return Boolean(promptText(content)) || content.some((block) => block.type === "image");
|
|
2076
|
+
}
|
|
2077
|
+
function describeCall(block) {
|
|
2078
|
+
const input = block.input ?? {};
|
|
2079
|
+
if (typeof input.command === "string")
|
|
2080
|
+
return `\`${input.command.replace(/\s+/g, " ").slice(0, 300)}\``;
|
|
2081
|
+
if (typeof input.file_path === "string")
|
|
2082
|
+
return input.file_path;
|
|
2083
|
+
if (typeof input.pattern === "string")
|
|
2084
|
+
return `"${input.pattern.slice(0, 120)}"${input.path ? ` in ${input.path}` : ""}`;
|
|
2085
|
+
if (typeof input.url === "string")
|
|
2086
|
+
return input.url;
|
|
2087
|
+
if (typeof input.description === "string")
|
|
2088
|
+
return input.description.slice(0, 160);
|
|
2089
|
+
return JSON.stringify(input).slice(0, 200);
|
|
2090
|
+
}
|
|
2091
|
+
function callBody(block) {
|
|
2092
|
+
const input = block.input ?? {};
|
|
2093
|
+
if (typeof input.new_string === "string")
|
|
2094
|
+
return `Replaced:
|
|
2095
|
+
\`\`\`
|
|
2096
|
+
${input.old_string ?? ""}
|
|
2097
|
+
\`\`\`
|
|
2098
|
+
With:
|
|
2099
|
+
\`\`\`
|
|
2100
|
+
${input.new_string}
|
|
2101
|
+
\`\`\``;
|
|
2102
|
+
if (typeof input.content === "string" && block.name === "Write")
|
|
2103
|
+
return `\`\`\`
|
|
2104
|
+
${input.content.slice(0, OUTPUT_CAP)}
|
|
2105
|
+
\`\`\``;
|
|
2106
|
+
return "";
|
|
2107
|
+
}
|
|
2108
|
+
function outputOf(block) {
|
|
2109
|
+
const content = typeof block.content === "string" ? block.content : contentBlocks(block.content).map((part) => part.type === "text" ? part.text : `[${part.type}]`).join(`
|
|
2110
|
+
`);
|
|
2111
|
+
return content.length > OUTPUT_CAP ? `${content.slice(0, OUTPUT_CAP)}
|
|
2112
|
+
… [${content.length - OUTPUT_CAP} more characters]` : content;
|
|
2113
|
+
}
|
|
2114
|
+
function promptTurn(entry, number) {
|
|
2115
|
+
const text = promptText(contentBlocks(entry.message?.content));
|
|
2116
|
+
const time = String(entry.timestamp ?? "").slice(0, 16).replace("T", " ");
|
|
2117
|
+
return { number, time, prompt: text || "[image]", parts: [], final: "", calls: 0 };
|
|
2118
|
+
}
|
|
2119
|
+
function recordAssistant(turn, content, pending, narration) {
|
|
2120
|
+
let said = narration;
|
|
2121
|
+
for (const block of content) {
|
|
2122
|
+
if (block.type === "text" && String(block.text).trim()) {
|
|
2123
|
+
said = String(block.text).trim();
|
|
2124
|
+
turn.final = said;
|
|
2125
|
+
}
|
|
2126
|
+
if (block.type !== "tool_use")
|
|
2127
|
+
continue;
|
|
2128
|
+
turn.calls += 1;
|
|
2129
|
+
if (said) {
|
|
2130
|
+
turn.parts.push({ kind: "say", text: said });
|
|
2131
|
+
said = "";
|
|
2132
|
+
}
|
|
2133
|
+
turn.final = "";
|
|
2134
|
+
const call = {
|
|
2135
|
+
kind: "tool",
|
|
2136
|
+
n: turn.calls,
|
|
2137
|
+
id: String(block.id ?? ""),
|
|
2138
|
+
name: block.name,
|
|
2139
|
+
prompt: typeof block.input?.prompt === "string" ? block.input.prompt : undefined,
|
|
2140
|
+
call: describeCall(block),
|
|
2141
|
+
body: callBody(block),
|
|
2142
|
+
output: "",
|
|
2143
|
+
error: false
|
|
2144
|
+
};
|
|
2145
|
+
pending.set(block.id, call);
|
|
2146
|
+
turn.parts.push(call);
|
|
2147
|
+
}
|
|
2148
|
+
return said;
|
|
2149
|
+
}
|
|
2150
|
+
function recordResults(content, pending) {
|
|
2151
|
+
for (const block of content) {
|
|
2152
|
+
if (block.type !== "tool_result")
|
|
2153
|
+
continue;
|
|
2154
|
+
const call = pending.get(block.tool_use_id);
|
|
2155
|
+
if (!call)
|
|
2156
|
+
continue;
|
|
2157
|
+
pending.delete(block.tool_use_id);
|
|
2158
|
+
call.output = outputOf(block).trim();
|
|
2159
|
+
call.error = Boolean(block.is_error);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
function parseTurns(entries, sidechain = false) {
|
|
2163
|
+
const turns = [];
|
|
2164
|
+
const pending = new Map;
|
|
2165
|
+
let narration = "";
|
|
2166
|
+
for (const entry of entries) {
|
|
2167
|
+
if (sidechain !== "any" && Boolean(entry.isSidechain) !== sidechain)
|
|
2168
|
+
continue;
|
|
2169
|
+
if (isPrompt2(entry)) {
|
|
2170
|
+
turns.push(promptTurn(entry, turns.length + 1));
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
const turn = turns.at(-1);
|
|
2174
|
+
if (!turn)
|
|
2175
|
+
continue;
|
|
2176
|
+
const content = contentBlocks(entry.message?.content);
|
|
2177
|
+
if (entry.type === "system" && entry.subtype === "compact_boundary")
|
|
2178
|
+
turn.parts.push({ kind: "say", text: "(Claude Code compacted the context here.)" });
|
|
2179
|
+
else if (entry.type === "assistant")
|
|
2180
|
+
narration = recordAssistant(turn, content, pending, narration);
|
|
2181
|
+
else if (entry.type === "user")
|
|
2182
|
+
recordResults(content, pending);
|
|
2183
|
+
}
|
|
2184
|
+
return turns;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// src/core/transcript.ts
|
|
2188
|
+
var isNote = (message) => message.role === "user" && !isPrompt(message) && !blocks(message).some((b) => b.type === "tool_result");
|
|
2189
|
+
var turnsOf = (messages, sidechain = false) => parseTurns(messages.map((message) => ({ type: message.role, isMeta: isNote(message), message })), sidechain);
|
|
2190
|
+
function subagentRecords(messages, turns, subagents, format) {
|
|
2191
|
+
const perTurn = new Map;
|
|
2192
|
+
return subagents.map((subagent) => {
|
|
2193
|
+
const parentTurn = launchTurnOf(messages, subagent.toolUseId);
|
|
2194
|
+
const k = (perTurn.get(parentTurn) ?? 0) + 1;
|
|
2195
|
+
perTurn.set(parentTurn, k);
|
|
2196
|
+
const file = subagentFile(parentTurn, k, format);
|
|
2197
|
+
const launcher = turns[parentTurn - 1]?.parts.find((part) => part.kind === "tool" && part.id === subagent.toolUseId);
|
|
2198
|
+
if (launcher?.kind === "tool")
|
|
2199
|
+
launcher.record = file;
|
|
2200
|
+
return { run: { description: subagent.description }, turns: turnsOf(subagent.messages, "any"), parentTurn, file };
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
function renderTranscriptFiles(messages, { format = "markdown", subagents = [] } = {}) {
|
|
2204
|
+
const turns = turnsOf(messages);
|
|
2205
|
+
const records = subagentRecords(messages, turns, subagents, format);
|
|
2206
|
+
const files = {};
|
|
2207
|
+
for (const turn of turns)
|
|
2208
|
+
files[turnFile(turn, format)] = format === "xml" ? renderXml(turn) : renderMarkdown(turn);
|
|
2209
|
+
for (const record of records)
|
|
2210
|
+
files[record.file] = format === "xml" ? renderSubagentXml(record) : renderSubagentMarkdown(record);
|
|
2211
|
+
files["index.md"] = renderIndex(turns, format, records);
|
|
2212
|
+
return files;
|
|
2213
|
+
}
|
|
2214
|
+
var transcriptGuide = (dir, format = "markdown") => searchGuide({ mode: format, dir, jsonl: "" });
|
|
2215
|
+
export {
|
|
2216
|
+
CORE_DEFAULTS,
|
|
2217
|
+
claudeCodeSessionId,
|
|
2218
|
+
continueToken,
|
|
2219
|
+
createForwarder,
|
|
2220
|
+
createWindowLookup,
|
|
2221
|
+
foldThread,
|
|
2222
|
+
inputTokens,
|
|
2223
|
+
jevReranker,
|
|
2224
|
+
listSubagentRecall,
|
|
2225
|
+
memoryStore,
|
|
2226
|
+
pinTurns,
|
|
2227
|
+
recordOverflow,
|
|
2228
|
+
recordUsage,
|
|
2229
|
+
renderTranscriptFiles,
|
|
2230
|
+
rewrite,
|
|
2231
|
+
rewriteAsync,
|
|
2232
|
+
threadKeyOf,
|
|
2233
|
+
transcriptGuide,
|
|
2234
|
+
watchUsage
|
|
2235
|
+
};
|