@qwen-code/arena-agent 0.0.8 → 0.0.9-dev.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 +41 -11
- package/README.md +128 -29
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +57 -36
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +12 -4
- package/dist/cli.js.map +1 -1
- package/dist/compaction.js +2 -1
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +73 -9
- package/dist/config.js.map +1 -1
- package/dist/conversation-metrics.d.ts +61 -0
- package/dist/conversation-metrics.js +433 -0
- package/dist/conversation-metrics.js.map +1 -0
- package/dist/document-reader.js +9 -0
- package/dist/document-reader.js.map +1 -1
- package/dist/history.d.ts +31 -2
- package/dist/history.js +53 -3
- package/dist/history.js.map +1 -1
- package/dist/metrics.d.ts +61 -0
- package/dist/metrics.js +578 -0
- package/dist/metrics.js.map +1 -0
- package/dist/mock.js +153 -0
- package/dist/mock.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.js +9 -2
- package/dist/protocol.js.map +1 -1
- package/dist/provider.js +22 -8
- package/dist/provider.js.map +1 -1
- package/dist/runtime-assets.d.ts +3 -0
- package/dist/runtime-assets.js +16 -0
- package/dist/runtime-assets.js.map +1 -0
- package/dist/sdk.d.ts +28 -1
- package/dist/sdk.js +38 -0
- package/dist/sdk.js.map +1 -1
- package/dist/server.js +486 -36
- package/dist/server.js.map +1 -1
- package/dist/side-query.d.ts +31 -0
- package/dist/side-query.js +138 -0
- package/dist/side-query.js.map +1 -0
- package/dist/spec.js +4 -1
- package/dist/spec.js.map +1 -1
- package/dist/tools.js +122 -45
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +4 -3
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +155 -0
- package/dist/web/app.d.ts +1 -0
- package/dist/web/app.js +333 -10
- package/dist/web/app.js.map +1 -1
- package/docs/SDK.md +563 -0
- package/package.json +5 -1
- package/web/index.html +25 -0
- package/web/styles.css +109 -3
package/dist/server.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import { cp, mkdir, readFile, readdir, realpath, rename, stat, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { cp, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
4
|
import http from "node:http";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { Agent } from "./agent.js";
|
|
7
7
|
import { attachmentMimeType, attachmentUserMessage, maxAttachmentBytesPerMessage, maxAttachmentsPerMessage, maxUploadBytes, mimeTypes, saveAttachment, uploadsDirectory, } from "./attachments.js";
|
|
8
8
|
import { configFromEnv } from "./config.js";
|
|
9
|
-
import { loadStoredHistory, saveStoredHistory, sessionStorageRoot, } from "./history.js";
|
|
9
|
+
import { historyMetadata, loadStoredHistory, loadStoredHistoryState, saveStoredHistory, sessionStorageRoot, } from "./history.js";
|
|
10
|
+
import { ModelBashMetricJudge, SessionMetricsAnalyzer, } from "./metrics.js";
|
|
11
|
+
import { ModelConversationMetricJudge } from "./conversation-metrics.js";
|
|
12
|
+
import { MockModelClient } from "./mock.js";
|
|
10
13
|
import { EventSink } from "./protocol.js";
|
|
11
|
-
import {
|
|
14
|
+
import { HttpModelClient } from "./provider.js";
|
|
15
|
+
import { embeddedAsset } from "./runtime-assets.js";
|
|
16
|
+
import { StructuredSideQueryRunner } from "./side-query.js";
|
|
17
|
+
import { projectRoot, toolDefinitions } from "./spec.js";
|
|
12
18
|
const sessionSeedExcludes = new Set([
|
|
13
19
|
".arena",
|
|
14
20
|
".cache",
|
|
@@ -154,6 +160,69 @@ function sessionHistoryPath(session) {
|
|
|
154
160
|
function sessionMetadataPath(session) {
|
|
155
161
|
return path.join(session.root, "session.json");
|
|
156
162
|
}
|
|
163
|
+
function sessionUndoRoot(session) {
|
|
164
|
+
return path.join(session.root, "undo");
|
|
165
|
+
}
|
|
166
|
+
function sessionUndoMetadataPath(session) {
|
|
167
|
+
return path.join(sessionUndoRoot(session), "checkpoint.json");
|
|
168
|
+
}
|
|
169
|
+
function sessionUndoWorkspacePath(session) {
|
|
170
|
+
return path.join(sessionUndoRoot(session), "workspace");
|
|
171
|
+
}
|
|
172
|
+
function sessionRetryPath(session) {
|
|
173
|
+
return path.join(session.root, "model-retry.json");
|
|
174
|
+
}
|
|
175
|
+
function historyHash(messages) {
|
|
176
|
+
return crypto
|
|
177
|
+
.createHash("sha256")
|
|
178
|
+
.update(JSON.stringify({ messages }))
|
|
179
|
+
.digest("hex");
|
|
180
|
+
}
|
|
181
|
+
async function loadModelRetry(session) {
|
|
182
|
+
try {
|
|
183
|
+
const value = JSON.parse(await readFile(sessionRetryPath(session), "utf8"));
|
|
184
|
+
if (value.version !== 1 ||
|
|
185
|
+
value.sessionId !== session.id ||
|
|
186
|
+
typeof value.messageId !== "string" ||
|
|
187
|
+
typeof value.prompt !== "string" ||
|
|
188
|
+
typeof value.failedAt !== "number" ||
|
|
189
|
+
typeof value.error !== "string" ||
|
|
190
|
+
typeof value.historyHash !== "string" ||
|
|
191
|
+
!/^[a-f0-9]{64}$/.test(value.historyHash)) {
|
|
192
|
+
throw new Error("invalid persisted model retry state");
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code === "ENOENT")
|
|
198
|
+
return undefined;
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function saveModelRetry(session, retry) {
|
|
203
|
+
const target = sessionRetryPath(session);
|
|
204
|
+
const temporary = `${target}.${crypto.randomUUID()}.tmp`;
|
|
205
|
+
await writeFile(temporary, JSON.stringify(retry), "utf8");
|
|
206
|
+
await rename(temporary, target);
|
|
207
|
+
}
|
|
208
|
+
async function clearModelRetry(session) {
|
|
209
|
+
await rm(sessionRetryPath(session), { force: true });
|
|
210
|
+
}
|
|
211
|
+
async function modelRetryForHistory(session, messages) {
|
|
212
|
+
let retry;
|
|
213
|
+
try {
|
|
214
|
+
retry = await loadModelRetry(session);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
await clearModelRetry(session);
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
if (retry && historyHash(messages) !== retry.historyHash) {
|
|
221
|
+
await clearModelRetry(session);
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
return retry;
|
|
225
|
+
}
|
|
157
226
|
function hashToken(token) {
|
|
158
227
|
return crypto.createHash("sha256").update(token).digest("hex");
|
|
159
228
|
}
|
|
@@ -206,6 +275,120 @@ async function saveSessionMetadata(session) {
|
|
|
206
275
|
await writeFile(temporary, JSON.stringify(metadata), "utf8");
|
|
207
276
|
await rename(temporary, target);
|
|
208
277
|
}
|
|
278
|
+
async function loadUndoCheckpoint(session) {
|
|
279
|
+
try {
|
|
280
|
+
const checkpoint = JSON.parse(await readFile(sessionUndoMetadataPath(session), "utf8"));
|
|
281
|
+
if (checkpoint.version !== 1 ||
|
|
282
|
+
typeof checkpoint.messageId !== "string" ||
|
|
283
|
+
typeof checkpoint.input !== "string" ||
|
|
284
|
+
!Array.isArray(checkpoint.attachments) ||
|
|
285
|
+
!Array.isArray(checkpoint.messages) ||
|
|
286
|
+
typeof checkpoint.createdAt !== "number") {
|
|
287
|
+
throw new Error("invalid persisted undo checkpoint");
|
|
288
|
+
}
|
|
289
|
+
const workspace = await stat(sessionUndoWorkspacePath(session));
|
|
290
|
+
if (!workspace.isDirectory()) {
|
|
291
|
+
throw new Error("undo checkpoint workspace is not a directory");
|
|
292
|
+
}
|
|
293
|
+
return checkpoint;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (error.code === "ENOENT")
|
|
297
|
+
return null;
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function snapshotIncludes(root, source) {
|
|
302
|
+
const relative = path.relative(root, source);
|
|
303
|
+
if (!relative)
|
|
304
|
+
return true;
|
|
305
|
+
return !relative
|
|
306
|
+
.split(path.sep)
|
|
307
|
+
.some((part) => sessionSeedExcludes.has(part));
|
|
308
|
+
}
|
|
309
|
+
function checkpointAttachment(attachment) {
|
|
310
|
+
return {
|
|
311
|
+
name: attachment.name,
|
|
312
|
+
path: attachment.path,
|
|
313
|
+
mimeType: attachment.mimeType,
|
|
314
|
+
size: attachment.size,
|
|
315
|
+
...(attachment.imageMimeType
|
|
316
|
+
? { imageMimeType: attachment.imageMimeType }
|
|
317
|
+
: {}),
|
|
318
|
+
...(attachment.sourceUrl ? { sourceUrl: attachment.sourceUrl } : {}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async function installDirectory(staging, target) {
|
|
322
|
+
const previous = `${target}.${crypto.randomUUID()}.previous`;
|
|
323
|
+
let movedPrevious = false;
|
|
324
|
+
try {
|
|
325
|
+
await rename(target, previous);
|
|
326
|
+
movedPrevious = true;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error.code !== "ENOENT")
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
await rename(staging, target);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (movedPrevious)
|
|
337
|
+
await rename(previous, target).catch(() => undefined);
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
if (movedPrevious) {
|
|
341
|
+
await rm(previous, { recursive: true, force: true }).catch(() => undefined);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function prepareUndoCheckpoint(session, messageId, input, attachments) {
|
|
345
|
+
const storedHistory = await loadStoredHistoryState(sessionHistoryPath(session));
|
|
346
|
+
const checkpoint = {
|
|
347
|
+
version: 1,
|
|
348
|
+
messageId,
|
|
349
|
+
input,
|
|
350
|
+
attachments: attachments.map(checkpointAttachment),
|
|
351
|
+
messages: structuredClone((await readHistory(session)) || []),
|
|
352
|
+
...(storedHistory?.metrics
|
|
353
|
+
? { metrics: structuredClone(storedHistory.metrics) }
|
|
354
|
+
: {}),
|
|
355
|
+
createdAt: Date.now(),
|
|
356
|
+
};
|
|
357
|
+
const target = sessionUndoRoot(session);
|
|
358
|
+
const staging = `${target}.${crypto.randomUUID()}.tmp`;
|
|
359
|
+
try {
|
|
360
|
+
await mkdir(staging, { recursive: true });
|
|
361
|
+
await cp(session.workspace, path.join(staging, "workspace"), {
|
|
362
|
+
recursive: true,
|
|
363
|
+
mode: fs.constants.COPYFILE_FICLONE,
|
|
364
|
+
filter: (source) => snapshotIncludes(session.workspace, source),
|
|
365
|
+
verbatimSymlinks: true,
|
|
366
|
+
});
|
|
367
|
+
await writeFile(path.join(staging, "checkpoint.json"), JSON.stringify(checkpoint), "utf8");
|
|
368
|
+
await installDirectory(staging, target);
|
|
369
|
+
session.undoCheckpoint = checkpoint;
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
await rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function restoreUndoWorkspace(session) {
|
|
377
|
+
const source = sessionUndoWorkspacePath(session);
|
|
378
|
+
const staging = `${session.workspace}.${crypto.randomUUID()}.restore`;
|
|
379
|
+
try {
|
|
380
|
+
await cp(source, staging, {
|
|
381
|
+
recursive: true,
|
|
382
|
+
mode: fs.constants.COPYFILE_FICLONE,
|
|
383
|
+
verbatimSymlinks: true,
|
|
384
|
+
});
|
|
385
|
+
await installDirectory(staging, session.workspace);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
await rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
209
392
|
async function seedSessionWorkspace(context, target) {
|
|
210
393
|
await mkdir(target, { recursive: true });
|
|
211
394
|
if (process.env.ARENA_SESSION_WORKSPACE_MODE === "empty") {
|
|
@@ -246,10 +429,13 @@ async function createSession(context, clientContext) {
|
|
|
246
429
|
root: paths.root,
|
|
247
430
|
workspace: paths.workspace,
|
|
248
431
|
artifacts: paths.artifacts,
|
|
432
|
+
history: null,
|
|
433
|
+
undoCheckpoint: null,
|
|
249
434
|
events: [],
|
|
250
435
|
nextEventId: 1,
|
|
251
436
|
subscribers: new Set(),
|
|
252
437
|
running: null,
|
|
438
|
+
preparingTurn: false,
|
|
253
439
|
pendingInput: null,
|
|
254
440
|
pendingInterruption: false,
|
|
255
441
|
interruptionGeneration: 0,
|
|
@@ -304,10 +490,13 @@ async function getSession(context, sessionId) {
|
|
|
304
490
|
root: paths.root,
|
|
305
491
|
workspace: paths.workspace,
|
|
306
492
|
artifacts: paths.artifacts,
|
|
493
|
+
history: null,
|
|
494
|
+
undoCheckpoint: null,
|
|
307
495
|
events: [],
|
|
308
496
|
nextEventId: 1,
|
|
309
497
|
subscribers: new Set(),
|
|
310
498
|
running: null,
|
|
499
|
+
preparingTurn: false,
|
|
311
500
|
pendingInput: null,
|
|
312
501
|
pendingInterruption: metadata.pendingInterruption === true,
|
|
313
502
|
interruptionGeneration: typeof metadata.interruptionGeneration === "number" &&
|
|
@@ -327,6 +516,7 @@ async function getSession(context, sessionId) {
|
|
|
327
516
|
mkdir(loaded.workspace, { recursive: true }),
|
|
328
517
|
mkdir(loaded.artifacts, { recursive: true }),
|
|
329
518
|
]);
|
|
519
|
+
loaded.undoCheckpoint = await loadUndoCheckpoint(loaded);
|
|
330
520
|
const raced = context.sessions.get(sessionId);
|
|
331
521
|
if (raced)
|
|
332
522
|
return raced;
|
|
@@ -546,7 +736,7 @@ function createUiStream(session, messageId) {
|
|
|
546
736
|
};
|
|
547
737
|
emit(session, { type: "start", messageId });
|
|
548
738
|
return {
|
|
549
|
-
consume(event) {
|
|
739
|
+
consume(event, resultOptions = {}) {
|
|
550
740
|
if (event.type === "model.start") {
|
|
551
741
|
state.toolParts = new Map();
|
|
552
742
|
emit(session, {
|
|
@@ -737,6 +927,8 @@ function createUiStream(session, messageId) {
|
|
|
737
927
|
type: "trigger:turn-complete",
|
|
738
928
|
status: event.status,
|
|
739
929
|
error: event.error,
|
|
930
|
+
messageId: state.messageId,
|
|
931
|
+
retryable: resultOptions.retryable === true,
|
|
740
932
|
});
|
|
741
933
|
state.completed = true;
|
|
742
934
|
}
|
|
@@ -757,16 +949,56 @@ function createUiStream(session, messageId) {
|
|
|
757
949
|
};
|
|
758
950
|
}
|
|
759
951
|
async function readHistory(session) {
|
|
760
|
-
|
|
952
|
+
if (session.history)
|
|
953
|
+
return structuredClone(session.history);
|
|
954
|
+
const messages = await loadStoredHistory(sessionHistoryPath(session));
|
|
955
|
+
if (messages)
|
|
956
|
+
session.history = structuredClone(messages);
|
|
957
|
+
return messages ? structuredClone(messages) : undefined;
|
|
958
|
+
}
|
|
959
|
+
async function saveHistory(session, messages, config, metrics) {
|
|
960
|
+
await saveStoredHistory(sessionHistoryPath(session), messages, historyMetadata(config, session.id), metrics);
|
|
961
|
+
session.history = structuredClone([...messages]);
|
|
761
962
|
}
|
|
762
|
-
|
|
763
|
-
|
|
963
|
+
function sessionMetricsAnalyzer(config, signal) {
|
|
964
|
+
const sideQueryModel = config.metricsSideQueryModel || config.model;
|
|
965
|
+
const runner = config.metricsSideQueryEnabled
|
|
966
|
+
? new StructuredSideQueryRunner(sideQueryModel.startsWith("mock/")
|
|
967
|
+
? new MockModelClient()
|
|
968
|
+
: new HttpModelClient(config), {
|
|
969
|
+
model: sideQueryModel,
|
|
970
|
+
maxTokens: config.metricsSideQueryMaxTokens,
|
|
971
|
+
maxAttempts: config.metricsSideQueryMaxAttempts,
|
|
972
|
+
enableThinking: config.metricsSideQueryEnableThinking,
|
|
973
|
+
thinkingBudget: config.metricsSideQueryThinkingBudget,
|
|
974
|
+
signal,
|
|
975
|
+
})
|
|
976
|
+
: undefined;
|
|
977
|
+
return new SessionMetricsAnalyzer({
|
|
978
|
+
fallbackOfferedToolNames: toolDefinitions().map((definition) => definition.function.name),
|
|
979
|
+
...(runner ? { judge: new ModelBashMetricJudge(runner) } : {}),
|
|
980
|
+
...(runner
|
|
981
|
+
? { conversationJudge: new ModelConversationMetricJudge(runner) }
|
|
982
|
+
: {}),
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
function lastAssistantText(messages) {
|
|
986
|
+
if (!messages)
|
|
987
|
+
return "";
|
|
988
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
989
|
+
const message = messages[index];
|
|
990
|
+
if (message?.role === "assistant")
|
|
991
|
+
return message.content;
|
|
992
|
+
}
|
|
993
|
+
return "";
|
|
764
994
|
}
|
|
765
|
-
async function runAgent(context, session, prompt, messageId, interruptionGeneration, userMessage) {
|
|
995
|
+
async function runAgent(context, session, prompt, messageId, controller, interruptionGeneration, userMessage, resumeFromHistory = false) {
|
|
766
996
|
const ui = createUiStream(session, messageId);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
let
|
|
997
|
+
let failedModelCheckpoint;
|
|
998
|
+
let failedModelError;
|
|
999
|
+
let currentOfferedToolNames = [];
|
|
1000
|
+
const offeredToolNamesByCallId = new Map();
|
|
1001
|
+
const currentTurnMessages = [];
|
|
770
1002
|
let sink;
|
|
771
1003
|
const askUserHandler = (toolCallId, questions) => new Promise((resolve, reject) => {
|
|
772
1004
|
if (controller.signal.aborted) {
|
|
@@ -785,10 +1017,20 @@ async function runAgent(context, session, prompt, messageId, interruptionGenerat
|
|
|
785
1017
|
sink = new EventSink({
|
|
786
1018
|
format: "silent",
|
|
787
1019
|
onEvent: (event) => {
|
|
788
|
-
if (event.type === "
|
|
789
|
-
|
|
1020
|
+
if (event.type === "model.start") {
|
|
1021
|
+
currentOfferedToolNames = [...(event.offered_tool_names || [])];
|
|
790
1022
|
}
|
|
791
|
-
else {
|
|
1023
|
+
else if (event.type === "message" && event.message?.role === "assistant") {
|
|
1024
|
+
for (const call of event.message.tool_calls || []) {
|
|
1025
|
+
if (call.id) {
|
|
1026
|
+
offeredToolNamesByCallId.set(call.id, [...currentOfferedToolNames]);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (event.type === "message" && event.message) {
|
|
1031
|
+
currentTurnMessages.push(structuredClone(event.message));
|
|
1032
|
+
}
|
|
1033
|
+
if (event.type !== "result") {
|
|
792
1034
|
ui.consume(event);
|
|
793
1035
|
}
|
|
794
1036
|
},
|
|
@@ -798,12 +1040,17 @@ async function runAgent(context, session, prompt, messageId, interruptionGenerat
|
|
|
798
1040
|
workspace: session.workspace,
|
|
799
1041
|
};
|
|
800
1042
|
try {
|
|
801
|
-
const
|
|
1043
|
+
const storedHistory = await loadStoredHistoryState(sessionHistoryPath(session));
|
|
1044
|
+
const history = storedHistory?.messages;
|
|
802
1045
|
const result = await new Agent(sessionConfig, sink, {
|
|
803
1046
|
askUserHandler,
|
|
804
1047
|
sessionId: session.id,
|
|
805
1048
|
signal: controller.signal,
|
|
806
|
-
onCheckpoint: (messages) => saveHistory(session, messages),
|
|
1049
|
+
onCheckpoint: (messages) => saveHistory(session, messages, sessionConfig),
|
|
1050
|
+
onModelFailure: (messages, error) => {
|
|
1051
|
+
failedModelCheckpoint = structuredClone([...messages]);
|
|
1052
|
+
failedModelError = error;
|
|
1053
|
+
},
|
|
807
1054
|
clientContext: {
|
|
808
1055
|
currentDate: session.currentDate,
|
|
809
1056
|
timezone: session.timezone,
|
|
@@ -812,10 +1059,42 @@ async function runAgent(context, session, prompt, messageId, interruptionGenerat
|
|
|
812
1059
|
}).run(prompt, history, {
|
|
813
1060
|
interrupted: interruptionGeneration !== undefined,
|
|
814
1061
|
userMessage,
|
|
1062
|
+
resumeFromHistory,
|
|
815
1063
|
});
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1064
|
+
let deliveredResult = result;
|
|
1065
|
+
let retryable = false;
|
|
1066
|
+
if (result.status === "error" &&
|
|
1067
|
+
!controller.signal.aborted &&
|
|
1068
|
+
failedModelCheckpoint) {
|
|
1069
|
+
const checkpoint = structuredClone(failedModelCheckpoint);
|
|
1070
|
+
await saveHistory(session, checkpoint, sessionConfig);
|
|
1071
|
+
await saveModelRetry(session, {
|
|
1072
|
+
version: 1,
|
|
1073
|
+
sessionId: session.id,
|
|
1074
|
+
messageId,
|
|
1075
|
+
prompt,
|
|
1076
|
+
failedAt: Date.now(),
|
|
1077
|
+
error: errorMessage(failedModelError ?? result.error ?? "model failed"),
|
|
1078
|
+
historyHash: historyHash(checkpoint),
|
|
1079
|
+
});
|
|
1080
|
+
deliveredResult = { ...result, messages: checkpoint };
|
|
1081
|
+
retryable = true;
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
await clearModelRetry(session);
|
|
1085
|
+
}
|
|
1086
|
+
const metrics = await sessionMetricsAnalyzer(sessionConfig, controller.signal).update({
|
|
1087
|
+
messages: deliveredResult.messages,
|
|
1088
|
+
previous: storedHistory?.metrics,
|
|
1089
|
+
throughTurnId: messageId,
|
|
1090
|
+
turnStatus: deliveredResult.status,
|
|
1091
|
+
offeredToolNamesByCallId,
|
|
1092
|
+
...(!resumeFromHistory ? { currentUserText: prompt } : {}),
|
|
1093
|
+
previousAssistantText: lastAssistantText(history),
|
|
1094
|
+
currentTurnMessages,
|
|
1095
|
+
});
|
|
1096
|
+
await saveHistory(session, deliveredResult.messages, sessionConfig, metrics);
|
|
1097
|
+
if (result.status === "success" || controller.signal.aborted) {
|
|
819
1098
|
if (interruptionGeneration !== undefined &&
|
|
820
1099
|
session.interruptionGeneration === interruptionGeneration) {
|
|
821
1100
|
session.pendingInterruption = false;
|
|
@@ -823,7 +1102,11 @@ async function runAgent(context, session, prompt, messageId, interruptionGenerat
|
|
|
823
1102
|
}
|
|
824
1103
|
session.touchedAt = Date.now();
|
|
825
1104
|
await saveSessionMetadata(session);
|
|
826
|
-
|
|
1105
|
+
emit(session, {
|
|
1106
|
+
type: "data-session-metrics",
|
|
1107
|
+
data: { metrics },
|
|
1108
|
+
});
|
|
1109
|
+
ui.consume(deliveredResult, { retryable });
|
|
827
1110
|
}
|
|
828
1111
|
catch (error) {
|
|
829
1112
|
session.touchedAt = Date.now();
|
|
@@ -916,7 +1199,9 @@ async function triggerToken(context, request, response) {
|
|
|
916
1199
|
async function uploadAttachment(request, response, session) {
|
|
917
1200
|
if (!authorize(request, response, session))
|
|
918
1201
|
return;
|
|
919
|
-
if (session.running
|
|
1202
|
+
if (session.running ||
|
|
1203
|
+
session.preparingTurn ||
|
|
1204
|
+
session.activePrompt !== null) {
|
|
920
1205
|
writeJson(response, 409, {
|
|
921
1206
|
error: "attachments cannot be uploaded while a turn is running",
|
|
922
1207
|
});
|
|
@@ -949,7 +1234,9 @@ async function uploadAttachment(request, response, session) {
|
|
|
949
1234
|
async function appendMessage(context, request, response, session) {
|
|
950
1235
|
if (!authorize(request, response, session))
|
|
951
1236
|
return;
|
|
952
|
-
if (session.running
|
|
1237
|
+
if (session.running ||
|
|
1238
|
+
session.preparingTurn ||
|
|
1239
|
+
session.activePrompt !== null) {
|
|
953
1240
|
writeJson(response, 409, { error: "a turn is already running" });
|
|
954
1241
|
return;
|
|
955
1242
|
}
|
|
@@ -1001,6 +1288,22 @@ async function appendMessage(context, request, response, session) {
|
|
|
1001
1288
|
? attachmentUserMessage(prompt, attachments)
|
|
1002
1289
|
: undefined;
|
|
1003
1290
|
const messageId = String(payload.messageId || message.id || crypto.randomUUID());
|
|
1291
|
+
if (session.running ||
|
|
1292
|
+
session.preparingTurn ||
|
|
1293
|
+
session.activePrompt !== null) {
|
|
1294
|
+
writeJson(response, 409, { error: "a turn is already running" });
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const controller = new AbortController();
|
|
1298
|
+
session.preparingTurn = true;
|
|
1299
|
+
try {
|
|
1300
|
+
await prepareUndoCheckpoint(session, messageId, prompt, attachments);
|
|
1301
|
+
await clearModelRetry(session);
|
|
1302
|
+
session.running = { controller };
|
|
1303
|
+
}
|
|
1304
|
+
finally {
|
|
1305
|
+
session.preparingTurn = false;
|
|
1306
|
+
}
|
|
1004
1307
|
const interruptionGeneration = session.pendingInterruption
|
|
1005
1308
|
? session.interruptionGeneration
|
|
1006
1309
|
: undefined;
|
|
@@ -1019,19 +1322,144 @@ async function appendMessage(context, request, response, session) {
|
|
|
1019
1322
|
partId: request.headers["x-part-id"] || null,
|
|
1020
1323
|
});
|
|
1021
1324
|
queueMicrotask(() => {
|
|
1022
|
-
void runAgent(context, session, prompt, messageId, interruptionGeneration, userMessage);
|
|
1325
|
+
void runAgent(context, session, prompt, messageId, controller, interruptionGeneration, userMessage);
|
|
1023
1326
|
});
|
|
1024
1327
|
}
|
|
1328
|
+
async function retryModelTurn(context, request, response, session) {
|
|
1329
|
+
if (!authorize(request, response, session))
|
|
1330
|
+
return;
|
|
1331
|
+
const body = await readJson(request);
|
|
1332
|
+
if (body.messageId !== undefined && typeof body.messageId !== "string") {
|
|
1333
|
+
writeJson(response, 400, { error: "messageId must be a string" });
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (session.running ||
|
|
1337
|
+
session.preparingTurn ||
|
|
1338
|
+
session.activePrompt !== null) {
|
|
1339
|
+
writeJson(response, 409, { error: "a turn is already running" });
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const controller = new AbortController();
|
|
1343
|
+
session.running = { controller };
|
|
1344
|
+
const releaseReservation = () => {
|
|
1345
|
+
if (session.running?.controller === controller)
|
|
1346
|
+
session.running = null;
|
|
1347
|
+
};
|
|
1348
|
+
try {
|
|
1349
|
+
const currentHistory = (await readHistory(session)) || [];
|
|
1350
|
+
const retry = await modelRetryForHistory(session, currentHistory);
|
|
1351
|
+
if (!retry) {
|
|
1352
|
+
releaseReservation();
|
|
1353
|
+
writeJson(response, 409, {
|
|
1354
|
+
error: "no failed model request is available",
|
|
1355
|
+
});
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (body.messageId && body.messageId !== retry.messageId) {
|
|
1359
|
+
releaseReservation();
|
|
1360
|
+
writeJson(response, 409, {
|
|
1361
|
+
error: "the requested failed turn is no longer retryable",
|
|
1362
|
+
});
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const messageId = crypto.randomUUID();
|
|
1366
|
+
session.turnStartEventId = session.events.at(-1)?.id || "0";
|
|
1367
|
+
session.activePrompt = retry.prompt;
|
|
1368
|
+
session.touchedAt = Date.now();
|
|
1369
|
+
writeJson(response, 202, {
|
|
1370
|
+
accepted: true,
|
|
1371
|
+
sessionId: session.id,
|
|
1372
|
+
messageId,
|
|
1373
|
+
retryOf: retry.messageId,
|
|
1374
|
+
});
|
|
1375
|
+
queueMicrotask(() => {
|
|
1376
|
+
void runAgent(context, session, retry.prompt, messageId, controller, undefined, undefined, true);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
releaseReservation();
|
|
1381
|
+
throw error;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1025
1384
|
async function sessionMessages(context, request, response, session) {
|
|
1026
1385
|
if (!authorize(request, response, session))
|
|
1027
1386
|
return;
|
|
1028
|
-
const
|
|
1387
|
+
const stored = await loadStoredHistoryState(sessionHistoryPath(session));
|
|
1388
|
+
const messages = stored?.messages || [];
|
|
1389
|
+
const turnActive = Boolean(session.running || session.preparingTurn || session.activePrompt !== null);
|
|
1390
|
+
const retry = turnActive
|
|
1391
|
+
? undefined
|
|
1392
|
+
: await modelRetryForHistory(session, messages);
|
|
1029
1393
|
writeJson(response, 200, {
|
|
1030
1394
|
messages,
|
|
1031
1395
|
lastEventId: session.running
|
|
1032
1396
|
? session.turnStartEventId
|
|
1033
1397
|
: session.events.at(-1)?.id || "0",
|
|
1034
1398
|
activePrompt: session.activePrompt,
|
|
1399
|
+
canUndo: session.undoCheckpoint !== null,
|
|
1400
|
+
retryable: Boolean(retry),
|
|
1401
|
+
retryMessageId: retry?.messageId || null,
|
|
1402
|
+
metrics: stored?.metrics || null,
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
async function undoLastTurn(config, request, response, session) {
|
|
1406
|
+
if (!authorize(request, response, session))
|
|
1407
|
+
return;
|
|
1408
|
+
if (session.running || session.preparingTurn || session.activePrompt !== null) {
|
|
1409
|
+
writeJson(response, 409, {
|
|
1410
|
+
error: "the active turn must finish or be cancelled before it can be undone",
|
|
1411
|
+
});
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
const checkpoint = session.undoCheckpoint;
|
|
1415
|
+
if (!checkpoint) {
|
|
1416
|
+
writeJson(response, 200, {
|
|
1417
|
+
undone: false,
|
|
1418
|
+
messages: (await readHistory(session)) || [],
|
|
1419
|
+
lastEventId: session.events.at(-1)?.id || "0",
|
|
1420
|
+
canUndo: false,
|
|
1421
|
+
});
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
await restoreUndoWorkspace(session);
|
|
1425
|
+
await saveHistory(session, checkpoint.messages, config, checkpoint.metrics ?? null);
|
|
1426
|
+
await clearModelRetry(session);
|
|
1427
|
+
session.pendingInterruption = false;
|
|
1428
|
+
session.activePrompt = null;
|
|
1429
|
+
session.touchedAt = Date.now();
|
|
1430
|
+
await saveSessionMetadata(session);
|
|
1431
|
+
let turnStart = -1;
|
|
1432
|
+
for (let index = session.events.length - 1; index >= 0; index -= 1) {
|
|
1433
|
+
if (session.events[index]?.data.type === "start") {
|
|
1434
|
+
turnStart = index;
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (turnStart >= 0)
|
|
1439
|
+
session.events.splice(turnStart);
|
|
1440
|
+
session.turnStartEventId = session.events.at(-1)?.id || "0";
|
|
1441
|
+
const event = emit(session, {
|
|
1442
|
+
type: "data-turn-undone",
|
|
1443
|
+
data: {
|
|
1444
|
+
messageId: checkpoint.messageId,
|
|
1445
|
+
input: checkpoint.input,
|
|
1446
|
+
attachments: checkpoint.attachments,
|
|
1447
|
+
messages: checkpoint.messages,
|
|
1448
|
+
metrics: checkpoint.metrics ?? null,
|
|
1449
|
+
canUndo: false,
|
|
1450
|
+
},
|
|
1451
|
+
});
|
|
1452
|
+
session.undoCheckpoint = null;
|
|
1453
|
+
await rm(sessionUndoRoot(session), { recursive: true, force: true });
|
|
1454
|
+
writeJson(response, 200, {
|
|
1455
|
+
undone: true,
|
|
1456
|
+
messageId: checkpoint.messageId,
|
|
1457
|
+
input: checkpoint.input,
|
|
1458
|
+
attachments: checkpoint.attachments,
|
|
1459
|
+
messages: checkpoint.messages,
|
|
1460
|
+
metrics: checkpoint.metrics ?? null,
|
|
1461
|
+
lastEventId: event.id,
|
|
1462
|
+
canUndo: false,
|
|
1035
1463
|
});
|
|
1036
1464
|
}
|
|
1037
1465
|
function normalizeInputAnswers(pending, submitted) {
|
|
@@ -1208,18 +1636,26 @@ async function safeWorkspaceFile(session, encodedPath) {
|
|
|
1208
1636
|
throw error;
|
|
1209
1637
|
}
|
|
1210
1638
|
}
|
|
1211
|
-
async function serveFile(response, filePath, cache = false, isolated = false) {
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1639
|
+
async function serveFile(response, filePath, cache = false, isolated = false, assetName) {
|
|
1640
|
+
const asset = assetName ? embeddedAsset(assetName) : undefined;
|
|
1641
|
+
let size;
|
|
1642
|
+
if (asset) {
|
|
1643
|
+
size = asset.byteLength;
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
const info = await stat(filePath);
|
|
1647
|
+
if (!info.isFile()) {
|
|
1648
|
+
const error = new Error("not found");
|
|
1649
|
+
error.code = "ENOENT";
|
|
1650
|
+
throw error;
|
|
1651
|
+
}
|
|
1652
|
+
size = info.size;
|
|
1217
1653
|
}
|
|
1218
1654
|
response.writeHead(200, {
|
|
1219
1655
|
...corsHeaders(),
|
|
1220
1656
|
"content-type": mimeTypes[path.extname(filePath).toLowerCase()] ||
|
|
1221
1657
|
"application/octet-stream",
|
|
1222
|
-
"content-length":
|
|
1658
|
+
"content-length": size,
|
|
1223
1659
|
"cache-control": cache ? "public, max-age=300" : "no-cache",
|
|
1224
1660
|
"x-content-type-options": "nosniff",
|
|
1225
1661
|
...(isolated
|
|
@@ -1229,7 +1665,12 @@ async function serveFile(response, filePath, cache = false, isolated = false) {
|
|
|
1229
1665
|
}
|
|
1230
1666
|
: {}),
|
|
1231
1667
|
});
|
|
1232
|
-
|
|
1668
|
+
if (asset) {
|
|
1669
|
+
response.end(asset);
|
|
1670
|
+
}
|
|
1671
|
+
else {
|
|
1672
|
+
fs.createReadStream(filePath).pipe(response);
|
|
1673
|
+
}
|
|
1233
1674
|
}
|
|
1234
1675
|
function sessionRoute(pathname, suffix) {
|
|
1235
1676
|
return pathname.match(new RegExp(`^/ai-proxy/realtime/v1/sessions/([A-Za-z0-9-]{1,128})/${suffix}$`));
|
|
@@ -1251,7 +1692,9 @@ function createRequestHandler(context) {
|
|
|
1251
1692
|
const outputMatch = sessionRoute(url.pathname, "out");
|
|
1252
1693
|
const appendMatch = sessionRoute(url.pathname, "in/append");
|
|
1253
1694
|
const messagesMatch = apiSessionRoute(url.pathname, "messages");
|
|
1695
|
+
const retryMatch = apiSessionRoute(url.pathname, "retry");
|
|
1254
1696
|
const cancelMatch = apiSessionRoute(url.pathname, "cancel");
|
|
1697
|
+
const undoMatch = apiSessionRoute(url.pathname, "undo");
|
|
1255
1698
|
const inputMatch = apiSessionRoute(url.pathname, "input-response");
|
|
1256
1699
|
const uploadMatch = apiSessionRoute(url.pathname, "uploads");
|
|
1257
1700
|
const workspaceMatch = workspaceRoute(url.pathname);
|
|
@@ -1289,9 +1732,15 @@ function createRequestHandler(context) {
|
|
|
1289
1732
|
else if (messagesMatch && request.method === "GET") {
|
|
1290
1733
|
await sessionMessages(context, request, response, await getSession(context, messagesMatch[1] || ""));
|
|
1291
1734
|
}
|
|
1735
|
+
else if (retryMatch && request.method === "POST") {
|
|
1736
|
+
await retryModelTurn(context, request, response, await getSession(context, retryMatch[1] || ""));
|
|
1737
|
+
}
|
|
1292
1738
|
else if (cancelMatch && request.method === "POST") {
|
|
1293
1739
|
await cancelTurn(request, response, await getSession(context, cancelMatch[1] || ""));
|
|
1294
1740
|
}
|
|
1741
|
+
else if (undoMatch && request.method === "POST") {
|
|
1742
|
+
await undoLastTurn(context.config, request, response, await getSession(context, undoMatch[1] || ""));
|
|
1743
|
+
}
|
|
1295
1744
|
else if (inputMatch && request.method === "POST") {
|
|
1296
1745
|
await respondToInput(request, response, await getSession(context, inputMatch[1] || ""));
|
|
1297
1746
|
}
|
|
@@ -1318,16 +1767,16 @@ function createRequestHandler(context) {
|
|
|
1318
1767
|
}
|
|
1319
1768
|
else if (["/", "/index.html"].includes(url.pathname) ||
|
|
1320
1769
|
/^\/agent\/[A-Za-z0-9-]{1,128}$/.test(url.pathname)) {
|
|
1321
|
-
await serveFile(response, path.join(context.webRoot, "index.html"));
|
|
1770
|
+
await serveFile(response, path.join(context.webRoot, "index.html"), false, false, "web/index.html");
|
|
1322
1771
|
}
|
|
1323
1772
|
else if (url.pathname === "/app.js") {
|
|
1324
|
-
await serveFile(response, context.appScript, true);
|
|
1773
|
+
await serveFile(response, context.appScript, true, false, "dist/web/app.js");
|
|
1325
1774
|
}
|
|
1326
1775
|
else if (url.pathname === "/sdk.js") {
|
|
1327
|
-
await serveFile(response, path.join(projectRoot(), "dist", "sdk.js"), true);
|
|
1776
|
+
await serveFile(response, path.join(projectRoot(), "dist", "sdk.js"), true, false, "dist/sdk.js");
|
|
1328
1777
|
}
|
|
1329
1778
|
else if (["/styles.css", "/fonts.css", "/arena-logo.svg"].includes(url.pathname)) {
|
|
1330
|
-
await serveFile(response, path.join(context.webRoot, url.pathname.slice(1)), true);
|
|
1779
|
+
await serveFile(response, path.join(context.webRoot, url.pathname.slice(1)), true, false, `web/${url.pathname.slice(1)}`);
|
|
1331
1780
|
}
|
|
1332
1781
|
else {
|
|
1333
1782
|
writeJson(response, 404, { error: "not found" });
|
|
@@ -1379,6 +1828,7 @@ export async function startServer(options = {}) {
|
|
|
1379
1828
|
const cutoff = Date.now() - 6 * 60 * 60 * 1000;
|
|
1380
1829
|
for (const [sessionId, session] of context.sessions) {
|
|
1381
1830
|
if (!session.running &&
|
|
1831
|
+
!session.preparingTurn &&
|
|
1382
1832
|
!session.subscribers.size &&
|
|
1383
1833
|
session.touchedAt < cutoff) {
|
|
1384
1834
|
context.sessions.delete(sessionId);
|