@qwen-code/arena-agent 0.0.10-dev.9 → 0.0.10-test.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +61 -10
- package/CHANGELOG.md +140 -11
- package/README.md +309 -46
- package/dist/agent.d.ts +20 -6
- package/dist/agent.js +303 -73
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/anthropic.js +46 -57
- package/dist/anthropic.js.map +1 -1
- package/dist/arena-history.d.ts +12 -0
- package/dist/arena-history.js +119 -0
- package/dist/arena-history.js.map +1 -0
- package/dist/ask-user.js +44 -2
- package/dist/ask-user.js.map +1 -1
- package/dist/attachment-limits.d.ts +4 -0
- package/dist/attachment-limits.js +10 -0
- package/dist/attachment-limits.js.map +1 -0
- package/dist/attachments.d.ts +4 -2
- package/dist/attachments.js +9 -11
- package/dist/attachments.js.map +1 -1
- package/dist/brave-search.js +11 -7
- package/dist/brave-search.js.map +1 -1
- package/dist/canonical-data.d.ts +9 -0
- package/dist/canonical-data.js +12 -3
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +140 -40
- package/dist/cli.js.map +1 -1
- package/dist/code-tool-schema.d.ts +3 -0
- package/dist/code-tool-schema.js +92 -0
- package/dist/code-tool-schema.js.map +1 -0
- package/dist/code-tools.js +63 -26
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction-threshold.d.ts +3 -0
- package/dist/compaction-threshold.js +6 -0
- package/dist/compaction-threshold.js.map +1 -0
- package/dist/compaction.d.ts +24 -7
- package/dist/compaction.js +242 -66
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.js +30 -5
- package/dist/config.js.map +1 -1
- package/dist/connector-errors.d.ts +16 -0
- package/dist/connector-errors.js +69 -0
- package/dist/connector-errors.js.map +1 -0
- package/dist/connector-files.d.ts +31 -0
- package/dist/connector-files.js +249 -0
- package/dist/connector-files.js.map +1 -0
- package/dist/connector-input.d.ts +10 -0
- package/dist/connector-input.js +79 -0
- package/dist/connector-input.js.map +1 -0
- package/dist/connector-mcp.d.ts +60 -0
- package/dist/connector-mcp.js +474 -0
- package/dist/connector-mcp.js.map +1 -0
- package/dist/connectors.d.ts +13 -0
- package/dist/connectors.js +69 -0
- package/dist/connectors.js.map +1 -0
- package/dist/fetch-page.js +2 -2
- package/dist/fetch-page.js.map +1 -1
- package/dist/history.d.ts +15 -5
- package/dist/history.js +59 -14
- package/dist/history.js.map +1 -1
- package/dist/image-choice.d.ts +23 -0
- package/dist/image-choice.js +190 -0
- package/dist/image-choice.js.map +1 -0
- package/dist/image-input-fallback.js +5 -0
- package/dist/image-input-fallback.js.map +1 -1
- package/dist/image-model.d.ts +6 -0
- package/dist/image-model.js +32 -0
- package/dist/image-model.js.map +1 -0
- package/dist/midturn-checkpoint-hook.d.ts +14 -1
- package/dist/midturn-checkpoint-hook.js.map +1 -1
- package/dist/mode.js +1 -1
- package/dist/mode.js.map +1 -1
- package/dist/model-capabilities.js +18 -8
- package/dist/model-capabilities.js.map +1 -1
- package/dist/model-router-media.js +55 -3
- package/dist/model-router-media.js.map +1 -1
- package/dist/model-stream.d.ts +16 -0
- package/dist/model-stream.js +129 -0
- package/dist/model-stream.js.map +1 -0
- package/dist/openai-responses.d.ts +2 -1
- package/dist/openai-responses.js +85 -88
- package/dist/openai-responses.js.map +1 -1
- package/dist/process-manager.js +9 -9
- package/dist/process-manager.js.map +1 -1
- package/dist/prompts.d.ts +0 -2
- package/dist/prompts.js +10 -60
- package/dist/prompts.js.map +1 -1
- package/dist/protocol.d.ts +5 -1
- package/dist/protocol.js +23 -2
- package/dist/protocol.js.map +1 -1
- package/dist/provider.d.ts +18 -0
- package/dist/provider.js +120 -51
- package/dist/provider.js.map +1 -1
- package/dist/repo.d.ts +1 -1
- package/dist/repo.js +26 -4
- package/dist/repo.js.map +1 -1
- package/dist/routify-web-fetch.js +2 -2
- package/dist/routify-web-fetch.js.map +1 -1
- package/dist/server.js +55 -27
- package/dist/server.js.map +1 -1
- package/dist/session-memory-prompts.d.ts +3 -1
- package/dist/session-memory-prompts.js +4 -2
- package/dist/session-memory-prompts.js.map +1 -1
- package/dist/side-query.js +5 -0
- package/dist/side-query.js.map +1 -1
- package/dist/spec.d.ts +5 -4
- package/dist/spec.js +30 -11
- package/dist/spec.js.map +1 -1
- package/dist/startup-config.d.ts +39 -0
- package/dist/startup-config.js +347 -0
- package/dist/startup-config.js.map +1 -0
- package/dist/startup-main-config.d.ts +63 -0
- package/dist/startup-main-config.js +94 -0
- package/dist/startup-main-config.js.map +1 -0
- package/dist/startup-tool-config.d.ts +8 -0
- package/dist/startup-tool-config.js +116 -0
- package/dist/startup-tool-config.js.map +1 -0
- package/dist/summary-compaction.d.ts +29 -0
- package/dist/{legacy-compaction.js → summary-compaction.js} +79 -14
- package/dist/summary-compaction.js.map +1 -0
- package/dist/tools.d.ts +5 -1
- package/dist/tools.js +164 -54
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +26 -6
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/web/app.js +30 -5
- package/dist/web/app.js.map +1 -1
- package/dist/workspace.js +61 -14
- package/dist/workspace.js.map +1 -1
- package/docs/BINARY_DISTRIBUTION.md +44 -8
- package/docs/MODES_AND_RELEASES.md +79 -6
- package/docs/SDK.md +1 -0
- package/modes/code-arena/capture-provenance.json +9 -6
- package/modes/code-arena/full-stack/dependencies.lock.json +8670 -0
- package/modes/code-arena/full-stack/system-prompt.txt +1 -1
- package/modes/code-arena/html/system-prompt.txt +1 -1
- package/modes/code-arena/react/dependencies.lock.json +2707 -0
- package/package.json +7 -2
- package/scripts/README.md +23 -4
- package/web/styles.css +22 -0
- package/dist/legacy-compaction.d.ts +0 -16
- package/dist/legacy-compaction.js.map +0 -1
package/dist/agent.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { agentArenaCompactionThreshold } from "./compaction-threshold.js";
|
|
2
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
5
|
import { supportsModelInput, } from "./config.js";
|
|
4
|
-
import { compactMessagesIfNeeded, maintainSessionMemory, contextFromHistory } from "./compaction.js";
|
|
6
|
+
import { compactMessagesIfNeeded, maintainSessionMemory, contextFromHistory, sessionMemoryMatchesHistory, settleSessionMemory, storedSessionMemory } from "./compaction.js";
|
|
5
7
|
import { ANTHROPIC_INCOMPLETE_STREAM_ERROR_CODE } from "./anthropic.js";
|
|
8
|
+
import { rememberRestoredCalls, restoredArenaHistory } from "./arena-history.js";
|
|
6
9
|
import { modelCallLogPath, withoutLeadingSystemMessage } from "./history.js";
|
|
7
10
|
import { MockModelClient } from "./mock.js";
|
|
11
|
+
import { CAPACITY_MODEL_ERROR, INCOMPLETE_MODEL_RESPONSE, TRANSIENT_MODEL_ERROR, incompleteModelResponse } from "./model-stream.js";
|
|
8
12
|
import { contentPartMediaTypes, JsonlModelCallDiagnostics, } from "./model-diagnostics.js";
|
|
9
|
-
import {
|
|
13
|
+
import { RESPONSES_INCOMPLETE_STREAM_ERROR_CODE } from "./openai-responses.js";
|
|
10
14
|
import { HttpModelClient, ModelCapabilityError } from "./provider.js";
|
|
11
15
|
import { RepetitiveOutputDetector } from "./repetitive-output.js";
|
|
12
16
|
import { detectRepoContext } from "./repo.js";
|
|
13
|
-
import { isCodeArenaMode } from "./mode.js";
|
|
17
|
+
import { defaultStepLimit, isCodeArenaMode } from "./mode.js";
|
|
14
18
|
import { defaultClientContext, interruptedUserMessage, repoSystemMessage, systemMessage, } from "./spec.js";
|
|
15
19
|
import { imageInputFallbackConnection, ToolExecutor, } from "./tools.js";
|
|
16
20
|
import { initializeWorkspace } from "./workspace.js";
|
|
@@ -25,7 +29,7 @@ function wrapMalformedToolArguments(argumentsText) {
|
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
/** Keep Agent Arena history replayable without losing the model's invalid input. */
|
|
28
|
-
function normalizeToolCallHistory(message, mode) {
|
|
32
|
+
function normalizeToolCallHistory(message, mode, historyCall) {
|
|
29
33
|
if (isCodeArenaMode(mode) || message.role !== "assistant")
|
|
30
34
|
return message;
|
|
31
35
|
// A failed Responses stream can retain completed native items without the
|
|
@@ -37,7 +41,10 @@ function normalizeToolCallHistory(message, mode) {
|
|
|
37
41
|
? [{ id: item.call_id, type: "function", function: { name: item.name, arguments: item.arguments } }]
|
|
38
42
|
: []) ?? message.tool_calls;
|
|
39
43
|
let changed = Boolean(calls?.length && calls !== message.tool_calls);
|
|
40
|
-
const toolCalls = calls?.map((
|
|
44
|
+
const toolCalls = calls?.map((original) => {
|
|
45
|
+
const call = historyCall ? historyCall(original) : original;
|
|
46
|
+
if (call !== original)
|
|
47
|
+
changed = true;
|
|
41
48
|
if (typeof call.function?.arguments !== "string")
|
|
42
49
|
return call;
|
|
43
50
|
const argumentsText = wrapMalformedToolArguments(call.function.arguments);
|
|
@@ -51,7 +58,8 @@ function normalizeToolCallHistory(message, mode) {
|
|
|
51
58
|
const responseItems = message.openai_response_items?.map((item) => {
|
|
52
59
|
if (item.type !== "function_call" || typeof item.arguments !== "string")
|
|
53
60
|
return item;
|
|
54
|
-
const
|
|
61
|
+
const paired = toolCalls?.find(call => call.id === item.call_id);
|
|
62
|
+
const argumentsText = paired?.function.arguments ?? wrapMalformedToolArguments(item.arguments);
|
|
55
63
|
if (argumentsText === item.arguments)
|
|
56
64
|
return item;
|
|
57
65
|
changed = true;
|
|
@@ -73,6 +81,41 @@ function parseToolResult(content) {
|
|
|
73
81
|
return {};
|
|
74
82
|
}
|
|
75
83
|
}
|
|
84
|
+
/** Live Agent Arena results arrive in completion order. Checkpoint writes stay
|
|
85
|
+
* serialized by the consumer, and every started operation settles before exit.
|
|
86
|
+
* Code Arena retains its sequential execution path.
|
|
87
|
+
*/
|
|
88
|
+
async function* completedTools(calls, parallel, execute) {
|
|
89
|
+
if (!parallel) {
|
|
90
|
+
for (const call of calls)
|
|
91
|
+
yield { call, message: await execute(call) };
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const queue = [];
|
|
95
|
+
let wake;
|
|
96
|
+
const settled = Promise.all(calls.map(async (call) => {
|
|
97
|
+
try {
|
|
98
|
+
queue.push({ status: "fulfilled", value: { call, message: await execute(call) } });
|
|
99
|
+
}
|
|
100
|
+
catch (reason) {
|
|
101
|
+
queue.push({ status: "rejected", reason });
|
|
102
|
+
}
|
|
103
|
+
wake?.();
|
|
104
|
+
}));
|
|
105
|
+
try {
|
|
106
|
+
for (let count = 0; count < calls.length; count++) {
|
|
107
|
+
if (!queue.length)
|
|
108
|
+
await new Promise(resolve => { wake = resolve; });
|
|
109
|
+
const result = queue.shift();
|
|
110
|
+
if (result.status === "rejected")
|
|
111
|
+
throw result.reason;
|
|
112
|
+
yield result.value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await settled;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
76
119
|
// Network faults that abort an in-flight stream (a dropped socket, a reset, a
|
|
77
120
|
// DNS blip). undici surfaces these mid-stream as `TypeError: fetch failed` or
|
|
78
121
|
// `TypeError: terminated`; some are also reachable via an errno-style code on
|
|
@@ -93,8 +136,11 @@ const retryableStreamCodes = new Set([
|
|
|
93
136
|
"UND_ERR_BODY_TIMEOUT",
|
|
94
137
|
"ARENA_STREAM_IDLE_TIMEOUT",
|
|
95
138
|
ANTHROPIC_INCOMPLETE_STREAM_ERROR_CODE,
|
|
96
|
-
|
|
97
|
-
|
|
139
|
+
CAPACITY_MODEL_ERROR,
|
|
140
|
+
TRANSIENT_MODEL_ERROR,
|
|
141
|
+
RESPONSES_INCOMPLETE_STREAM_ERROR_CODE,
|
|
142
|
+
"ARENA_EMPTY_MODEL_RESPONSE",
|
|
143
|
+
INCOMPLETE_MODEL_RESPONSE,
|
|
98
144
|
]);
|
|
99
145
|
const capacityErrorMaxRetries = 5;
|
|
100
146
|
function describeError(error) {
|
|
@@ -114,7 +160,7 @@ function isRetryableStreamError(error) {
|
|
|
114
160
|
return code !== undefined && retryableStreamCodes.has(code);
|
|
115
161
|
}
|
|
116
162
|
function streamRetryLimit(error, configuredLimit) {
|
|
117
|
-
return errorCode(error) ===
|
|
163
|
+
return errorCode(error) === CAPACITY_MODEL_ERROR
|
|
118
164
|
? Math.min(configuredLimit, capacityErrorMaxRetries)
|
|
119
165
|
: configuredLimit;
|
|
120
166
|
}
|
|
@@ -320,15 +366,20 @@ class CheckpointFailure extends Error {
|
|
|
320
366
|
this.cause = cause;
|
|
321
367
|
}
|
|
322
368
|
}
|
|
323
|
-
//
|
|
369
|
+
// Refresh live Git state on every run, using the saved target when available.
|
|
324
370
|
// 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);
|
|
371
|
+
export function resolveRepoContext(workspace, _history, mode = "arena", repoDirectory) {
|
|
372
|
+
return isCodeArenaMode(mode) ? undefined : detectRepoContext(workspace, repoDirectory);
|
|
327
373
|
}
|
|
328
374
|
export class Agent {
|
|
329
375
|
config;
|
|
330
376
|
sink;
|
|
331
377
|
options;
|
|
378
|
+
checkpointQueue = Promise.resolve();
|
|
379
|
+
restoredToolCalls = new Set();
|
|
380
|
+
checkpointError;
|
|
381
|
+
persistedMemory;
|
|
382
|
+
repoDirectory;
|
|
332
383
|
memoryCache;
|
|
333
384
|
client;
|
|
334
385
|
tools;
|
|
@@ -340,6 +391,7 @@ export class Agent {
|
|
|
340
391
|
this.sink = sink;
|
|
341
392
|
this.options = options;
|
|
342
393
|
this.memoryCache = options.sessionMemory || {};
|
|
394
|
+
this.repoDirectory = options.repoDirectory;
|
|
343
395
|
this.sessionId =
|
|
344
396
|
options.sessionId || crypto.randomUUID().replaceAll("-", "");
|
|
345
397
|
this.sessionAffinityId = options.sessionAffinityId || crypto.randomUUID();
|
|
@@ -355,19 +407,24 @@ export class Agent {
|
|
|
355
407
|
(!needsMainHttpClient && !needsFallbackHttpClient)
|
|
356
408
|
? undefined
|
|
357
409
|
: new JsonlModelCallDiagnostics(modelCallLogPath(config.stateRoot, config.tenantId, this.sessionId));
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
(
|
|
361
|
-
|
|
362
|
-
:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
410
|
+
const client = options.client ||
|
|
411
|
+
(config.model.startsWith("mock/")
|
|
412
|
+
? new MockModelClient()
|
|
413
|
+
: new HttpModelClient(config, {
|
|
414
|
+
sessionAffinityId: this.sessionAffinityId,
|
|
415
|
+
onRequest: options.onModelRequest,
|
|
416
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
417
|
+
}));
|
|
418
|
+
this.client = isCodeArenaMode(config.mode) ? client : {
|
|
419
|
+
stream: request => client.stream({ ...request,
|
|
420
|
+
messages: restoredArenaHistory(request.messages, this.restoredToolCalls) }),
|
|
421
|
+
};
|
|
366
422
|
const imageInputFallbackClient = options.imageInputFallbackClient ||
|
|
367
423
|
(fallbackConnection
|
|
368
424
|
? new HttpModelClient(config, {
|
|
369
425
|
connection: fallbackConnection,
|
|
370
426
|
sessionAffinityId: this.sessionAffinityId,
|
|
427
|
+
onRequest: options.onModelRequest,
|
|
371
428
|
...(diagnostics ? { diagnostics } : {}),
|
|
372
429
|
})
|
|
373
430
|
: undefined);
|
|
@@ -382,6 +439,16 @@ export class Agent {
|
|
|
382
439
|
});
|
|
383
440
|
}
|
|
384
441
|
async run(prompt, history, runOptions = {}) {
|
|
442
|
+
try {
|
|
443
|
+
return await this.runTurn(prompt, history, runOptions);
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
await this.tools.closeConnectors?.();
|
|
447
|
+
await settleSessionMemory(this.memoryCache);
|
|
448
|
+
await this.checkpointQueue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async runTurn(prompt, history, runOptions) {
|
|
385
452
|
if (runOptions.resumeFromHistory && !history?.length) {
|
|
386
453
|
throw new Error("resumeFromHistory requires a non-empty history");
|
|
387
454
|
}
|
|
@@ -394,15 +461,25 @@ export class Agent {
|
|
|
394
461
|
? interruptedUserMessage(baseUserMessage, this.config.mode)
|
|
395
462
|
: structuredClone(baseUserMessage);
|
|
396
463
|
const clientContext = this.options.clientContext || defaultClientContext();
|
|
397
|
-
const repoContext = resolveRepoContext(this.config.workspace, history, this.config.mode);
|
|
464
|
+
const repoContext = resolveRepoContext(this.config.workspace, history, this.config.mode, this.repoDirectory);
|
|
465
|
+
if (repoContext) {
|
|
466
|
+
this.repoDirectory = path.relative(this.config.workspace, repoContext.checkoutPath);
|
|
467
|
+
}
|
|
398
468
|
// System instructions belong to this invocation, never to the saved dialogue.
|
|
399
469
|
// Older local histories included a system message; accept them without
|
|
400
470
|
// carrying the old runtime configuration into the next request.
|
|
401
471
|
const dialogue = withoutLeadingSystemMessage(history || []).map((message) => normalizeToolCallHistory(message, this.config.mode));
|
|
472
|
+
this.restoredToolCalls.clear();
|
|
473
|
+
if (!isCodeArenaMode(this.config.mode))
|
|
474
|
+
rememberRestoredCalls(dialogue, this.restoredToolCalls);
|
|
402
475
|
const trajectory = completeInterruptedToolCalls(this.options.reuseHistoryMessages ? [...dialogue] : structuredClone(dialogue));
|
|
476
|
+
const connectors = this.config.disallowedTools?.includes("list_connector_tools")
|
|
477
|
+
? undefined : this.config.connectors;
|
|
478
|
+
const connectedConnectors = this.config.disallowedTools?.includes("list_connector_tools")
|
|
479
|
+
? undefined : this.config.connectedConnectors;
|
|
403
480
|
const runtimeSystemMessage = repoContext
|
|
404
|
-
? repoSystemMessage(clientContext, repoContext, this.config.mode, this.workspaceContext, this.config.maxTurns)
|
|
405
|
-
: systemMessage(clientContext, this.workspaceContext, this.config.mode, this.config.maxTurns);
|
|
481
|
+
? repoSystemMessage(clientContext, repoContext, this.config.mode, this.workspaceContext, this.config.maxTurns, connectors, connectedConnectors)
|
|
482
|
+
: systemMessage(clientContext, this.workspaceContext, this.config.mode, this.config.maxTurns, connectors, connectedConnectors);
|
|
406
483
|
applySystemPromptConfig(runtimeSystemMessage, this.config);
|
|
407
484
|
if (runOptions.resumeFromHistory) {
|
|
408
485
|
if (!isContinuableHistory(dialogue)) {
|
|
@@ -412,26 +489,65 @@ export class Agent {
|
|
|
412
489
|
else {
|
|
413
490
|
trajectory.push(userMessage);
|
|
414
491
|
}
|
|
492
|
+
// A new human reply requires discovery again. Same-turn continuation only
|
|
493
|
+
// reloads schemas from paired discovery results; it never repeats writes.
|
|
494
|
+
await this.tools.beginConnectorTurn?.(runOptions.resumeFromHistory ? dialogue : undefined);
|
|
415
495
|
// Compaction projects model input with a runtime system prefix; the lossless
|
|
416
496
|
// trajectory and its checkpoints remain in the official dialogue format.
|
|
417
497
|
const messages = modelContextFromTrajectory([runtimeSystemMessage, ...trajectory]);
|
|
498
|
+
if (!isCodeArenaMode(this.config.mode)) {
|
|
499
|
+
const saved = this.persistedMemory ?? this.options.storedSessionMemory ?? storedSessionMemory(this.memoryCache);
|
|
500
|
+
preflightUnsupportedMedia(this.config, messages);
|
|
501
|
+
// Opting out of restoration must not delete a still-valid durable snapshot.
|
|
502
|
+
this.persistedMemory = saved?.version === 1 ? structuredClone(saved) : undefined;
|
|
503
|
+
if (this.persistedMemory?.latest && !sessionMemoryMatchesHistory(this.persistedMemory.latest, messages)) {
|
|
504
|
+
this.persistedMemory.latest = undefined;
|
|
505
|
+
}
|
|
506
|
+
if (this.persistedMemory?.update?.status === "pending") {
|
|
507
|
+
this.persistedMemory.update = { status: "cancelled", code: "MEMORY_INTERRUPTED",
|
|
508
|
+
error: "Previous process ended during a memory update" };
|
|
509
|
+
}
|
|
510
|
+
this.memoryCache.latest = undefined;
|
|
511
|
+
this.memoryCache.state = undefined;
|
|
512
|
+
this.memoryCache.latestUsage = undefined;
|
|
513
|
+
this.memoryCache.attemptedPrefix = undefined;
|
|
514
|
+
this.memoryCache.update = undefined;
|
|
515
|
+
if (this.config.compactionMode !== "summary" &&
|
|
516
|
+
(runOptions.restoreSessionMemory ?? this.config.restoreSessionMemory ?? true) && saved?.version === 1) {
|
|
517
|
+
if (sessionMemoryMatchesHistory(saved.latest, messages)) {
|
|
518
|
+
this.memoryCache.latest = structuredClone(saved.latest);
|
|
519
|
+
}
|
|
520
|
+
this.memoryCache.update = this.persistedMemory?.update;
|
|
521
|
+
if (saved.latest && !this.memoryCache.latest) {
|
|
522
|
+
this.memoryCache.update = { status: "error", code: "MEMORY_STALE_HISTORY",
|
|
523
|
+
error: "Saved memory does not match the current history prefix" };
|
|
524
|
+
}
|
|
525
|
+
if (this.memoryCache.update)
|
|
526
|
+
this.sink.emit({ type: "memory.update", session_id: this.sessionId, ...this.memoryCache.update });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
418
529
|
const recordMessage = async (message) => {
|
|
419
|
-
const normalized = normalizeToolCallHistory(message, this.config.mode);
|
|
530
|
+
const normalized = normalizeToolCallHistory(message, this.config.mode, call => this.tools.historyCall(call));
|
|
420
531
|
messages.push(normalized);
|
|
421
532
|
const recorded = structuredClone(normalized);
|
|
422
533
|
trajectory.push(recorded);
|
|
423
534
|
this.sink.message(this.sessionId, recorded, trajectory);
|
|
424
535
|
await this.checkpoint(trajectory);
|
|
425
536
|
};
|
|
426
|
-
await this.maybeCompact(messages, trajectory);
|
|
427
|
-
this.sink.start(this.sessionId, trajectory);
|
|
428
537
|
try {
|
|
538
|
+
const compaction = await this.maybeCompact(messages, trajectory);
|
|
539
|
+
this.sink.start(this.sessionId, trajectory);
|
|
540
|
+
if (compaction === "checkpoint")
|
|
541
|
+
return await this.pause(trajectory, "checkpoint");
|
|
429
542
|
await this.checkpoint(trajectory);
|
|
430
543
|
const maxTurns = this.config.maxTurns
|
|
431
544
|
? Math.max(1, Math.floor(this.config.maxTurns))
|
|
432
|
-
: undefined;
|
|
545
|
+
: isCodeArenaMode(this.config.mode) ? undefined : defaultStepLimit("arena");
|
|
433
546
|
for (let step = 1;; step += 1) {
|
|
434
547
|
if (maxTurns !== undefined && step > maxTurns) {
|
|
548
|
+
if (!isCodeArenaMode(this.config.mode)) {
|
|
549
|
+
return await this.pause(trajectory, "step_limit");
|
|
550
|
+
}
|
|
435
551
|
throw new Error(`agent reached max_turns limit (${maxTurns})`);
|
|
436
552
|
}
|
|
437
553
|
if (this.options.signal?.aborted) {
|
|
@@ -440,13 +556,15 @@ export class Agent {
|
|
|
440
556
|
// Every transport is replayable from local history. Native Responses
|
|
441
557
|
// stores its encrypted reasoning output items there, so compaction is
|
|
442
558
|
// safe at the same tool-step boundaries as the other protocols.
|
|
443
|
-
if (step > 1)
|
|
444
|
-
await this.
|
|
559
|
+
if (step > 1 && await this.maybeCompact(messages, trajectory, "inner") === "checkpoint") {
|
|
560
|
+
return await this.pause(trajectory, "checkpoint");
|
|
561
|
+
}
|
|
445
562
|
const offeredTools = this.tools.availableTools();
|
|
446
563
|
this.sink.modelStart(this.sessionId, step, offeredTools.map((tool) => tool.function.name));
|
|
447
564
|
let assistant;
|
|
565
|
+
let finishReason;
|
|
448
566
|
try {
|
|
449
|
-
assistant = await this.assistantTurn(messages, offeredTools, trajectory);
|
|
567
|
+
({ assistant, finishReason } = await this.assistantTurn(messages, offeredTools, trajectory));
|
|
450
568
|
}
|
|
451
569
|
catch (error) {
|
|
452
570
|
if (error instanceof CheckpointExit) {
|
|
@@ -466,8 +584,19 @@ export class Agent {
|
|
|
466
584
|
this.options.onModelFailure?.(trajectory, error);
|
|
467
585
|
throw error;
|
|
468
586
|
}
|
|
469
|
-
const finishReason = assistant.tool_calls?.length ? "tool_calls" : "stop";
|
|
470
587
|
this.sink.modelComplete(this.sessionId, step, finishReason, assistant.usage);
|
|
588
|
+
if (finishReason === "length") {
|
|
589
|
+
// A completed but truncated request is not a transport failure. Keep
|
|
590
|
+
// its evidence without retrying it or executing unfinished tool calls.
|
|
591
|
+
await recordMessage(assistant);
|
|
592
|
+
throw new Error("Model response was truncated (finish_reason=length). " +
|
|
593
|
+
"The turn did not complete; any partial output has been preserved.");
|
|
594
|
+
}
|
|
595
|
+
if (finishReason === "incomplete" || finishReason === "content_filter") {
|
|
596
|
+
await recordMessage(assistant);
|
|
597
|
+
throw new Error(`Model response was incomplete (finish_reason=${finishReason}). ` +
|
|
598
|
+
"The turn did not complete; any partial output has been preserved.");
|
|
599
|
+
}
|
|
471
600
|
if (assistant.tool_calls?.length &&
|
|
472
601
|
this.options.midturnCheckpointHook) {
|
|
473
602
|
const event = {
|
|
@@ -485,49 +614,65 @@ export class Agent {
|
|
|
485
614
|
}
|
|
486
615
|
await recordMessage(assistant);
|
|
487
616
|
if (!assistant.tool_calls?.length) {
|
|
488
|
-
return this.
|
|
617
|
+
return await this.finish(trajectory, "success");
|
|
489
618
|
}
|
|
490
619
|
const completedToolCallIds = [];
|
|
491
|
-
|
|
620
|
+
let needsInput = false;
|
|
621
|
+
const execute = async (call) => {
|
|
492
622
|
const toolCallId = String(call.id || "");
|
|
493
623
|
const toolName = String(call.function?.name || "tool");
|
|
494
624
|
this.sink.toolStart(this.sessionId, toolCallId, toolName);
|
|
495
|
-
|
|
625
|
+
return this.tools.execute(call, toolName === "ask_user" || toolName === "add_voice" ||
|
|
626
|
+
(!isCodeArenaMode(this.config.mode) && toolName === "generate_image")
|
|
496
627
|
? {
|
|
497
628
|
sessionId: this.sessionId,
|
|
498
629
|
messages: structuredClone(trajectory),
|
|
499
630
|
signal: this.options.signal,
|
|
500
631
|
}
|
|
501
632
|
: undefined);
|
|
633
|
+
};
|
|
634
|
+
for await (const { call, message: toolMessage } of completedTools(assistant.tool_calls, !isCodeArenaMode(this.config.mode), execute)) {
|
|
635
|
+
const toolCallId = String(call.id || "");
|
|
636
|
+
const toolName = String(call.function?.name || "tool");
|
|
502
637
|
const toolResult = parseToolResult(toolMessage.content);
|
|
503
638
|
const skippedAskUser = toolName === "ask_user" && toolResult.skipped === true;
|
|
504
639
|
if (this.options.signal?.aborted && !skippedAskUser) {
|
|
505
640
|
throw this.options.signal.reason || new Error("operation cancelled");
|
|
506
641
|
}
|
|
507
642
|
await recordMessage(toolMessage);
|
|
643
|
+
if (this.tools.takeInteractionBoundary(toolCallId)) {
|
|
644
|
+
rememberRestoredCalls(messages, this.restoredToolCalls);
|
|
645
|
+
}
|
|
508
646
|
completedToolCallIds.push(toolCallId);
|
|
509
647
|
this.sink.toolComplete(this.sessionId, toolCallId, toolName);
|
|
510
|
-
if (toolName === "ask_user" || toolName === "add_voice"
|
|
648
|
+
if (toolName === "ask_user" || toolName === "add_voice" ||
|
|
649
|
+
(!isCodeArenaMode(this.config.mode) && toolName === "generate_image")) {
|
|
511
650
|
if (toolResult.status === "needs_input") {
|
|
512
|
-
const visualMessage = this.tools.takeVisualMessage(completedToolCallIds);
|
|
513
|
-
if (visualMessage) {
|
|
514
|
-
await recordMessage(visualMessage);
|
|
515
|
-
}
|
|
516
651
|
const questions = Array.isArray(toolResult.questions)
|
|
517
652
|
? toolResult.questions
|
|
518
653
|
: [];
|
|
519
654
|
this.sink.inputRequired(this.sessionId, toolCallId, questions);
|
|
520
|
-
|
|
655
|
+
if (isCodeArenaMode(this.config.mode)) {
|
|
656
|
+
const visualMessage = this.tools.takeVisualMessage(completedToolCallIds);
|
|
657
|
+
if (visualMessage)
|
|
658
|
+
await recordMessage(visualMessage);
|
|
659
|
+
return await this.finish(trajectory, "needs_input");
|
|
660
|
+
}
|
|
661
|
+
needsInput = true;
|
|
521
662
|
}
|
|
522
663
|
}
|
|
523
664
|
if (this.options.signal?.aborted) {
|
|
524
665
|
throw this.options.signal.reason || new Error("operation cancelled");
|
|
525
666
|
}
|
|
526
667
|
}
|
|
527
|
-
|
|
668
|
+
// Image parts retain the assistant's declaration order independently
|
|
669
|
+
// of which tool result completed first.
|
|
670
|
+
const visualMessage = this.tools.takeVisualMessage(assistant.tool_calls.map(call => call.id));
|
|
528
671
|
if (visualMessage) {
|
|
529
672
|
await recordMessage(visualMessage);
|
|
530
673
|
}
|
|
674
|
+
if (needsInput)
|
|
675
|
+
return await this.finish(trajectory, "needs_input");
|
|
531
676
|
if (runOptions.singleStep) {
|
|
532
677
|
return await this.pause(trajectory, "single_step");
|
|
533
678
|
}
|
|
@@ -535,13 +680,38 @@ export class Agent {
|
|
|
535
680
|
}
|
|
536
681
|
catch (error) {
|
|
537
682
|
const message = error instanceof Error ? error.message : String(error);
|
|
538
|
-
return this.
|
|
683
|
+
return await this.finish(trajectory, "error", message);
|
|
539
684
|
}
|
|
540
685
|
}
|
|
541
686
|
async checkpoint(messages) {
|
|
542
|
-
|
|
687
|
+
const snapshot = [...messages];
|
|
688
|
+
const tools = this.tools.availableTools();
|
|
689
|
+
const currentMemory = storedSessionMemory(this.memoryCache);
|
|
690
|
+
const memory = isCodeArenaMode(this.config.mode) ? undefined : (currentMemory || this.persistedMemory
|
|
691
|
+
? { version: 1, latest: currentMemory?.latest ?? this.persistedMemory?.latest,
|
|
692
|
+
update: currentMemory?.update ?? this.persistedMemory?.update }
|
|
693
|
+
: null);
|
|
694
|
+
if (memory !== undefined)
|
|
695
|
+
this.persistedMemory = memory ?? undefined;
|
|
696
|
+
const write = this.checkpointQueue.then(() => this.options.onCheckpoint?.(snapshot, tools, memory));
|
|
697
|
+
this.checkpointQueue = write.catch(error => { this.checkpointError ??= error; });
|
|
698
|
+
await write;
|
|
699
|
+
if (this.checkpointError)
|
|
700
|
+
throw this.checkpointError;
|
|
701
|
+
}
|
|
702
|
+
async finish(messages, status, error) {
|
|
703
|
+
await settleSessionMemory(this.memoryCache);
|
|
704
|
+
try {
|
|
705
|
+
await this.checkpoint(messages);
|
|
706
|
+
}
|
|
707
|
+
catch (failure) {
|
|
708
|
+
status = "error";
|
|
709
|
+
error ??= failure instanceof Error ? failure.message : String(failure);
|
|
710
|
+
}
|
|
711
|
+
return this.sink.result(this.sessionId, status, messages, error, undefined, this.memoryCache.update);
|
|
543
712
|
}
|
|
544
713
|
async makeCheckpoint(messages, tools, event, action) {
|
|
714
|
+
await settleSessionMemory(this.memoryCache, event.type !== "before_compaction" || action !== "snapshot_continue");
|
|
545
715
|
await this.checkpoint(messages);
|
|
546
716
|
if (!this.options.onMidturnCheckpoint) {
|
|
547
717
|
throw new Error("mid-turn checkpoint hook triggered without a snapshot handler");
|
|
@@ -550,9 +720,12 @@ export class Agent {
|
|
|
550
720
|
}
|
|
551
721
|
async pause(messages, reason) {
|
|
552
722
|
this.options.signal?.throwIfAborted();
|
|
723
|
+
await settleSessionMemory(this.memoryCache, reason !== "single_step");
|
|
724
|
+
if (reason === "single_step" || reason === "step_limit")
|
|
725
|
+
await this.checkpoint(messages);
|
|
553
726
|
await this.options.onPause?.(messages, this.tools.availableTools());
|
|
554
727
|
this.options.signal?.throwIfAborted();
|
|
555
|
-
return this.sink.result(this.sessionId, "paused", messages, undefined, reason);
|
|
728
|
+
return this.sink.result(this.sessionId, "paused", messages, undefined, reason, this.memoryCache.update);
|
|
556
729
|
}
|
|
557
730
|
async maybeCompact(messages, trajectory, source = "outer") {
|
|
558
731
|
preflightUnsupportedMedia(this.config, messages);
|
|
@@ -562,50 +735,83 @@ export class Agent {
|
|
|
562
735
|
triggerRatio: this.config.contextCompactionRatio,
|
|
563
736
|
summaryMaxTokens: this.config.compactionSummaryMaxTokens,
|
|
564
737
|
reasoningEffort: this.config.reasoningEffort,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
738
|
+
compactionThresholdTokens: isCodeArenaMode(this.config.mode) ? undefined
|
|
739
|
+
: agentArenaCompactionThreshold(this.config.contextWindow, this.config.contextCompactionRatio, this.options.compactionThresholdTokens ?? this.config.compactionThresholdTokens),
|
|
740
|
+
legacy: isCodeArenaMode(this.config.mode) || this.config.compactionMode === "summary",
|
|
741
|
+
onBeforeCompaction: !isCodeArenaMode(this.config.mode) && this.options.midturnCheckpointHook
|
|
742
|
+
? async (estimatedTokensBefore, thresholdTokens) => {
|
|
743
|
+
const event = {
|
|
744
|
+
type: "before_compaction", sessionId: this.sessionId, toolCalls: [],
|
|
745
|
+
compaction: {
|
|
746
|
+
count: trajectory.filter(message => message.role === "compaction" && !message.undone).length + 1,
|
|
747
|
+
mode: this.config.compactionMode === "summary" ? "summary" : "rolling",
|
|
748
|
+
source, estimatedTokensBefore, thresholdTokens,
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
let action;
|
|
752
|
+
try {
|
|
753
|
+
action = checkpointAction(await this.options.midturnCheckpointHook(event));
|
|
754
|
+
if (action !== "continue") {
|
|
755
|
+
await this.makeCheckpoint(trajectory, this.tools.availableTools(), event, action);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
throw new CheckpointFailure(error);
|
|
760
|
+
}
|
|
761
|
+
if (action === "snapshot_exit")
|
|
762
|
+
throw new CheckpointExit(event);
|
|
763
|
+
}
|
|
764
|
+
: undefined,
|
|
569
765
|
};
|
|
570
766
|
const contextLength = messages.length;
|
|
571
767
|
const result = settings.legacy
|
|
572
768
|
? await compactMessagesIfNeeded(messages, this.tools.availableTools(), this.client, this.config.model, settings, this.options.signal)
|
|
573
|
-
: await maintainSessionMemory(messages, messages, this.memoryCache, this.tools.availableTools(), this.client, this.config.model, settings, this.config.memoryUpdateTokens, this.options.signal,
|
|
574
|
-
|
|
769
|
+
: await maintainSessionMemory(messages, messages, this.memoryCache, this.tools.availableTools(), this.client, this.config.model, settings, this.config.memoryUpdateTokens, this.options.signal, update => {
|
|
770
|
+
this.sink.emit({ type: "memory.update", session_id: this.sessionId, ...update });
|
|
771
|
+
// Serialized with message checkpoints; persistence failures surface at
|
|
772
|
+
// the next checkpoint/finish instead of becoming unhandled rejections.
|
|
773
|
+
return this.checkpoint(trajectory).catch(() => undefined);
|
|
774
|
+
});
|
|
575
775
|
if (result.compacted) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
776
|
+
const covered = settings.legacy ? result.coveredMessages : this.memoryCache.state.coveredMessages;
|
|
777
|
+
const retained = contextLength - covered;
|
|
778
|
+
// Insert at the covered boundary, before the retained original tail.
|
|
779
|
+
// Marker position is the resume boundary; no separate index is stored.
|
|
780
|
+
let insertion = trajectory.length;
|
|
781
|
+
let remaining = retained;
|
|
782
|
+
while (remaining > 0 && insertion > 0) {
|
|
783
|
+
if (trajectory[--insertion].role !== "compaction")
|
|
784
|
+
remaining--;
|
|
785
|
+
}
|
|
786
|
+
trajectory.splice(insertion, 0, {
|
|
787
|
+
role: "compaction", content: [{ type: "text", text: result.memory }],
|
|
788
|
+
undone: false,
|
|
789
|
+
compaction: {
|
|
790
|
+
ratio: result.estimatedTokensBefore / this.config.contextWindow,
|
|
791
|
+
tokens_at_compaction: result.estimatedTokensBefore,
|
|
792
|
+
source,
|
|
793
|
+
retained_messages: retained,
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
if (!isCodeArenaMode(this.config.mode)) {
|
|
596
797
|
// Coverage indices referred to the pre-compaction history. The next
|
|
597
798
|
// update reads its prior memory from the newly embedded block.
|
|
598
799
|
this.memoryCache.state = undefined;
|
|
599
800
|
this.memoryCache.latest = undefined;
|
|
600
801
|
this.memoryCache.latestUsage = undefined;
|
|
601
802
|
this.memoryCache.attemptedPrefix = undefined;
|
|
803
|
+
this.persistedMemory = undefined;
|
|
602
804
|
}
|
|
603
805
|
await this.checkpoint(trajectory);
|
|
604
806
|
this.sink.compaction(this.sessionId, result.usage);
|
|
605
807
|
}
|
|
606
808
|
return result.compacted;
|
|
607
809
|
}
|
|
608
|
-
catch {
|
|
810
|
+
catch (error) {
|
|
811
|
+
if (error instanceof CheckpointExit)
|
|
812
|
+
return "checkpoint";
|
|
813
|
+
if (error instanceof CheckpointFailure)
|
|
814
|
+
throw error.cause;
|
|
609
815
|
// Compaction is protective rather than task-critical. If its dedicated
|
|
610
816
|
// summary request fails, keep the original history and let the normal
|
|
611
817
|
// model request (and its retry policy) decide whether it still fits.
|
|
@@ -632,6 +838,7 @@ export class Agent {
|
|
|
632
838
|
const calls = new Map();
|
|
633
839
|
const responseItems = new Map();
|
|
634
840
|
let usage;
|
|
841
|
+
let finishReason;
|
|
635
842
|
let reasoningDetails;
|
|
636
843
|
const detectors = this.options.midturnCheckpointHook ? {
|
|
637
844
|
content: new RepetitiveOutputDetector(),
|
|
@@ -700,6 +907,9 @@ export class Agent {
|
|
|
700
907
|
// letting the last non-empty value win.
|
|
701
908
|
if (chunk.usage)
|
|
702
909
|
usage = chunk.usage;
|
|
910
|
+
if (chunk.choices?.[0]?.finish_reason) {
|
|
911
|
+
finishReason = chunk.choices[0].finish_reason;
|
|
912
|
+
}
|
|
703
913
|
if (chunk.reasoning_details?.length) {
|
|
704
914
|
reasoningDetails = chunk.reasoning_details;
|
|
705
915
|
}
|
|
@@ -738,6 +948,11 @@ export class Agent {
|
|
|
738
948
|
if (fragment.function?.arguments) {
|
|
739
949
|
call.function.arguments += String(fragment.function.arguments);
|
|
740
950
|
}
|
|
951
|
+
// Metadata is an opaque value for this index, not a text delta.
|
|
952
|
+
// Later fragments that omit it must not erase an earlier signature.
|
|
953
|
+
if (fragment.extra_content !== undefined) {
|
|
954
|
+
call.extra_content = structuredClone(fragment.extra_content);
|
|
955
|
+
}
|
|
741
956
|
calls.set(index, call);
|
|
742
957
|
this.sink.delta(this.sessionId, {
|
|
743
958
|
tool_calls: [
|
|
@@ -749,11 +964,23 @@ export class Agent {
|
|
|
749
964
|
name: String(fragment.function?.name || ""),
|
|
750
965
|
arguments: String(fragment.function?.arguments || ""),
|
|
751
966
|
},
|
|
967
|
+
...(fragment.extra_content !== undefined
|
|
968
|
+
? { extra_content: structuredClone(fragment.extra_content) }
|
|
969
|
+
: {}),
|
|
752
970
|
},
|
|
753
971
|
],
|
|
754
972
|
});
|
|
755
973
|
}
|
|
756
974
|
}
|
|
975
|
+
if (finishReason === "tool_calls" && !calls.size) {
|
|
976
|
+
throw incompleteModelResponse("Model stopped for tool calls but returned no tool calls");
|
|
977
|
+
}
|
|
978
|
+
if (finishReason !== "length" && finishReason !== "incomplete" && finishReason !== "content_filter" &&
|
|
979
|
+
!calls.size && !contentParts.join("").trim()) {
|
|
980
|
+
throw Object.assign(new Error("Model returned an empty response without tool calls"), {
|
|
981
|
+
code: "ARENA_EMPTY_MODEL_RESPONSE",
|
|
982
|
+
});
|
|
983
|
+
}
|
|
757
984
|
}
|
|
758
985
|
catch (error) {
|
|
759
986
|
// Iterator cleanup may itself report the intentional transport abort.
|
|
@@ -785,7 +1012,7 @@ export class Agent {
|
|
|
785
1012
|
const openaiResponseItems = [...responseItems.entries()]
|
|
786
1013
|
.sort(([left], [right]) => left - right)
|
|
787
1014
|
.map(([, item]) => item);
|
|
788
|
-
const partial = content || reasoningContent || openaiResponseItems.length
|
|
1015
|
+
const partial = content || reasoningContent || openaiResponseItems.length || usage
|
|
789
1016
|
? {
|
|
790
1017
|
role: "assistant",
|
|
791
1018
|
content,
|
|
@@ -821,7 +1048,10 @@ export class Agent {
|
|
|
821
1048
|
.sort(([left], [right]) => left - right)
|
|
822
1049
|
.map(([, call]) => call);
|
|
823
1050
|
}
|
|
824
|
-
return
|
|
1051
|
+
return {
|
|
1052
|
+
assistant,
|
|
1053
|
+
finishReason: finishReason ?? (assistant.tool_calls?.length ? "tool_calls" : "stop"),
|
|
1054
|
+
};
|
|
825
1055
|
}
|
|
826
1056
|
}
|
|
827
1057
|
async retryBackoff(attempt) {
|