@qwen-code/arena-agent 0.0.10-dev.0 → 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 +240 -57
- package/dist/agent.d.ts +30 -2
- package/dist/agent.js +351 -104
- 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 +197 -34
- 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 +383 -170
- 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 +35 -1
- package/dist/history.js +400 -16
- 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 +18 -11
- 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/sdk.js +9 -1
- package/dist/sdk.js.map +1 -1
- package/dist/server.js +127 -44
- 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 +744 -121
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +20 -12
- 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/app.js +17 -3
- package/dist/web/app.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 +10 -4
- 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 +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);
|
|
@@ -142,6 +194,11 @@ function preflightUnsupportedMedia(config, messages) {
|
|
|
142
194
|
!isCodeArenaMode(config.mode);
|
|
143
195
|
return replaceUnsupportedMedia(messages, mediaTypes, imageReadFileFallback);
|
|
144
196
|
}
|
|
197
|
+
function modelContextFromTrajectory(messages) {
|
|
198
|
+
return contextFromHistory(messages).map((message) => message.role === "user" && Array.isArray(message.content)
|
|
199
|
+
? { ...message, content: structuredClone(message.content) }
|
|
200
|
+
: message);
|
|
201
|
+
}
|
|
145
202
|
function completeInterruptedToolCalls(messages) {
|
|
146
203
|
const completed = [];
|
|
147
204
|
let pending = [];
|
|
@@ -176,6 +233,13 @@ function completeInterruptedToolCalls(messages) {
|
|
|
176
233
|
flushPending();
|
|
177
234
|
return completed;
|
|
178
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
|
+
}
|
|
179
243
|
const identityModelsWithQwen = "Claude, ChatGPT, Gemini, Grok, Qwen, and Kimi";
|
|
180
244
|
const identityModelsWithoutQwen = "Claude, ChatGPT, Gemini, Grok, and Kimi";
|
|
181
245
|
const arenaWorkspaceIdentityOpening = "You are a helpful agentic assistant with tool access running on Arena.ai's Agent Mode.";
|
|
@@ -209,19 +273,12 @@ function withAppendedSystemPrompt(content, appendix) {
|
|
|
209
273
|
: "\n\n";
|
|
210
274
|
return `${content}${separator}${appendix}`;
|
|
211
275
|
}
|
|
212
|
-
function applySystemPromptConfig(message, config
|
|
276
|
+
function applySystemPromptConfig(message, config) {
|
|
213
277
|
if (message?.role !== "system" || typeof message.content !== "string") {
|
|
214
278
|
return;
|
|
215
279
|
}
|
|
216
280
|
const appendix = config.appendSystemPrompt;
|
|
217
|
-
|
|
218
|
-
appendix &&
|
|
219
|
-
message.content.endsWith(`\n\n${appendix}`)
|
|
220
|
-
? appendix
|
|
221
|
-
: undefined;
|
|
222
|
-
let content = existingAppendix
|
|
223
|
-
? message.content.slice(0, -existingAppendix.length)
|
|
224
|
-
: message.content;
|
|
281
|
+
let content = message.content;
|
|
225
282
|
if (typeof config.systemPrompt === "string") {
|
|
226
283
|
content = config.systemPrompt;
|
|
227
284
|
}
|
|
@@ -234,11 +291,7 @@ function applySystemPromptConfig(message, config, preserveExistingAppendix = fal
|
|
|
234
291
|
? content.replace(identityModelsWithQwen, identityModelsWithoutQwen)
|
|
235
292
|
: content.replace(identityModelsWithoutQwen, identityModelsWithQwen);
|
|
236
293
|
}
|
|
237
|
-
|
|
238
|
-
if (reuseExistingAppendix) {
|
|
239
|
-
content += existingAppendix;
|
|
240
|
-
}
|
|
241
|
-
if (appendix !== undefined && !reuseExistingAppendix) {
|
|
294
|
+
if (appendix !== undefined) {
|
|
242
295
|
content = withAppendedSystemPrompt(content, appendix);
|
|
243
296
|
}
|
|
244
297
|
message.content = content;
|
|
@@ -253,20 +306,33 @@ class PartialAssistantTurnError extends Error {
|
|
|
253
306
|
this.name = "PartialAssistantTurnError";
|
|
254
307
|
}
|
|
255
308
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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);
|
|
265
327
|
}
|
|
266
328
|
export class Agent {
|
|
267
329
|
config;
|
|
268
330
|
sink;
|
|
269
331
|
options;
|
|
332
|
+
checkpointQueue = Promise.resolve();
|
|
333
|
+
checkpointError;
|
|
334
|
+
persistedMemory;
|
|
335
|
+
memoryCache;
|
|
270
336
|
client;
|
|
271
337
|
tools;
|
|
272
338
|
workspaceContext;
|
|
@@ -276,40 +342,58 @@ export class Agent {
|
|
|
276
342
|
this.config = config;
|
|
277
343
|
this.sink = sink;
|
|
278
344
|
this.options = options;
|
|
345
|
+
this.memoryCache = options.sessionMemory || {};
|
|
279
346
|
this.sessionId =
|
|
280
347
|
options.sessionId || crypto.randomUUID().replaceAll("-", "");
|
|
281
348
|
this.sessionAffinityId = options.sessionAffinityId || crypto.randomUUID();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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));
|
|
291
361
|
this.client =
|
|
292
362
|
options.client ||
|
|
293
363
|
(config.model.startsWith("mock/")
|
|
294
364
|
? new MockModelClient()
|
|
295
365
|
: new HttpModelClient(config, {
|
|
296
366
|
sessionAffinityId: this.sessionAffinityId,
|
|
297
|
-
...(
|
|
298
|
-
? {}
|
|
299
|
-
: {
|
|
300
|
-
diagnostics: new JsonlModelCallDiagnostics(modelCallLogPath(config.stateRoot, config.tenantId, this.sessionId)),
|
|
301
|
-
}),
|
|
367
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
302
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);
|
|
303
377
|
this.tools =
|
|
304
378
|
options.tools ||
|
|
305
379
|
new ToolExecutor(config, {
|
|
306
380
|
modelClient: this.client,
|
|
381
|
+
imageInputFallbackClient,
|
|
307
382
|
signal: options.signal,
|
|
308
383
|
askUserHandler: options.askUserHandler,
|
|
309
384
|
workspaceContext: this.workspaceContext,
|
|
310
385
|
});
|
|
311
386
|
}
|
|
312
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) {
|
|
313
397
|
if (runOptions.resumeFromHistory && !history?.length) {
|
|
314
398
|
throw new Error("resumeFromHistory requires a non-empty history");
|
|
315
399
|
}
|
|
@@ -323,57 +407,66 @@ export class Agent {
|
|
|
323
407
|
: structuredClone(baseUserMessage);
|
|
324
408
|
const clientContext = this.options.clientContext || defaultClientContext();
|
|
325
409
|
const repoContext = resolveRepoContext(this.config.workspace, history, this.config.mode);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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");
|
|
337
422
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
trajectory.push(userMessage);
|
|
426
|
+
}
|
|
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;
|
|
351
437
|
}
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
throw new Error("resumeFromHistory history must end with a user or tool message");
|
|
356
|
-
}
|
|
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" };
|
|
357
441
|
}
|
|
358
|
-
|
|
359
|
-
|
|
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);
|
|
451
|
+
}
|
|
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 });
|
|
360
459
|
}
|
|
361
460
|
}
|
|
362
|
-
else {
|
|
363
|
-
trajectory[1] = userMessage;
|
|
364
|
-
}
|
|
365
|
-
// Persisted history is the lossless source of truth for replay and
|
|
366
|
-
// training. Compaction may rewrite only this private model-context copy;
|
|
367
|
-
// every real user, assistant, and tool message is appended to both.
|
|
368
|
-
const messages = structuredClone(trajectory);
|
|
369
461
|
const recordMessage = async (message) => {
|
|
370
|
-
|
|
371
|
-
|
|
462
|
+
const normalized = normalizeToolCallHistory(message, this.config.mode);
|
|
463
|
+
messages.push(normalized);
|
|
464
|
+
const recorded = structuredClone(normalized);
|
|
372
465
|
trajectory.push(recorded);
|
|
373
466
|
this.sink.message(this.sessionId, recorded, trajectory);
|
|
374
467
|
await this.checkpoint(trajectory);
|
|
375
468
|
};
|
|
376
|
-
await this.maybeCompact(messages);
|
|
469
|
+
await this.maybeCompact(messages, trajectory);
|
|
377
470
|
this.sink.start(this.sessionId, trajectory);
|
|
378
471
|
try {
|
|
379
472
|
await this.checkpoint(trajectory);
|
|
@@ -391,14 +484,21 @@ export class Agent {
|
|
|
391
484
|
// stores its encrypted reasoning output items there, so compaction is
|
|
392
485
|
// safe at the same tool-step boundaries as the other protocols.
|
|
393
486
|
if (step > 1)
|
|
394
|
-
await this.maybeCompact(messages);
|
|
487
|
+
await this.maybeCompact(messages, trajectory, "inner");
|
|
395
488
|
const offeredTools = this.tools.availableTools();
|
|
396
489
|
this.sink.modelStart(this.sessionId, step, offeredTools.map((tool) => tool.function.name));
|
|
397
490
|
let assistant;
|
|
398
491
|
try {
|
|
399
|
-
assistant = await this.assistantTurn(messages, offeredTools);
|
|
492
|
+
assistant = await this.assistantTurn(messages, offeredTools, trajectory);
|
|
400
493
|
}
|
|
401
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;
|
|
402
502
|
if (error instanceof PartialAssistantTurnError) {
|
|
403
503
|
this.options.onModelFailure?.(trajectory, error.cause);
|
|
404
504
|
if (error.assistant) {
|
|
@@ -411,9 +511,24 @@ export class Agent {
|
|
|
411
511
|
}
|
|
412
512
|
const finishReason = assistant.tool_calls?.length ? "tool_calls" : "stop";
|
|
413
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
|
+
}
|
|
414
529
|
await recordMessage(assistant);
|
|
415
530
|
if (!assistant.tool_calls?.length) {
|
|
416
|
-
return this.
|
|
531
|
+
return await this.finish(trajectory, "success");
|
|
417
532
|
}
|
|
418
533
|
const completedToolCallIds = [];
|
|
419
534
|
for (const call of assistant.tool_calls) {
|
|
@@ -445,7 +560,7 @@ export class Agent {
|
|
|
445
560
|
? toolResult.questions
|
|
446
561
|
: [];
|
|
447
562
|
this.sink.inputRequired(this.sessionId, toolCallId, questions);
|
|
448
|
-
return this.
|
|
563
|
+
return await this.finish(trajectory, "needs_input");
|
|
449
564
|
}
|
|
450
565
|
}
|
|
451
566
|
if (this.options.signal?.aborted) {
|
|
@@ -456,28 +571,111 @@ export class Agent {
|
|
|
456
571
|
if (visualMessage) {
|
|
457
572
|
await recordMessage(visualMessage);
|
|
458
573
|
}
|
|
574
|
+
if (runOptions.singleStep) {
|
|
575
|
+
return await this.pause(trajectory, "single_step");
|
|
576
|
+
}
|
|
459
577
|
}
|
|
460
578
|
}
|
|
461
579
|
catch (error) {
|
|
462
580
|
const message = error instanceof Error ? error.message : String(error);
|
|
463
|
-
return this.
|
|
581
|
+
return await this.finish(trajectory, "error", message);
|
|
464
582
|
}
|
|
465
583
|
}
|
|
466
584
|
async checkpoint(messages) {
|
|
467
|
-
|
|
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);
|
|
618
|
+
}
|
|
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);
|
|
468
627
|
}
|
|
469
|
-
async maybeCompact(messages) {
|
|
628
|
+
async maybeCompact(messages, trajectory, source = "outer") {
|
|
470
629
|
preflightUnsupportedMedia(this.config, messages);
|
|
471
630
|
try {
|
|
472
|
-
const
|
|
631
|
+
const settings = {
|
|
473
632
|
contextWindow: this.config.contextWindow,
|
|
474
633
|
triggerRatio: this.config.contextCompactionRatio,
|
|
475
634
|
summaryMaxTokens: this.config.compactionSummaryMaxTokens,
|
|
476
|
-
|
|
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
|
+
});
|
|
477
649
|
if (result.compacted) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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);
|
|
481
679
|
this.sink.compaction(this.sessionId, result.usage);
|
|
482
680
|
}
|
|
483
681
|
return result.compacted;
|
|
@@ -489,7 +687,7 @@ export class Agent {
|
|
|
489
687
|
return false;
|
|
490
688
|
}
|
|
491
689
|
}
|
|
492
|
-
async assistantTurn(messages, offeredTools) {
|
|
690
|
+
async assistantTurn(messages, offeredTools, trajectory) {
|
|
493
691
|
// A mid-stream network fault loses only the tokens produced so far, not the
|
|
494
692
|
// conversation: the request is idempotent, so the whole turn is re-issued
|
|
495
693
|
// from scratch. Accumulators live inside the loop and reset each attempt, so
|
|
@@ -499,11 +697,9 @@ export class Agent {
|
|
|
499
697
|
// original content, and route-specific rejections keep the runtime fallback.
|
|
500
698
|
preflightUnsupportedMedia(this.config, messages);
|
|
501
699
|
const maxRetries = Math.max(0, Math.floor(this.config.modelMaxRetries ?? 10));
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
let requestMessages = structuredClone(messages);
|
|
506
|
-
const requestTools = structuredClone(offeredTools);
|
|
700
|
+
// Each attempt receives one fresh clone, so a stateful ModelClient cannot
|
|
701
|
+
// contaminate the retry. Keeping an additional immutable clone of the same
|
|
702
|
+
// request doubled the largest live allocation without adding isolation.
|
|
507
703
|
let multimodalFallbackApplied = false;
|
|
508
704
|
for (let attempt = 0;; attempt += 1) {
|
|
509
705
|
const contentParts = [];
|
|
@@ -512,11 +708,56 @@ export class Agent {
|
|
|
512
708
|
const responseItems = new Map();
|
|
513
709
|
let usage;
|
|
514
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
|
+
};
|
|
515
756
|
try {
|
|
516
757
|
const stream = this.client.stream({
|
|
517
758
|
model: this.config.model,
|
|
518
|
-
messages: structuredClone(
|
|
519
|
-
tools: structuredClone(
|
|
759
|
+
messages: structuredClone(messages),
|
|
760
|
+
tools: structuredClone(offeredTools),
|
|
520
761
|
toolChoice: this.config.toolChoice,
|
|
521
762
|
temperature: this.config.temperature,
|
|
522
763
|
maxTokens: this.config.maxTokens,
|
|
@@ -526,7 +767,7 @@ export class Agent {
|
|
|
526
767
|
enableThinking: this.config.enableThinking,
|
|
527
768
|
thinkingBudget: this.config.thinkingBudget,
|
|
528
769
|
preserveThinking: this.config.preserveThinking,
|
|
529
|
-
signal
|
|
770
|
+
signal,
|
|
530
771
|
});
|
|
531
772
|
for await (const chunk of stream) {
|
|
532
773
|
// With stream_options.include_usage the usage arrives on the final
|
|
@@ -545,12 +786,16 @@ export class Agent {
|
|
|
545
786
|
const content = String(delta.content);
|
|
546
787
|
contentParts.push(content);
|
|
547
788
|
this.sink.delta(this.sessionId, { content });
|
|
789
|
+
if (detectors)
|
|
790
|
+
await inspectOutput("content", content);
|
|
548
791
|
}
|
|
549
792
|
const reasoning = delta.reasoning_content || delta.reasoning;
|
|
550
793
|
if (reasoning) {
|
|
551
794
|
const value = String(reasoning);
|
|
552
795
|
reasoningParts.push(value);
|
|
553
796
|
this.sink.delta(this.sessionId, { reasoning_content: value });
|
|
797
|
+
if (detectors)
|
|
798
|
+
await inspectOutput("reasoning", value);
|
|
554
799
|
}
|
|
555
800
|
for (const [position, fragment] of (delta.tool_calls || []).entries()) {
|
|
556
801
|
const index = Number(fragment.index ?? position);
|
|
@@ -586,6 +831,9 @@ export class Agent {
|
|
|
586
831
|
}
|
|
587
832
|
}
|
|
588
833
|
catch (error) {
|
|
834
|
+
// Iterator cleanup may itself report the intentional transport abort.
|
|
835
|
+
if (checkpointControl)
|
|
836
|
+
throw checkpointControl;
|
|
589
837
|
// A user cancellation is not a transient fault: skip the retry, but
|
|
590
838
|
// still fall through so any partial output is preserved in history.
|
|
591
839
|
const aborted = Boolean(this.options.signal?.aborted);
|
|
@@ -595,7 +843,6 @@ export class Agent {
|
|
|
595
843
|
const replacedMedia = replaceUnsupportedMedia(messages, error.mediaTypes);
|
|
596
844
|
if (replacedMedia > 0) {
|
|
597
845
|
multimodalFallbackApplied = true;
|
|
598
|
-
requestMessages = structuredClone(messages);
|
|
599
846
|
this.sink.modelRetry(this.sessionId, attempt + 1, `model rejected ${error.mediaTypes.join(", ")} input; replaced ` +
|
|
600
847
|
`${replacedMedia} media part(s) with an unsupported-input notice`);
|
|
601
848
|
continue;
|