@qwen-code/arena-agent 0.0.10-dev.1 → 0.0.10-dev.10
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 +57 -14
- package/CHANGELOG.md +228 -0
- package/README.md +228 -50
- package/dist/agent.d.ts +28 -2
- package/dist/agent.js +341 -106
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.js +23 -22
- 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.js +2 -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 +49 -3
- package/dist/compaction.js +382 -186
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +17 -2
- package/dist/config.js +47 -5
- package/dist/config.js.map +1 -1
- 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/legacy-compaction.d.ts +20 -0
- package/dist/legacy-compaction.js +209 -0
- package/dist/legacy-compaction.js.map +1 -0
- package/dist/midturn-checkpoint-hook.d.ts +31 -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 +71 -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 -0
- package/dist/prompts.js +6 -2
- 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 +60 -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.js +18 -10
- 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/tools.d.ts +5 -0
- package/dist/tools.js +378 -132
- 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 +28 -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 +28 -5
- 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 +14 -4
- package/scripts/README.md +62 -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/agent.js
CHANGED
|
@@ -1,18 +1,70 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
3
|
import { supportsModelInput, } from "./config.js";
|
|
4
|
-
import { compactMessagesIfNeeded } from "./compaction.js";
|
|
4
|
+
import { compactMessagesIfNeeded, maintainSessionMemory, contextFromHistory, sessionMemoryMatchesHistory, settleSessionMemory, storedSessionMemory } from "./compaction.js";
|
|
5
5
|
import { ANTHROPIC_INCOMPLETE_STREAM_ERROR_CODE } from "./anthropic.js";
|
|
6
|
-
import { modelCallLogPath } from "./history.js";
|
|
6
|
+
import { modelCallLogPath, withoutLeadingSystemMessage } from "./history.js";
|
|
7
7
|
import { MockModelClient } from "./mock.js";
|
|
8
8
|
import { contentPartMediaTypes, JsonlModelCallDiagnostics, } from "./model-diagnostics.js";
|
|
9
9
|
import { RESPONSES_CAPACITY_ERROR_CODE, RESPONSES_TRANSIENT_ERROR_CODE, } from "./openai-responses.js";
|
|
10
10
|
import { HttpModelClient, ModelCapabilityError } from "./provider.js";
|
|
11
|
+
import { RepetitiveOutputDetector } from "./repetitive-output.js";
|
|
11
12
|
import { detectRepoContext } from "./repo.js";
|
|
12
13
|
import { isCodeArenaMode } from "./mode.js";
|
|
13
|
-
import { defaultClientContext,
|
|
14
|
-
import { ToolExecutor } from "./tools.js";
|
|
14
|
+
import { defaultClientContext, interruptedUserMessage, repoSystemMessage, systemMessage, } from "./spec.js";
|
|
15
|
+
import { imageInputFallbackConnection, ToolExecutor, } from "./tools.js";
|
|
15
16
|
import { initializeWorkspace } from "./workspace.js";
|
|
17
|
+
import { checkpointAction, } from "./midturn-checkpoint-hook.js";
|
|
18
|
+
function wrapMalformedToolArguments(argumentsText) {
|
|
19
|
+
try {
|
|
20
|
+
JSON.parse(argumentsText);
|
|
21
|
+
return argumentsText;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return JSON.stringify({ __malformedToolArguments: argumentsText });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Keep Agent Arena history replayable without losing the model's invalid input. */
|
|
28
|
+
function normalizeToolCallHistory(message, mode) {
|
|
29
|
+
if (isCodeArenaMode(mode) || message.role !== "assistant")
|
|
30
|
+
return message;
|
|
31
|
+
// A failed Responses stream can retain completed native items without the
|
|
32
|
+
// assembled tool_calls batch. Restore their identities so resume can pair
|
|
33
|
+
// them with interrupted results instead of replaying orphaned calls.
|
|
34
|
+
const calls = message.tool_calls?.length ? message.tool_calls
|
|
35
|
+
: message.openai_response_items?.flatMap((item) => item.type === "function_call" && typeof item.call_id === "string" &&
|
|
36
|
+
typeof item.name === "string" && typeof item.arguments === "string"
|
|
37
|
+
? [{ id: item.call_id, type: "function", function: { name: item.name, arguments: item.arguments } }]
|
|
38
|
+
: []) ?? message.tool_calls;
|
|
39
|
+
let changed = Boolean(calls?.length && calls !== message.tool_calls);
|
|
40
|
+
const toolCalls = calls?.map((call) => {
|
|
41
|
+
if (typeof call.function?.arguments !== "string")
|
|
42
|
+
return call;
|
|
43
|
+
const argumentsText = wrapMalformedToolArguments(call.function.arguments);
|
|
44
|
+
if (argumentsText === call.function.arguments)
|
|
45
|
+
return call;
|
|
46
|
+
changed = true;
|
|
47
|
+
return { ...call, function: { ...call.function, arguments: argumentsText } };
|
|
48
|
+
});
|
|
49
|
+
// Native Responses replays these items instead of tool_calls. Normalize both
|
|
50
|
+
// representations while leaving reasoning and other opaque items intact.
|
|
51
|
+
const responseItems = message.openai_response_items?.map((item) => {
|
|
52
|
+
if (item.type !== "function_call" || typeof item.arguments !== "string")
|
|
53
|
+
return item;
|
|
54
|
+
const argumentsText = wrapMalformedToolArguments(item.arguments);
|
|
55
|
+
if (argumentsText === item.arguments)
|
|
56
|
+
return item;
|
|
57
|
+
changed = true;
|
|
58
|
+
return { ...item, arguments: argumentsText };
|
|
59
|
+
});
|
|
60
|
+
return changed
|
|
61
|
+
? {
|
|
62
|
+
...message,
|
|
63
|
+
...(toolCalls ? { tool_calls: toolCalls } : {}),
|
|
64
|
+
...(responseItems ? { openai_response_items: responseItems } : {}),
|
|
65
|
+
}
|
|
66
|
+
: message;
|
|
67
|
+
}
|
|
16
68
|
function parseToolResult(content) {
|
|
17
69
|
try {
|
|
18
70
|
return JSON.parse(content);
|
|
@@ -143,7 +195,7 @@ function preflightUnsupportedMedia(config, messages) {
|
|
|
143
195
|
return replaceUnsupportedMedia(messages, mediaTypes, imageReadFileFallback);
|
|
144
196
|
}
|
|
145
197
|
function modelContextFromTrajectory(messages) {
|
|
146
|
-
return messages.map((message) => message.role === "user" && Array.isArray(message.content)
|
|
198
|
+
return contextFromHistory(messages).map((message) => message.role === "user" && Array.isArray(message.content)
|
|
147
199
|
? { ...message, content: structuredClone(message.content) }
|
|
148
200
|
: message);
|
|
149
201
|
}
|
|
@@ -181,6 +233,13 @@ function completeInterruptedToolCalls(messages) {
|
|
|
181
233
|
flushPending();
|
|
182
234
|
return completed;
|
|
183
235
|
}
|
|
236
|
+
export function isContinuableHistory(messages) {
|
|
237
|
+
const last = messages.at(-1);
|
|
238
|
+
return Boolean(last &&
|
|
239
|
+
(last.role === "user" ||
|
|
240
|
+
last.role === "tool" ||
|
|
241
|
+
(last.role === "assistant" && Boolean(last.tool_calls?.length))));
|
|
242
|
+
}
|
|
184
243
|
const identityModelsWithQwen = "Claude, ChatGPT, Gemini, Grok, Qwen, and Kimi";
|
|
185
244
|
const identityModelsWithoutQwen = "Claude, ChatGPT, Gemini, Grok, and Kimi";
|
|
186
245
|
const arenaWorkspaceIdentityOpening = "You are a helpful agentic assistant with tool access running on Arena.ai's Agent Mode.";
|
|
@@ -214,19 +273,12 @@ function withAppendedSystemPrompt(content, appendix) {
|
|
|
214
273
|
: "\n\n";
|
|
215
274
|
return `${content}${separator}${appendix}`;
|
|
216
275
|
}
|
|
217
|
-
function applySystemPromptConfig(message, config
|
|
276
|
+
function applySystemPromptConfig(message, config) {
|
|
218
277
|
if (message?.role !== "system" || typeof message.content !== "string") {
|
|
219
278
|
return;
|
|
220
279
|
}
|
|
221
280
|
const appendix = config.appendSystemPrompt;
|
|
222
|
-
|
|
223
|
-
appendix &&
|
|
224
|
-
message.content.endsWith(`\n\n${appendix}`)
|
|
225
|
-
? appendix
|
|
226
|
-
: undefined;
|
|
227
|
-
let content = existingAppendix
|
|
228
|
-
? message.content.slice(0, -existingAppendix.length)
|
|
229
|
-
: message.content;
|
|
281
|
+
let content = message.content;
|
|
230
282
|
if (typeof config.systemPrompt === "string") {
|
|
231
283
|
content = config.systemPrompt;
|
|
232
284
|
}
|
|
@@ -239,11 +291,7 @@ function applySystemPromptConfig(message, config, preserveExistingAppendix = fal
|
|
|
239
291
|
? content.replace(identityModelsWithQwen, identityModelsWithoutQwen)
|
|
240
292
|
: content.replace(identityModelsWithoutQwen, identityModelsWithQwen);
|
|
241
293
|
}
|
|
242
|
-
|
|
243
|
-
if (reuseExistingAppendix) {
|
|
244
|
-
content += existingAppendix;
|
|
245
|
-
}
|
|
246
|
-
if (appendix !== undefined && !reuseExistingAppendix) {
|
|
294
|
+
if (appendix !== undefined) {
|
|
247
295
|
content = withAppendedSystemPrompt(content, appendix);
|
|
248
296
|
}
|
|
249
297
|
message.content = content;
|
|
@@ -258,20 +306,33 @@ class PartialAssistantTurnError extends Error {
|
|
|
258
306
|
this.name = "PartialAssistantTurnError";
|
|
259
307
|
}
|
|
260
308
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
309
|
+
class CheckpointExit extends Error {
|
|
310
|
+
event;
|
|
311
|
+
constructor(event) {
|
|
312
|
+
super("model generation stopped by checkpoint hook");
|
|
313
|
+
this.event = event;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
class CheckpointFailure extends Error {
|
|
317
|
+
cause;
|
|
318
|
+
constructor(cause) {
|
|
319
|
+
super(describeError(cause));
|
|
320
|
+
this.cause = cause;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Rebuild the runtime repo context from the current workspace on every run.
|
|
324
|
+
// The history argument is retained for callers; it never supplies runtime state.
|
|
325
|
+
export function resolveRepoContext(workspace, _history, mode = "arena") {
|
|
326
|
+
return isCodeArenaMode(mode) ? undefined : detectRepoContext(workspace);
|
|
270
327
|
}
|
|
271
328
|
export class Agent {
|
|
272
329
|
config;
|
|
273
330
|
sink;
|
|
274
331
|
options;
|
|
332
|
+
checkpointQueue = Promise.resolve();
|
|
333
|
+
checkpointError;
|
|
334
|
+
persistedMemory;
|
|
335
|
+
memoryCache;
|
|
275
336
|
client;
|
|
276
337
|
tools;
|
|
277
338
|
workspaceContext;
|
|
@@ -281,40 +342,58 @@ export class Agent {
|
|
|
281
342
|
this.config = config;
|
|
282
343
|
this.sink = sink;
|
|
283
344
|
this.options = options;
|
|
345
|
+
this.memoryCache = options.sessionMemory || {};
|
|
284
346
|
this.sessionId =
|
|
285
347
|
options.sessionId || crypto.randomUUID().replaceAll("-", "");
|
|
286
348
|
this.sessionAffinityId = options.sessionAffinityId || crypto.randomUUID();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
349
|
+
// Local workspace guidance is explicit opt-in. Code Arena defaults must
|
|
350
|
+
// send the captured prompt; Agent Arena already used this same opt-in path.
|
|
351
|
+
this.workspaceContext = options.workspaceContext;
|
|
352
|
+
const fallbackConnection = config.imageInputFallbackModel
|
|
353
|
+
? imageInputFallbackConnection(config)
|
|
354
|
+
: undefined;
|
|
355
|
+
const needsMainHttpClient = !options.client && !config.model.startsWith("mock/");
|
|
356
|
+
const needsFallbackHttpClient = Boolean(fallbackConnection) && !options.imageInputFallbackClient;
|
|
357
|
+
const diagnostics = config.modelCallLogging === false ||
|
|
358
|
+
(!needsMainHttpClient && !needsFallbackHttpClient)
|
|
359
|
+
? undefined
|
|
360
|
+
: new JsonlModelCallDiagnostics(modelCallLogPath(config.stateRoot, config.tenantId, this.sessionId));
|
|
296
361
|
this.client =
|
|
297
362
|
options.client ||
|
|
298
363
|
(config.model.startsWith("mock/")
|
|
299
364
|
? new MockModelClient()
|
|
300
365
|
: new HttpModelClient(config, {
|
|
301
366
|
sessionAffinityId: this.sessionAffinityId,
|
|
302
|
-
...(
|
|
303
|
-
? {}
|
|
304
|
-
: {
|
|
305
|
-
diagnostics: new JsonlModelCallDiagnostics(modelCallLogPath(config.stateRoot, config.tenantId, this.sessionId)),
|
|
306
|
-
}),
|
|
367
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
307
368
|
}));
|
|
369
|
+
const imageInputFallbackClient = options.imageInputFallbackClient ||
|
|
370
|
+
(fallbackConnection
|
|
371
|
+
? new HttpModelClient(config, {
|
|
372
|
+
connection: fallbackConnection,
|
|
373
|
+
sessionAffinityId: this.sessionAffinityId,
|
|
374
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
375
|
+
})
|
|
376
|
+
: undefined);
|
|
308
377
|
this.tools =
|
|
309
378
|
options.tools ||
|
|
310
379
|
new ToolExecutor(config, {
|
|
311
380
|
modelClient: this.client,
|
|
381
|
+
imageInputFallbackClient,
|
|
312
382
|
signal: options.signal,
|
|
313
383
|
askUserHandler: options.askUserHandler,
|
|
314
384
|
workspaceContext: this.workspaceContext,
|
|
315
385
|
});
|
|
316
386
|
}
|
|
317
387
|
async run(prompt, history, runOptions = {}) {
|
|
388
|
+
try {
|
|
389
|
+
return await this.runTurn(prompt, history, runOptions);
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
await settleSessionMemory(this.memoryCache);
|
|
393
|
+
await this.checkpointQueue;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async runTurn(prompt, history, runOptions) {
|
|
318
397
|
if (runOptions.resumeFromHistory && !history?.length) {
|
|
319
398
|
throw new Error("resumeFromHistory requires a non-empty history");
|
|
320
399
|
}
|
|
@@ -328,67 +407,66 @@ export class Agent {
|
|
|
328
407
|
: structuredClone(baseUserMessage);
|
|
329
408
|
const clientContext = this.options.clientContext || defaultClientContext();
|
|
330
409
|
const repoContext = resolveRepoContext(this.config.workspace, history, this.config.mode);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
410
|
+
// System instructions belong to this invocation, never to the saved dialogue.
|
|
411
|
+
// Older local histories included a system message; accept them without
|
|
412
|
+
// carrying the old runtime configuration into the next request.
|
|
413
|
+
const dialogue = withoutLeadingSystemMessage(history || []).map((message) => normalizeToolCallHistory(message, this.config.mode));
|
|
414
|
+
const trajectory = completeInterruptedToolCalls(this.options.reuseHistoryMessages ? [...dialogue] : structuredClone(dialogue));
|
|
415
|
+
const runtimeSystemMessage = repoContext
|
|
416
|
+
? repoSystemMessage(clientContext, repoContext, this.config.mode, this.workspaceContext, this.config.maxTurns)
|
|
417
|
+
: systemMessage(clientContext, this.workspaceContext, this.config.mode, this.config.maxTurns);
|
|
418
|
+
applySystemPromptConfig(runtimeSystemMessage, this.config);
|
|
419
|
+
if (runOptions.resumeFromHistory) {
|
|
420
|
+
if (!isContinuableHistory(dialogue)) {
|
|
421
|
+
throw new Error("resumeFromHistory requires an unfinished user or tool boundary");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
trajectory.push(userMessage);
|
|
340
426
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
427
|
+
// Compaction projects model input with a runtime system prefix; the lossless
|
|
428
|
+
// trajectory and its checkpoints remain in the official dialogue format.
|
|
429
|
+
const messages = modelContextFromTrajectory([runtimeSystemMessage, ...trajectory]);
|
|
430
|
+
if (!isCodeArenaMode(this.config.mode)) {
|
|
431
|
+
const saved = this.persistedMemory ?? this.options.storedSessionMemory ?? storedSessionMemory(this.memoryCache);
|
|
432
|
+
preflightUnsupportedMedia(this.config, messages);
|
|
433
|
+
// Opting out of restoration must not delete a still-valid durable snapshot.
|
|
434
|
+
this.persistedMemory = saved?.version === 1 ? structuredClone(saved) : undefined;
|
|
435
|
+
if (this.persistedMemory?.latest && !sessionMemoryMatchesHistory(this.persistedMemory.latest, messages)) {
|
|
436
|
+
this.persistedMemory.latest = undefined;
|
|
349
437
|
}
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (!repoContext &&
|
|
354
|
-
this.workspaceContext &&
|
|
355
|
-
isCanonicalSystemMessage(trajectory[0], portableExpected, this.config.mode, this.config.maxTurns)) {
|
|
356
|
-
const runtimeSystemMessage = initialMessages("", this.options.clientContext, this.workspaceContext, undefined, this.config.mode, this.config.maxTurns)[0];
|
|
357
|
-
applySystemPromptConfig(runtimeSystemMessage, this.config);
|
|
358
|
-
trajectory[0] = runtimeSystemMessage;
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
throw new Error("history does not match the current workspace runtime context");
|
|
362
|
-
}
|
|
438
|
+
if (this.persistedMemory?.update?.status === "pending") {
|
|
439
|
+
this.persistedMemory.update = { status: "cancelled", code: "MEMORY_INTERRUPTED",
|
|
440
|
+
error: "Previous process ended during a memory update" };
|
|
363
441
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
442
|
+
this.memoryCache.latest = undefined;
|
|
443
|
+
this.memoryCache.state = undefined;
|
|
444
|
+
this.memoryCache.latestUsage = undefined;
|
|
445
|
+
this.memoryCache.attemptedPrefix = undefined;
|
|
446
|
+
this.memoryCache.update = undefined;
|
|
447
|
+
if (this.config.compactionMode !== "summary" &&
|
|
448
|
+
(runOptions.restoreSessionMemory ?? runOptions.singleStep) && saved?.version === 1) {
|
|
449
|
+
if (sessionMemoryMatchesHistory(saved.latest, messages)) {
|
|
450
|
+
this.memoryCache.latest = structuredClone(saved.latest);
|
|
368
451
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
452
|
+
this.memoryCache.update = this.persistedMemory?.update;
|
|
453
|
+
if (saved.latest && !this.memoryCache.latest) {
|
|
454
|
+
this.memoryCache.update = { status: "error", code: "MEMORY_STALE_HISTORY",
|
|
455
|
+
error: "Saved memory does not match the current history prefix" };
|
|
456
|
+
}
|
|
457
|
+
if (this.memoryCache.update)
|
|
458
|
+
this.sink.emit({ type: "memory.update", session_id: this.sessionId, ...this.memoryCache.update });
|
|
372
459
|
}
|
|
373
460
|
}
|
|
374
|
-
else {
|
|
375
|
-
trajectory[1] = userMessage;
|
|
376
|
-
}
|
|
377
|
-
// Persisted history is the lossless source of truth for replay and
|
|
378
|
-
// training. Compaction may rewrite only this private model-context copy;
|
|
379
|
-
// every real user, assistant, and tool message is appended to both.
|
|
380
|
-
// Compaction replaces array entries, and media fallback can rewrite only
|
|
381
|
-
// multimodal user content. Share the immutable lossless prefix and isolate
|
|
382
|
-
// just that mutable content instead of cloning every historical string.
|
|
383
|
-
const messages = modelContextFromTrajectory(trajectory);
|
|
384
461
|
const recordMessage = async (message) => {
|
|
385
|
-
|
|
386
|
-
|
|
462
|
+
const normalized = normalizeToolCallHistory(message, this.config.mode);
|
|
463
|
+
messages.push(normalized);
|
|
464
|
+
const recorded = structuredClone(normalized);
|
|
387
465
|
trajectory.push(recorded);
|
|
388
466
|
this.sink.message(this.sessionId, recorded, trajectory);
|
|
389
467
|
await this.checkpoint(trajectory);
|
|
390
468
|
};
|
|
391
|
-
await this.maybeCompact(messages);
|
|
469
|
+
await this.maybeCompact(messages, trajectory);
|
|
392
470
|
this.sink.start(this.sessionId, trajectory);
|
|
393
471
|
try {
|
|
394
472
|
await this.checkpoint(trajectory);
|
|
@@ -406,14 +484,21 @@ export class Agent {
|
|
|
406
484
|
// stores its encrypted reasoning output items there, so compaction is
|
|
407
485
|
// safe at the same tool-step boundaries as the other protocols.
|
|
408
486
|
if (step > 1)
|
|
409
|
-
await this.maybeCompact(messages);
|
|
487
|
+
await this.maybeCompact(messages, trajectory, "inner");
|
|
410
488
|
const offeredTools = this.tools.availableTools();
|
|
411
489
|
this.sink.modelStart(this.sessionId, step, offeredTools.map((tool) => tool.function.name));
|
|
412
490
|
let assistant;
|
|
413
491
|
try {
|
|
414
|
-
assistant = await this.assistantTurn(messages, offeredTools);
|
|
492
|
+
assistant = await this.assistantTurn(messages, offeredTools, trajectory);
|
|
415
493
|
}
|
|
416
494
|
catch (error) {
|
|
495
|
+
if (error instanceof CheckpointExit) {
|
|
496
|
+
this.sink.modelComplete(this.sessionId, step, "checkpoint");
|
|
497
|
+
await this.makeCheckpoint(trajectory, offeredTools, error.event, "snapshot_exit");
|
|
498
|
+
return await this.pause(trajectory, "checkpoint");
|
|
499
|
+
}
|
|
500
|
+
if (error instanceof CheckpointFailure)
|
|
501
|
+
throw error.cause;
|
|
417
502
|
if (error instanceof PartialAssistantTurnError) {
|
|
418
503
|
this.options.onModelFailure?.(trajectory, error.cause);
|
|
419
504
|
if (error.assistant) {
|
|
@@ -426,9 +511,24 @@ export class Agent {
|
|
|
426
511
|
}
|
|
427
512
|
const finishReason = assistant.tool_calls?.length ? "tool_calls" : "stop";
|
|
428
513
|
this.sink.modelComplete(this.sessionId, step, finishReason, assistant.usage);
|
|
514
|
+
if (assistant.tool_calls?.length &&
|
|
515
|
+
this.options.midturnCheckpointHook) {
|
|
516
|
+
const event = {
|
|
517
|
+
type: "tool_validation",
|
|
518
|
+
sessionId: this.sessionId,
|
|
519
|
+
toolCalls: assistant.tool_calls.map((call) => this.tools.inspect(call)),
|
|
520
|
+
};
|
|
521
|
+
const action = checkpointAction(await this.options.midturnCheckpointHook(event));
|
|
522
|
+
if (action !== "continue") {
|
|
523
|
+
await this.makeCheckpoint(trajectory, offeredTools, event, action);
|
|
524
|
+
if (action === "snapshot_exit") {
|
|
525
|
+
return await this.pause(trajectory, "checkpoint");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
429
529
|
await recordMessage(assistant);
|
|
430
530
|
if (!assistant.tool_calls?.length) {
|
|
431
|
-
return this.
|
|
531
|
+
return await this.finish(trajectory, "success");
|
|
432
532
|
}
|
|
433
533
|
const completedToolCallIds = [];
|
|
434
534
|
for (const call of assistant.tool_calls) {
|
|
@@ -460,7 +560,7 @@ export class Agent {
|
|
|
460
560
|
? toolResult.questions
|
|
461
561
|
: [];
|
|
462
562
|
this.sink.inputRequired(this.sessionId, toolCallId, questions);
|
|
463
|
-
return this.
|
|
563
|
+
return await this.finish(trajectory, "needs_input");
|
|
464
564
|
}
|
|
465
565
|
}
|
|
466
566
|
if (this.options.signal?.aborted) {
|
|
@@ -471,28 +571,111 @@ export class Agent {
|
|
|
471
571
|
if (visualMessage) {
|
|
472
572
|
await recordMessage(visualMessage);
|
|
473
573
|
}
|
|
574
|
+
if (runOptions.singleStep) {
|
|
575
|
+
return await this.pause(trajectory, "single_step");
|
|
576
|
+
}
|
|
474
577
|
}
|
|
475
578
|
}
|
|
476
579
|
catch (error) {
|
|
477
580
|
const message = error instanceof Error ? error.message : String(error);
|
|
478
|
-
return this.
|
|
581
|
+
return await this.finish(trajectory, "error", message);
|
|
479
582
|
}
|
|
480
583
|
}
|
|
481
584
|
async checkpoint(messages) {
|
|
482
|
-
|
|
585
|
+
const snapshot = [...messages];
|
|
586
|
+
const tools = this.tools.availableTools();
|
|
587
|
+
const currentMemory = storedSessionMemory(this.memoryCache);
|
|
588
|
+
const memory = isCodeArenaMode(this.config.mode) ? undefined : (currentMemory || this.persistedMemory
|
|
589
|
+
? { version: 1, latest: currentMemory?.latest ?? this.persistedMemory?.latest,
|
|
590
|
+
update: currentMemory?.update ?? this.persistedMemory?.update }
|
|
591
|
+
: null);
|
|
592
|
+
if (memory !== undefined)
|
|
593
|
+
this.persistedMemory = memory ?? undefined;
|
|
594
|
+
const write = this.checkpointQueue.then(() => this.options.onCheckpoint?.(snapshot, tools, memory));
|
|
595
|
+
this.checkpointQueue = write.catch(error => { this.checkpointError ??= error; });
|
|
596
|
+
await write;
|
|
597
|
+
if (this.checkpointError)
|
|
598
|
+
throw this.checkpointError;
|
|
599
|
+
}
|
|
600
|
+
async finish(messages, status, error) {
|
|
601
|
+
await settleSessionMemory(this.memoryCache);
|
|
602
|
+
try {
|
|
603
|
+
await this.checkpoint(messages);
|
|
604
|
+
}
|
|
605
|
+
catch (failure) {
|
|
606
|
+
status = "error";
|
|
607
|
+
error ??= failure instanceof Error ? failure.message : String(failure);
|
|
608
|
+
}
|
|
609
|
+
return this.sink.result(this.sessionId, status, messages, error, undefined, this.memoryCache.update);
|
|
610
|
+
}
|
|
611
|
+
async makeCheckpoint(messages, tools, event, action) {
|
|
612
|
+
await settleSessionMemory(this.memoryCache);
|
|
613
|
+
await this.checkpoint(messages);
|
|
614
|
+
if (!this.options.onMidturnCheckpoint) {
|
|
615
|
+
throw new Error("mid-turn checkpoint hook triggered without a snapshot handler");
|
|
616
|
+
}
|
|
617
|
+
await this.options.onMidturnCheckpoint(messages, tools, event, action);
|
|
483
618
|
}
|
|
484
|
-
async
|
|
619
|
+
async pause(messages, reason) {
|
|
620
|
+
this.options.signal?.throwIfAborted();
|
|
621
|
+
await settleSessionMemory(this.memoryCache, reason !== "single_step");
|
|
622
|
+
if (reason === "single_step")
|
|
623
|
+
await this.checkpoint(messages);
|
|
624
|
+
await this.options.onPause?.(messages, this.tools.availableTools());
|
|
625
|
+
this.options.signal?.throwIfAborted();
|
|
626
|
+
return this.sink.result(this.sessionId, "paused", messages, undefined, reason, this.memoryCache.update);
|
|
627
|
+
}
|
|
628
|
+
async maybeCompact(messages, trajectory, source = "outer") {
|
|
485
629
|
preflightUnsupportedMedia(this.config, messages);
|
|
486
630
|
try {
|
|
487
|
-
const
|
|
631
|
+
const settings = {
|
|
488
632
|
contextWindow: this.config.contextWindow,
|
|
489
633
|
triggerRatio: this.config.contextCompactionRatio,
|
|
490
634
|
summaryMaxTokens: this.config.compactionSummaryMaxTokens,
|
|
491
|
-
|
|
635
|
+
reasoningEffort: this.config.reasoningEffort,
|
|
636
|
+
compactionThresholdTokens: isCodeArenaMode(this.config.mode) ? undefined
|
|
637
|
+
: this.options.compactionThresholdTokens ?? this.config.compactionThresholdTokens,
|
|
638
|
+
legacy: isCodeArenaMode(this.config.mode) || this.config.compactionMode === "summary",
|
|
639
|
+
};
|
|
640
|
+
const contextLength = messages.length;
|
|
641
|
+
const result = settings.legacy
|
|
642
|
+
? await compactMessagesIfNeeded(messages, this.tools.availableTools(), this.client, this.config.model, settings, this.options.signal)
|
|
643
|
+
: await maintainSessionMemory(messages, messages, this.memoryCache, this.tools.availableTools(), this.client, this.config.model, settings, this.config.memoryUpdateTokens, this.options.signal, update => {
|
|
644
|
+
this.sink.emit({ type: "memory.update", session_id: this.sessionId, ...update });
|
|
645
|
+
// Serialized with message checkpoints; persistence failures surface at
|
|
646
|
+
// the next checkpoint/finish instead of becoming unhandled rejections.
|
|
647
|
+
return this.checkpoint(trajectory).catch(() => undefined);
|
|
648
|
+
});
|
|
492
649
|
if (result.compacted) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
650
|
+
if (!isCodeArenaMode(this.config.mode)) {
|
|
651
|
+
const covered = settings.legacy ? result.coveredMessages : this.memoryCache.state.coveredMessages;
|
|
652
|
+
const retained = contextLength - covered;
|
|
653
|
+
// Insert at the covered boundary, before the retained original tail.
|
|
654
|
+
// Marker position is the resume boundary; no separate index is stored.
|
|
655
|
+
let insertion = trajectory.length;
|
|
656
|
+
let remaining = retained;
|
|
657
|
+
while (remaining > 0 && insertion > 0) {
|
|
658
|
+
if (trajectory[--insertion].role !== "compaction")
|
|
659
|
+
remaining--;
|
|
660
|
+
}
|
|
661
|
+
trajectory.splice(insertion, 0, {
|
|
662
|
+
role: "compaction", content: [{ type: "text", text: result.memory }],
|
|
663
|
+
undone: false,
|
|
664
|
+
compaction: {
|
|
665
|
+
ratio: result.estimatedTokensBefore / this.config.contextWindow,
|
|
666
|
+
tokens_at_compaction: result.estimatedTokensBefore,
|
|
667
|
+
source,
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
// Coverage indices referred to the pre-compaction history. The next
|
|
671
|
+
// update reads its prior memory from the newly embedded block.
|
|
672
|
+
this.memoryCache.state = undefined;
|
|
673
|
+
this.memoryCache.latest = undefined;
|
|
674
|
+
this.memoryCache.latestUsage = undefined;
|
|
675
|
+
this.memoryCache.attemptedPrefix = undefined;
|
|
676
|
+
this.persistedMemory = undefined;
|
|
677
|
+
}
|
|
678
|
+
await this.checkpoint(trajectory);
|
|
496
679
|
this.sink.compaction(this.sessionId, result.usage);
|
|
497
680
|
}
|
|
498
681
|
return result.compacted;
|
|
@@ -504,7 +687,7 @@ export class Agent {
|
|
|
504
687
|
return false;
|
|
505
688
|
}
|
|
506
689
|
}
|
|
507
|
-
async assistantTurn(messages, offeredTools) {
|
|
690
|
+
async assistantTurn(messages, offeredTools, trajectory) {
|
|
508
691
|
// A mid-stream network fault loses only the tokens produced so far, not the
|
|
509
692
|
// conversation: the request is idempotent, so the whole turn is re-issued
|
|
510
693
|
// from scratch. Accumulators live inside the loop and reset each attempt, so
|
|
@@ -525,6 +708,51 @@ export class Agent {
|
|
|
525
708
|
const responseItems = new Map();
|
|
526
709
|
let usage;
|
|
527
710
|
let reasoningDetails;
|
|
711
|
+
const detectors = this.options.midturnCheckpointHook ? {
|
|
712
|
+
content: new RepetitiveOutputDetector(),
|
|
713
|
+
reasoning: new RepetitiveOutputDetector(),
|
|
714
|
+
} : undefined;
|
|
715
|
+
const controller = detectors ? new AbortController() : undefined;
|
|
716
|
+
const signal = controller
|
|
717
|
+
? this.options.signal
|
|
718
|
+
? AbortSignal.any([this.options.signal, controller.signal])
|
|
719
|
+
: controller.signal
|
|
720
|
+
: this.options.signal;
|
|
721
|
+
let repetitionReported = false;
|
|
722
|
+
let checkpointControl;
|
|
723
|
+
const inspectOutput = async (channel, text) => {
|
|
724
|
+
if (!detectors || repetitionReported)
|
|
725
|
+
return;
|
|
726
|
+
const sample = detectors[channel].append(text);
|
|
727
|
+
if (sample === undefined)
|
|
728
|
+
return;
|
|
729
|
+
repetitionReported = true;
|
|
730
|
+
const event = {
|
|
731
|
+
type: "repetitive_output",
|
|
732
|
+
sessionId: this.sessionId,
|
|
733
|
+
toolCalls: [],
|
|
734
|
+
repetition: { channel, sample },
|
|
735
|
+
};
|
|
736
|
+
try {
|
|
737
|
+
const action = checkpointAction(await this.options.midturnCheckpointHook(event));
|
|
738
|
+
if (action === "snapshot_exit") {
|
|
739
|
+
checkpointControl = new CheckpointExit(event);
|
|
740
|
+
}
|
|
741
|
+
else if (action === "snapshot_continue") {
|
|
742
|
+
await this.makeCheckpoint(trajectory, offeredTools, event, action);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
checkpointControl = new CheckpointFailure(error);
|
|
747
|
+
}
|
|
748
|
+
if (checkpointControl) {
|
|
749
|
+
// Abort the transport, then let for-await close its iterator before
|
|
750
|
+
// the caller snapshots or reports an error. Never save this partial
|
|
751
|
+
// assistant or retry a policy decision as a network failure.
|
|
752
|
+
controller.abort(checkpointControl);
|
|
753
|
+
throw checkpointControl;
|
|
754
|
+
}
|
|
755
|
+
};
|
|
528
756
|
try {
|
|
529
757
|
const stream = this.client.stream({
|
|
530
758
|
model: this.config.model,
|
|
@@ -539,7 +767,7 @@ export class Agent {
|
|
|
539
767
|
enableThinking: this.config.enableThinking,
|
|
540
768
|
thinkingBudget: this.config.thinkingBudget,
|
|
541
769
|
preserveThinking: this.config.preserveThinking,
|
|
542
|
-
signal
|
|
770
|
+
signal,
|
|
543
771
|
});
|
|
544
772
|
for await (const chunk of stream) {
|
|
545
773
|
// With stream_options.include_usage the usage arrives on the final
|
|
@@ -558,12 +786,16 @@ export class Agent {
|
|
|
558
786
|
const content = String(delta.content);
|
|
559
787
|
contentParts.push(content);
|
|
560
788
|
this.sink.delta(this.sessionId, { content });
|
|
789
|
+
if (detectors)
|
|
790
|
+
await inspectOutput("content", content);
|
|
561
791
|
}
|
|
562
792
|
const reasoning = delta.reasoning_content || delta.reasoning;
|
|
563
793
|
if (reasoning) {
|
|
564
794
|
const value = String(reasoning);
|
|
565
795
|
reasoningParts.push(value);
|
|
566
796
|
this.sink.delta(this.sessionId, { reasoning_content: value });
|
|
797
|
+
if (detectors)
|
|
798
|
+
await inspectOutput("reasoning", value);
|
|
567
799
|
}
|
|
568
800
|
for (const [position, fragment] of (delta.tool_calls || []).entries()) {
|
|
569
801
|
const index = Number(fragment.index ?? position);
|
|
@@ -599,6 +831,9 @@ export class Agent {
|
|
|
599
831
|
}
|
|
600
832
|
}
|
|
601
833
|
catch (error) {
|
|
834
|
+
// Iterator cleanup may itself report the intentional transport abort.
|
|
835
|
+
if (checkpointControl)
|
|
836
|
+
throw checkpointControl;
|
|
602
837
|
// A user cancellation is not a transient fault: skip the retry, but
|
|
603
838
|
// still fall through so any partial output is preserved in history.
|
|
604
839
|
const aborted = Boolean(this.options.signal?.aborted);
|