@qwen-code/arena-agent 0.0.10-dev.1 → 0.0.10-dev.11
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/.env.example +68 -14
- package/CHANGELOG.md +245 -0
- package/README.md +307 -57
- package/dist/agent.d.ts +28 -2
- package/dist/agent.js +389 -109
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.js +32 -35
- package/dist/anthropic.js.map +1 -1
- package/dist/ask-user.js +2 -0
- package/dist/ask-user.js.map +1 -1
- package/dist/attachments.js +4 -3
- package/dist/attachments.js.map +1 -1
- package/dist/brave-search.js +1 -3
- package/dist/brave-search.js.map +1 -1
- package/dist/canonical-data.d.ts +2 -0
- package/dist/canonical-data.js +4 -2
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +184 -36
- package/dist/cli.js.map +1 -1
- package/dist/code-tools.d.ts +8 -2
- package/dist/code-tools.js +709 -117
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction.d.ts +50 -3
- package/dist/compaction.js +390 -185
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +23 -2
- package/dist/config.js +52 -5
- package/dist/config.js.map +1 -1
- package/dist/connector-mcp.d.ts +47 -0
- package/dist/connector-mcp.js +314 -0
- package/dist/connector-mcp.js.map +1 -0
- package/dist/connectors.d.ts +8 -0
- package/dist/connectors.js +57 -0
- package/dist/connectors.js.map +1 -0
- package/dist/fetch-page.js +9 -33
- package/dist/fetch-page.js.map +1 -1
- package/dist/file-edit.d.ts +8 -0
- package/dist/file-edit.js +63 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/history.d.ts +16 -3
- package/dist/history.js +117 -19
- package/dist/history.js.map +1 -1
- package/dist/midturn-checkpoint-hook.d.ts +44 -0
- package/dist/midturn-checkpoint-hook.js +38 -0
- package/dist/midturn-checkpoint-hook.js.map +1 -0
- package/dist/mock.js +5 -0
- package/dist/mock.js.map +1 -1
- package/dist/mode.js +4 -2
- package/dist/mode.js.map +1 -1
- package/dist/model-router-media.d.ts +1 -0
- package/dist/model-router-media.js +121 -1
- package/dist/model-router-media.js.map +1 -1
- package/dist/openai-responses.js +2 -0
- package/dist/openai-responses.js.map +1 -1
- package/dist/process-manager.js +2 -1
- package/dist/process-manager.js.map +1 -1
- package/dist/prompts.d.ts +2 -1
- package/dist/prompts.js +15 -37
- package/dist/prompts.js.map +1 -1
- package/dist/protocol.d.ts +5 -1
- package/dist/protocol.js +24 -2
- package/dist/protocol.js.map +1 -1
- package/dist/provider.d.ts +12 -5
- package/dist/provider.js +63 -52
- package/dist/provider.js.map +1 -1
- package/dist/repetitive-output.d.ts +12 -0
- package/dist/repetitive-output.js +80 -0
- package/dist/repetitive-output.js.map +1 -0
- package/dist/replay-responses.d.ts +17 -0
- package/dist/replay-responses.js +100 -0
- package/dist/replay-responses.js.map +1 -0
- package/dist/routify-web-fetch.js +20 -7
- package/dist/routify-web-fetch.js.map +1 -1
- package/dist/sandbox-file-write.d.ts +10 -0
- package/dist/sandbox-file-write.js +58 -0
- package/dist/sandbox-file-write.js.map +1 -0
- package/dist/server.js +17 -5
- package/dist/server.js.map +1 -1
- package/dist/session-memory-prompts.d.ts +5 -0
- package/dist/session-memory-prompts.js +7 -0
- package/dist/session-memory-prompts.js.map +1 -0
- package/dist/snapshot-client.d.ts +10 -0
- package/dist/snapshot-client.js +39 -0
- package/dist/snapshot-client.js.map +1 -0
- package/dist/spec.d.ts +5 -4
- package/dist/spec.js +31 -17
- package/dist/spec.js.map +1 -1
- package/dist/speech-audio.d.ts +9 -0
- package/dist/speech-audio.js +130 -0
- package/dist/speech-audio.js.map +1 -0
- package/dist/speech-model.d.ts +3 -0
- package/dist/speech-model.js +8 -0
- package/dist/speech-model.js.map +1 -0
- package/dist/summary-compaction.d.ts +21 -0
- package/dist/summary-compaction.js +211 -0
- package/dist/summary-compaction.js.map +1 -0
- package/dist/tools.d.ts +8 -1
- package/dist/tools.js +407 -135
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +13 -10
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +30 -3
- package/dist/voice-registry.d.ts +2 -0
- package/dist/voice-registry.js +10 -2
- package/dist/voice-registry.js.map +1 -1
- package/dist/web-fetch-output.d.ts +9 -0
- package/dist/web-fetch-output.js +38 -0
- package/dist/web-fetch-output.js.map +1 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.js +11 -6
- package/dist/workspace.js.map +1 -1
- package/docs/BINARY_DISTRIBUTION.md +27 -16
- package/docs/MODES_AND_RELEASES.md +60 -6
- package/docs/SDK.md +5 -0
- package/modes/code-arena/capture-provenance.json +57 -0
- package/modes/code-arena/full-stack/system-prompt.txt +1 -1
- package/modes/code-arena/full-stack/template/eslint.config.mjs +3 -4
- package/modes/code-arena/full-stack/template/postcss.config.mjs +2 -2
- package/modes/code-arena/full-stack/tools.json +186 -186
- package/modes/code-arena/html/system-prompt.txt +1 -1
- package/modes/code-arena/html/tools.json +123 -123
- package/modes/code-arena/react/system-prompt.txt +4 -3
- package/modes/code-arena/react/template/package.json +4 -4
- package/modes/code-arena/react/template/src/App.tsx +1 -1
- package/modes/code-arena/react/template/src/main.tsx +1 -1
- package/modes/code-arena/react/template/tsconfig.json +6 -0
- package/modes/code-arena/react/template/vite.config.ts +1 -0
- package/modes/code-arena/react/tools.json +178 -184
- package/package.json +15 -4
- package/scripts/README.md +80 -0
- package/scripts/check-changelog.mjs +66 -0
- package/vendor/audio/darwin-arm64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/darwin-arm64/LAME.LICENSE +481 -0
- package/vendor/audio/darwin-arm64/Opus.LICENSE +44 -0
- package/vendor/audio/darwin-arm64/ffmpeg.gz +0 -0
- package/vendor/audio/darwin-arm64/manifest.json +39 -0
- package/vendor/audio/darwin-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/darwin-x64/LAME.LICENSE +481 -0
- package/vendor/audio/darwin-x64/Opus.LICENSE +44 -0
- package/vendor/audio/darwin-x64/ffmpeg.gz +0 -0
- package/vendor/audio/darwin-x64/manifest.json +39 -0
- package/vendor/audio/linux-arm64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/linux-arm64/LAME.LICENSE +481 -0
- package/vendor/audio/linux-arm64/Opus.LICENSE +44 -0
- package/vendor/audio/linux-arm64/ffmpeg.gz +0 -0
- package/vendor/audio/linux-arm64/manifest.json +39 -0
- package/vendor/audio/linux-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/linux-x64/LAME.LICENSE +481 -0
- package/vendor/audio/linux-x64/Opus.LICENSE +44 -0
- package/vendor/audio/linux-x64/ffmpeg.gz +0 -0
- package/vendor/audio/linux-x64/manifest.json +39 -0
- package/vendor/audio/sources/build-audio-encoder.sh +76 -0
- package/vendor/audio/sources/ffmpeg-7.0.2.tar.xz +0 -0
- package/vendor/audio/sources/lame-3.100.tar.gz +0 -0
- package/vendor/audio/sources/opus-1.5.2.tar.gz +0 -0
- package/vendor/audio/sources/pkgconf-2.3.0.tar.xz +0 -0
- package/vendor/audio/sources/sources.lock.json +30 -0
- package/vendor/audio/win32-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/win32-x64/LAME.LICENSE +481 -0
- package/vendor/audio/win32-x64/Opus.LICENSE +44 -0
- package/vendor/audio/win32-x64/ffmpeg.gz +0 -0
- package/vendor/audio/win32-x64/manifest.json +39 -0
package/dist/compaction.js
CHANGED
|
@@ -1,207 +1,412 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { Tiktoken } from "js-tiktoken/lite";
|
|
3
|
+
import o200kBase from "js-tiktoken/ranks/o200k_base";
|
|
4
|
+
import { estimateContextTokens, continuationSummaryBlock as summaryBlock, compactMessagesIfNeeded as compactSummary } from "./summary-compaction.js";
|
|
5
|
+
import { memoryBudgetNoticeTemplate, emptySessionMemory, memoryContinuationPreamble, memorySystemPrompt, memoryUpdateTemplate } from "./session-memory-prompts.js";
|
|
6
|
+
export { estimateContextTokens };
|
|
7
|
+
/** Threshold with the default 1M context window and 0.25 trigger ratio. */
|
|
8
|
+
export const AGENT_ARENA_COMPACTION_THRESHOLD_TOKENS = 250_000;
|
|
9
|
+
export function storedSessionMemory(cache) {
|
|
10
|
+
if (!cache.latest && !cache.update)
|
|
11
|
+
return undefined;
|
|
12
|
+
return structuredClone({ version: 1, ...(cache.latest ? { latest: cache.latest } : {}),
|
|
13
|
+
...(cache.update ? { update: cache.update } : {}) });
|
|
14
|
+
}
|
|
15
|
+
export async function settleSessionMemory(cache, cancel = true) {
|
|
16
|
+
if (cancel)
|
|
17
|
+
cache.controller?.abort(new Error("Memory update cancelled at run boundary"));
|
|
18
|
+
await cache.pending;
|
|
19
|
+
}
|
|
20
|
+
const descriptions = new Map(emptySessionMemory.split("\n\n").map(section => {
|
|
21
|
+
const [header, description] = section.split("\n");
|
|
22
|
+
return [header, description];
|
|
23
|
+
}));
|
|
24
|
+
const headers = [...descriptions.keys()];
|
|
25
|
+
const budget = 1500;
|
|
26
|
+
function escapedMemory(text) {
|
|
27
|
+
return text.trim().replace(/<\/?(?:arena-system-message|session_memory)>/gi, tag => tag.replace("<", "<").replace(">", ">"));
|
|
28
|
+
}
|
|
29
|
+
/** Parse top-level section replacements. Code-fenced headers are content. */
|
|
30
|
+
function sections(text, strict = true) {
|
|
31
|
+
const found = new Map();
|
|
32
|
+
let header;
|
|
33
|
+
let body = [];
|
|
34
|
+
let fence;
|
|
35
|
+
const flush = () => { if (header)
|
|
36
|
+
found.set(header, body.join("\n").trim()); };
|
|
37
|
+
for (const line of text.trim().split("\n")) {
|
|
38
|
+
const marker = /^\s{0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
|
39
|
+
if (marker) {
|
|
40
|
+
if (!fence)
|
|
41
|
+
fence = { character: marker[0], length: marker.length };
|
|
42
|
+
else if (marker[0] === fence.character && marker.length >= fence.length && /^\s{0,3}(?:`+|~+)\s*$/.test(line))
|
|
43
|
+
fence = undefined;
|
|
44
|
+
}
|
|
45
|
+
if (!fence && /^# /.test(line) && (strict || descriptions.has(line))) {
|
|
46
|
+
if (!descriptions.has(line) || found.has(line) || header === line)
|
|
47
|
+
throw new Error("Invalid session memory section");
|
|
48
|
+
flush();
|
|
49
|
+
header = line;
|
|
50
|
+
body = [];
|
|
51
|
+
}
|
|
52
|
+
else if (header)
|
|
53
|
+
body.push(line);
|
|
54
|
+
else if (line.trim())
|
|
55
|
+
throw new Error("Session memory patch must begin with a section header");
|
|
56
|
+
}
|
|
57
|
+
if (fence)
|
|
58
|
+
throw new Error("Unclosed code fence in session memory patch");
|
|
59
|
+
flush();
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
function render(values) {
|
|
63
|
+
return headers.map(header => {
|
|
64
|
+
let body = values.get(header) || "";
|
|
65
|
+
const description = descriptions.get(header);
|
|
66
|
+
// The server-owned descriptions survive replacement; the model is told not to emit them.
|
|
67
|
+
if (body.startsWith(description))
|
|
68
|
+
body = body.slice(description.length).trim();
|
|
69
|
+
return `${header}\n${description}${body ? `\n\n${body}` : ""}`;
|
|
70
|
+
}).join("\n\n");
|
|
71
|
+
}
|
|
72
|
+
export function mergeSessionMemory(memory, patch) {
|
|
73
|
+
const changes = sections(patch);
|
|
74
|
+
if (!changes.get("# Current State"))
|
|
75
|
+
throw new Error("Missing Current State in session memory patch");
|
|
76
|
+
const previous = sections(memory, false);
|
|
77
|
+
for (const [header, body] of changes) {
|
|
78
|
+
if (!body)
|
|
79
|
+
throw new Error("Empty session memory replacement");
|
|
80
|
+
previous.set(header, escapedMemory(body));
|
|
44
81
|
}
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
return render(previous);
|
|
83
|
+
}
|
|
84
|
+
let memoryEncoder;
|
|
85
|
+
/** o200k_base matches all 45 captured section counts; unrelated to context estimation. */
|
|
86
|
+
export function memoryTokenEstimate(text) {
|
|
87
|
+
memoryEncoder ??= new Tiktoken(o200kBase);
|
|
88
|
+
// Memory may contain literal special-token spellings; count them as ordinary text.
|
|
89
|
+
return memoryEncoder.encode(text, [], []).length;
|
|
90
|
+
}
|
|
91
|
+
function inputTokens(usage) {
|
|
92
|
+
for (const key of ["prompt_tokens", "input_tokens"]) {
|
|
93
|
+
const value = usage?.[key];
|
|
94
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0)
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
function completedRequestTokens(usage) {
|
|
100
|
+
const total = usage?.total_tokens;
|
|
101
|
+
if (typeof total === "number" && Number.isFinite(total) && total > 0)
|
|
102
|
+
return total;
|
|
103
|
+
const input = inputTokens(usage);
|
|
104
|
+
if (input === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
for (const key of ["completion_tokens", "output_tokens"]) {
|
|
107
|
+
const output = usage?.[key];
|
|
108
|
+
if (typeof output === "number" && Number.isFinite(output) && output >= 0) {
|
|
109
|
+
return input + output;
|
|
110
|
+
}
|
|
53
111
|
}
|
|
112
|
+
return undefined;
|
|
54
113
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
114
|
+
function visibleContextTokens(messages, tools, imageTokenEstimate) {
|
|
115
|
+
// Usage is persisted for accounting but stripped at every provider wire boundary.
|
|
116
|
+
const visible = messages.map(message => {
|
|
117
|
+
if (message.role !== "assistant" || !message.usage)
|
|
118
|
+
return message;
|
|
119
|
+
const { usage: _usage, ...rest } = message;
|
|
120
|
+
return rest;
|
|
121
|
+
});
|
|
122
|
+
return estimateContextTokens(visible, tools, imageTokenEstimate);
|
|
123
|
+
}
|
|
124
|
+
function hasMemoryOnlyPrefix(messages, assistantIndex) {
|
|
125
|
+
const firstIndex = messages[0]?.role === "system" ? 1 : 0;
|
|
126
|
+
const first = messages[firstIndex];
|
|
127
|
+
if (first?.role !== "user")
|
|
128
|
+
return false;
|
|
129
|
+
const parts = typeof first.content === "string" ? [{ text: first.content }] : first.content;
|
|
130
|
+
if (parts.length !== 1 || !("text" in parts[0]))
|
|
131
|
+
return false;
|
|
132
|
+
const text = parts[0].text.trim();
|
|
133
|
+
if (!text.startsWith("<arena-system-message>\n") || !text.endsWith("</arena-system-message>"))
|
|
134
|
+
return false;
|
|
135
|
+
if (messages.slice(firstIndex + 1, assistantIndex).some(message => message.role === "user"))
|
|
136
|
+
return false;
|
|
137
|
+
return messages.findIndex((message, index) => index > firstIndex && message.role === "assistant") === assistantIndex;
|
|
138
|
+
}
|
|
139
|
+
/** Provider-reported completed-request usage plus an estimate of later messages. */
|
|
140
|
+
export function estimateCompactionTokens(messages, tools, imageTokenEstimate) {
|
|
141
|
+
const current = visibleContextTokens(messages, tools, imageTokenEstimate);
|
|
142
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
62
143
|
const message = messages[index];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
144
|
+
if (message?.role !== "assistant")
|
|
145
|
+
continue;
|
|
146
|
+
if (hasMemoryOnlyPrefix(messages, index))
|
|
147
|
+
continue;
|
|
148
|
+
const completed = completedRequestTokens(message.usage);
|
|
149
|
+
if (completed !== undefined) {
|
|
150
|
+
const throughAssistant = visibleContextTokens(messages.slice(0, index + 1), tools, imageTokenEstimate);
|
|
151
|
+
return Math.max(0, completed + current - throughAssistant);
|
|
152
|
+
}
|
|
153
|
+
// Some OpenAI-compatible endpoints omit total/output usage. Fall back to
|
|
154
|
+
// the exact input baseline and locally estimate the replayed assistant too.
|
|
155
|
+
const baseline = inputTokens(message.usage);
|
|
156
|
+
if (baseline === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
const previous = visibleContextTokens(messages.slice(0, index), tools, imageTokenEstimate);
|
|
159
|
+
return Math.max(0, baseline + current - previous);
|
|
160
|
+
}
|
|
161
|
+
return current;
|
|
162
|
+
}
|
|
163
|
+
export function sessionMemoryUpdatePrompt(memory) {
|
|
164
|
+
const oversized = [...sections(memory, false)].flatMap(([header, body]) => {
|
|
165
|
+
// Captured budgets exclude the heading and the leading italic description.
|
|
166
|
+
const count = memoryTokenEstimate(body.replace(/^_[^\n]+_\s*/, ""));
|
|
167
|
+
return count > budget ? [`- "${header}" is ~${count} tokens (limit: 1500)`] : [];
|
|
168
|
+
});
|
|
169
|
+
const notice = oversized.length ? "\n\n" + memoryBudgetNoticeTemplate.replace("{{SECTIONS}}", () => oversized.join("\n")) : "";
|
|
170
|
+
return memoryUpdateTemplate.replace("\n\n<session_memory>{{MEMORY}}</session_memory>", () => `${notice}\n\n<session_memory>\n${memory}\n</session_memory>`);
|
|
171
|
+
}
|
|
172
|
+
export function continuationSummaryBlock(memory) {
|
|
173
|
+
return `<arena-system-message>\n${memoryContinuationPreamble}\n\n${escapedMemory(memory)}\n\n</arena-system-message>`;
|
|
174
|
+
}
|
|
175
|
+
/** Rebuild model input from the latest applied marker, retaining raw history. */
|
|
176
|
+
export function contextFromHistory(history) {
|
|
177
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
178
|
+
const marker = history[i];
|
|
179
|
+
if (marker.role !== "compaction" || marker.undone)
|
|
180
|
+
continue;
|
|
181
|
+
const summary = marker.content.map(part => part.text).join("\n");
|
|
182
|
+
if (!summary.trim())
|
|
183
|
+
throw new Error("Compaction record has no summary");
|
|
184
|
+
const tail = history.slice(i + 1).filter(message => message.role !== "compaction");
|
|
185
|
+
if (tail[0]?.role === "tool")
|
|
186
|
+
throw new Error("Compaction record splits a tool call/result pair");
|
|
187
|
+
// The dump also contains older full summaries. Preserve their wrapper;
|
|
188
|
+
// current eight-section memory continues to use its captured preamble.
|
|
189
|
+
const block = summary.startsWith("# Current State\n")
|
|
190
|
+
? continuationSummaryBlock(summary) : summaryBlock(summary);
|
|
191
|
+
const first = tail[0];
|
|
192
|
+
const user = { role: "user", content: [{ text: block },
|
|
193
|
+
...(first?.role === "user" ? typeof first.content === "string"
|
|
194
|
+
? [{ text: first.content }] : structuredClone(first.content) : [])] };
|
|
195
|
+
return [...(history[0]?.role === "system" ? [history[0]] : []), user, ...(first?.role === "user" ? tail.slice(1) : tail)];
|
|
196
|
+
}
|
|
197
|
+
return history.filter(message => message.role !== "compaction");
|
|
198
|
+
}
|
|
199
|
+
function textOf(message) {
|
|
200
|
+
return typeof message.content === "string" ? message.content : message.content.flatMap(p => "text" in p ? [p.text] : []).join("");
|
|
201
|
+
}
|
|
202
|
+
function extractMemory(messages) {
|
|
203
|
+
const copy = [...messages];
|
|
204
|
+
const first = copy[1]?.role === "user" ? structuredClone(copy[1]) : copy[1];
|
|
205
|
+
if (first)
|
|
206
|
+
copy[1] = first;
|
|
207
|
+
if (first?.role !== "user")
|
|
208
|
+
return { memory: emptySessionMemory, messages: copy };
|
|
209
|
+
const prefix = `<arena-system-message>\n${memoryContinuationPreamble}\n\n`;
|
|
210
|
+
const parts = typeof first.content === "string" ? [{ text: first.content }] : first.content;
|
|
211
|
+
const part = parts[0];
|
|
212
|
+
if (!part || !("text" in part) || !part.text.startsWith(prefix))
|
|
213
|
+
return { memory: emptySessionMemory, messages: copy };
|
|
214
|
+
const end = part.text.indexOf("\n\n</arena-system-message>", prefix.length);
|
|
215
|
+
if (end < 0)
|
|
216
|
+
throw new Error("Invalid persisted session memory block");
|
|
217
|
+
const memory = part.text.slice(prefix.length, end);
|
|
218
|
+
const trailing = part.text.slice(end + "\n\n</arena-system-message>".length);
|
|
219
|
+
first.content = [...(trailing ? [{ text: trailing }] : []), ...parts.slice(1)];
|
|
220
|
+
return { memory, messages: copy };
|
|
221
|
+
}
|
|
222
|
+
function prepend(message, memory) {
|
|
223
|
+
return { role: "user", content: [{ text: continuationSummaryBlock(memory) },
|
|
224
|
+
...(typeof message.content === "string" ? [{ text: message.content }] : structuredClone(message.content))] };
|
|
225
|
+
}
|
|
226
|
+
export function memoryPrefixHash(messages, count) {
|
|
227
|
+
const hash = crypto.createHash("sha256");
|
|
228
|
+
// Runtime system/date overrides do not invalidate the actual conversation prefix.
|
|
229
|
+
for (let i = 1; i < count; i++)
|
|
230
|
+
hash.update(JSON.stringify(messages[i]) + "\n");
|
|
231
|
+
return hash.digest("hex");
|
|
232
|
+
}
|
|
233
|
+
export function sessionMemoryMatchesHistory(state, messages) {
|
|
234
|
+
if (!state || typeof state !== "object")
|
|
235
|
+
return false;
|
|
236
|
+
const value = state;
|
|
237
|
+
if (value.version !== 1 || typeof value.memory !== "string" || typeof value.prefixHash !== "string" ||
|
|
238
|
+
!Number.isSafeInteger(value.coveredMessages) || value.coveredMessages < 2 ||
|
|
239
|
+
value.coveredMessages > messages.length ||
|
|
240
|
+
messages[value.coveredMessages]?.role === "tool" ||
|
|
241
|
+
memoryPrefixHash(messages, value.coveredMessages) !== value.prefixHash)
|
|
242
|
+
return false;
|
|
243
|
+
try {
|
|
244
|
+
const parsed = sections(value.memory, false);
|
|
245
|
+
return parsed.size === headers.length && parsed.has("# Current State");
|
|
70
246
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
addCharacterCounts(counts, "}");
|
|
74
|
-
return Math.ceil(counts.ascii / 4 + counts.nonAscii);
|
|
75
|
-
}
|
|
76
|
-
function imageCount(value) {
|
|
77
|
-
if (Array.isArray(value)) {
|
|
78
|
-
return value.reduce((total, item) => total + imageCount(item), 0);
|
|
247
|
+
catch {
|
|
248
|
+
return false;
|
|
79
249
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
250
|
+
}
|
|
251
|
+
export function restoreSessionMemory(messages, state) {
|
|
252
|
+
if (!sessionMemoryMatchesHistory(state, messages))
|
|
253
|
+
return false;
|
|
254
|
+
const tail = messages.slice(state.coveredMessages);
|
|
255
|
+
const first = tail[0];
|
|
256
|
+
messages.splice(1, messages.length - 1, ...(first?.role === "user" ? [prepend(first, state.memory), ...tail.slice(1)] :
|
|
257
|
+
[{ role: "user", content: continuationSummaryBlock(state.memory) }, ...tail]));
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
export async function compactMessagesIfNeeded(messages, tools, client, model, settings, signal) {
|
|
261
|
+
if (settings.legacy)
|
|
262
|
+
return compactSummary(messages, tools, client, model, settings, signal);
|
|
263
|
+
const before = estimateCompactionTokens(messages, tools, settings.imageTokenEstimate);
|
|
264
|
+
const unchanged = { compacted: false, estimatedTokensBefore: before, estimatedTokensAfter: before };
|
|
265
|
+
const threshold = settings.compactionThresholdTokens ?? settings.contextWindow * settings.triggerRatio;
|
|
266
|
+
if (before < threshold)
|
|
267
|
+
return unchanged;
|
|
268
|
+
// Preserve the current human turn (including all its tool-call/result pairs).
|
|
269
|
+
let boundary = -1;
|
|
270
|
+
for (let i = messages.length - 1; i >= 1; i--) {
|
|
271
|
+
const m = messages[i];
|
|
272
|
+
if (m.role === "user" && !/^<arena-system-message>\nThe image\(s\) below are the result of the preceding tool call\.[\s\S]*?<\/arena-system-message>\s*$/.test(textOf(m))) {
|
|
273
|
+
boundary = i;
|
|
274
|
+
break;
|
|
94
275
|
}
|
|
95
|
-
: message);
|
|
96
|
-
const structuralOverhead = messages.length * 8 + tools.length * 12;
|
|
97
|
-
return (serializedContextTokens(messages, tools) +
|
|
98
|
-
imageCount(modelVisibleMessages) * Math.max(0, imageTokenEstimate) +
|
|
99
|
-
structuralOverhead);
|
|
100
|
-
}
|
|
101
|
-
function safeSummary(value) {
|
|
102
|
-
return value
|
|
103
|
-
.trim()
|
|
104
|
-
.replace(/<arena-system-message>/gi, "<arena-system-message>")
|
|
105
|
-
.replace(/<\/arena-system-message>/gi, "</arena-system-message>");
|
|
106
|
-
}
|
|
107
|
-
export function continuationSummaryBlock(summary) {
|
|
108
|
-
const summaryBody = safeSummary(summary).replace(/\n---$/, "");
|
|
109
|
-
return ("<arena-system-message>\n" +
|
|
110
|
-
`${continuationPreamble}\n\n---\n${summaryBody}\n---\n` +
|
|
111
|
-
"</arena-system-message>");
|
|
112
|
-
}
|
|
113
|
-
function prependSummary(message, summaryBlock) {
|
|
114
|
-
if (typeof message.content === "string") {
|
|
115
|
-
return {
|
|
116
|
-
role: "user",
|
|
117
|
-
content: [{ text: summaryBlock }, { text: message.content }],
|
|
118
|
-
};
|
|
119
276
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
277
|
+
if (boundary <= 1)
|
|
278
|
+
return unchanged;
|
|
279
|
+
if (settings.onBeforeCompaction)
|
|
280
|
+
await settings.onBeforeCompaction(before, threshold);
|
|
281
|
+
const prior = extractMemory(messages);
|
|
282
|
+
const { memory, usage } = await updateMemory(prior.messages.slice(1, boundary), prior.memory, client, model, settings, signal);
|
|
283
|
+
if (!memory)
|
|
284
|
+
return { ...unchanged, usage };
|
|
285
|
+
const current = messages[boundary];
|
|
286
|
+
messages.splice(1, messages.length - 1, prepend(current, memory), ...messages.slice(boundary + 1));
|
|
287
|
+
return { compacted: true, estimatedTokensBefore: before,
|
|
288
|
+
estimatedTokensAfter: visibleContextTokens(messages, tools, settings.imageTokenEstimate), usage, memory };
|
|
127
289
|
}
|
|
128
|
-
async function
|
|
290
|
+
async function updateMemory(source, previous, client, model, settings, signal) {
|
|
291
|
+
signal?.throwIfAborted();
|
|
129
292
|
const request = {
|
|
130
|
-
model,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
],
|
|
136
|
-
tools: [],
|
|
137
|
-
toolChoice: "none",
|
|
138
|
-
temperature: 0,
|
|
139
|
-
maxTokens,
|
|
140
|
-
enableThinking: false,
|
|
141
|
-
thinkingBudget: 0,
|
|
142
|
-
preserveThinking: false,
|
|
143
|
-
signal,
|
|
293
|
+
model, messages: [{ role: "system", content: memorySystemPrompt }, ...source,
|
|
294
|
+
{ role: "user", content: sessionMemoryUpdatePrompt(previous) }],
|
|
295
|
+
tools: [], toolChoice: "none", temperature: 1, maxTokens: settings.summaryMaxTokens,
|
|
296
|
+
...(settings.reasoningEffort ? { reasoningEffort: settings.reasoningEffort } : { thinkingBudget: 81920 }),
|
|
297
|
+
enableThinking: true, preserveThinking: true, useProviderDefaults: true, signal,
|
|
144
298
|
};
|
|
145
|
-
|
|
299
|
+
let patch = "";
|
|
146
300
|
let usage;
|
|
301
|
+
let finishReason;
|
|
147
302
|
for await (const chunk of client.stream(request)) {
|
|
148
303
|
if (chunk.usage)
|
|
149
304
|
usage = chunk.usage;
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
305
|
+
const choice = chunk.choices?.[0];
|
|
306
|
+
patch += choice?.delta?.content || "";
|
|
307
|
+
if (choice?.finish_reason)
|
|
308
|
+
finishReason = choice.finish_reason;
|
|
153
309
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
estimatedTokensBefore: before,
|
|
165
|
-
estimatedTokensAfter: before,
|
|
166
|
-
};
|
|
310
|
+
if (signal?.aborted)
|
|
311
|
+
throw signal.reason || new Error("Memory update cancelled");
|
|
312
|
+
if (!patch.trim())
|
|
313
|
+
return { usage, update: { status: "error", code: "MEMORY_EMPTY_RESPONSE", usage } };
|
|
314
|
+
if (finishReason && finishReason !== "stop")
|
|
315
|
+
return { usage,
|
|
316
|
+
update: { status: "error", code: "MEMORY_INCOMPLETE_RESPONSE", error: `Memory response finished with ${finishReason}`, usage } };
|
|
317
|
+
let memory;
|
|
318
|
+
try {
|
|
319
|
+
memory = mergeSessionMemory(previous, patch);
|
|
167
320
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
compacted: false,
|
|
172
|
-
estimatedTokensBefore: before,
|
|
173
|
-
estimatedTokensAfter: before,
|
|
174
|
-
};
|
|
321
|
+
catch (error) {
|
|
322
|
+
return { usage, update: { status: "error", code: "MEMORY_INVALID_PATCH", error: String(error), usage } };
|
|
175
323
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
324
|
+
return { memory, usage, update: { status: "success", usage } };
|
|
325
|
+
}
|
|
326
|
+
/** Provisional scheduling policy, not an observed official constant. */
|
|
327
|
+
export async function maintainSessionMemory(messages, history, cache, tools, client, model, settings, updateTokens = 24_000, signal, onUpdate) {
|
|
328
|
+
const before = estimateCompactionTokens(messages, tools, settings.imageTokenEstimate);
|
|
329
|
+
const unchanged = { compacted: false, estimatedTokensBefore: before, estimatedTokensAfter: before };
|
|
330
|
+
const threshold = settings.compactionThresholdTokens ?? settings.contextWindow * settings.triggerRatio;
|
|
331
|
+
const needsCompaction = before >= threshold;
|
|
332
|
+
if (cache.latest && !sessionMemoryMatchesHistory(cache.latest, history))
|
|
333
|
+
cache.latest = undefined;
|
|
334
|
+
if (cache.state && !sessionMemoryMatchesHistory(cache.state, history))
|
|
335
|
+
cache.state = undefined;
|
|
336
|
+
// Keep the newest complete assistant/tool step (or newest user message) verbatim.
|
|
337
|
+
// This also permits updates during a long single-user tool loop.
|
|
338
|
+
let boundary = history.length - 1;
|
|
339
|
+
while (boundary > 1 && history[boundary]?.role !== "assistant" && history[boundary]?.role !== "user")
|
|
340
|
+
boundary--;
|
|
341
|
+
// Keep an immediately preceding user instruction with the retained step.
|
|
342
|
+
while (boundary > 1 && history[boundary - 1]?.role === "user")
|
|
343
|
+
boundary--;
|
|
344
|
+
if (settings.onBeforeCompaction && needsCompaction && (cache.pending ||
|
|
345
|
+
boundary > (cache.latest?.coveredMessages ?? cache.state?.coveredMessages ?? 1) ||
|
|
346
|
+
(cache.latest && cache.latest.coveredMessages > (cache.state?.coveredMessages ?? 1)))) {
|
|
347
|
+
// Background refreshes are not compactions. Intercept the hard boundary
|
|
348
|
+
// before starting a forced update or applying an already prepared memory.
|
|
349
|
+
await settings.onBeforeCompaction(before, threshold);
|
|
184
350
|
}
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
351
|
+
const previous = cache.latest || cache.state;
|
|
352
|
+
const embedded = previous ? undefined : extractMemory(history);
|
|
353
|
+
const start = previous?.coveredMessages ?? 1;
|
|
354
|
+
if (!cache.pending && boundary > start) {
|
|
355
|
+
const source = structuredClone((embedded?.messages ?? history).slice(start, boundary));
|
|
356
|
+
// A synthetic memory-only user message has no new content to summarize.
|
|
357
|
+
if (source[0]?.role === "user" && Array.isArray(source[0].content) && !source[0].content.length)
|
|
358
|
+
source.shift();
|
|
359
|
+
const prefixHash = memoryPrefixHash(history, boundary);
|
|
360
|
+
// Incremental estimate excludes system/tools and the existing memory. Images use
|
|
361
|
+
// the local estimator; the exact official scheduling tokenizer is unknown.
|
|
362
|
+
const eligible = estimateContextTokens(source, [], settings.imageTokenEstimate) >= updateTokens;
|
|
363
|
+
if ((eligible || needsCompaction) && cache.attemptedPrefix !== prefixHash) {
|
|
364
|
+
cache.attemptedPrefix = prefixHash;
|
|
365
|
+
const controller = new AbortController();
|
|
366
|
+
cache.controller = controller;
|
|
367
|
+
const updateSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
368
|
+
cache.update = { status: "pending" };
|
|
369
|
+
// Publish the promise before any callback yields so concurrent schedulers
|
|
370
|
+
// observe the same in-flight update.
|
|
371
|
+
cache.pending = Promise.resolve().then(async () => {
|
|
372
|
+
if (onUpdate)
|
|
373
|
+
await onUpdate({ status: "pending" });
|
|
374
|
+
try {
|
|
375
|
+
const result = await updateMemory(source, previous?.memory ?? embedded?.memory ?? emptySessionMemory, client, model, settings, updateSignal);
|
|
376
|
+
updateSignal.throwIfAborted();
|
|
377
|
+
cache.update = result.update;
|
|
378
|
+
if (result.memory && memoryPrefixHash(history, boundary) === prefixHash) {
|
|
379
|
+
cache.latest = { version: 1, memory: result.memory, coveredMessages: boundary, prefixHash };
|
|
380
|
+
cache.latestUsage = result.usage;
|
|
381
|
+
}
|
|
382
|
+
else if (result.memory) {
|
|
383
|
+
cache.update = { status: "error", code: "MEMORY_STALE_HISTORY", usage: result.usage };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
cache.update = { status: updateSignal.aborted ? "cancelled" : "error",
|
|
388
|
+
code: updateSignal.aborted ? "MEMORY_CANCELLED" : "MEMORY_REQUEST_FAILED",
|
|
389
|
+
error: error instanceof Error ? error.message : String(error) };
|
|
390
|
+
}
|
|
391
|
+
await onUpdate?.(cache.update);
|
|
392
|
+
}).finally(() => { cache.pending = undefined; cache.controller = undefined; });
|
|
393
|
+
}
|
|
193
394
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
395
|
+
if (!needsCompaction)
|
|
396
|
+
return unchanged;
|
|
397
|
+
// At the hard context boundary, wait for an in-flight snapshot if necessary.
|
|
398
|
+
if (cache.pending)
|
|
399
|
+
await cache.pending;
|
|
400
|
+
const latest = cache.latest;
|
|
401
|
+
if (!latest || latest.coveredMessages <= (cache.state?.coveredMessages ?? 1))
|
|
402
|
+
return unchanged;
|
|
403
|
+
const replacement = [...history];
|
|
404
|
+
if (!restoreSessionMemory(replacement, latest))
|
|
405
|
+
return unchanged;
|
|
406
|
+
messages.splice(0, messages.length, ...replacement);
|
|
407
|
+
cache.state = latest;
|
|
408
|
+
return { compacted: true, estimatedTokensBefore: before,
|
|
409
|
+
estimatedTokensAfter: visibleContextTokens(messages, tools, settings.imageTokenEstimate),
|
|
410
|
+
memory: latest.memory, usage: cache.latestUsage };
|
|
206
411
|
}
|
|
207
412
|
//# sourceMappingURL=compaction.js.map
|