@mono-agent/agent-runtime 0.14.0 → 0.15.1
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/MIGRATION.md +147 -69
- package/README.md +83 -20
- package/package.json +3 -6
- package/src/agent/compaction.js +0 -11
- package/src/agent/prompt/skill-index.js +5 -1
- package/src/ai/index.js +7 -1
- package/src/ai/observer.js +48 -13
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/providers/claude-cli.js +2 -13
- package/src/ai/providers/claude-sdk.js +14 -8
- package/src/ai/providers/codex-app.js +235 -56
- package/src/ai/providers/opencode-app.js +168 -3
- package/src/ai/providers/pi-messages.js +0 -8
- package/src/ai/providers/pi-native/compaction-driver.js +106 -10
- package/src/ai/providers/pi-native/result-builder.js +2 -14
- package/src/ai/providers/pi-native/stream-subscriber.js +20 -2
- package/src/ai/providers/pi-native/turn-runner.js +31 -8
- package/src/ai/providers/pi-native.js +2 -5
- package/src/ai/runtime/live-input-events.js +94 -0
- package/src/ai/runtime/registry.js +8 -1
- package/src/ai/runtime/router.js +21 -0
- package/src/ai/types.js +2 -1
- package/src/runtime.js +3 -0
- package/types/agent/compaction.d.ts +0 -2
- package/types/agent/prompt/skill-index.d.ts +3 -0
- package/types/ai/index.d.ts +1 -1
- package/types/ai/observer.d.ts +4 -2
- package/types/ai/pi-interop.d.ts +113 -0
- package/types/ai/providers/claude-cli.d.ts +6 -30
- package/types/ai/providers/claude-sdk.d.ts +2 -9
- package/types/ai/providers/codex-app.d.ts +4 -10
- package/types/ai/providers/pi-messages.d.ts +0 -1
- package/types/ai/providers/pi-native/result-builder.d.ts +3 -11
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +4 -2
- package/types/ai/providers/pi-native/turn-runner.d.ts +1 -1
- package/types/ai/runtime/live-input-events.d.ts +22 -0
- package/types/ai/types.d.ts +10 -2
- package/src/ai/backend.js +0 -17
- package/src/ai/registry.js +0 -5
- package/types/ai/backend.d.ts +0 -57
- package/types/ai/registry.d.ts +0 -1
|
@@ -1048,6 +1048,27 @@ function usageFromTokenUsage(tokenUsage) {
|
|
|
1048
1048
|
};
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
+
function contextUsageFromTokenUsage(tokenUsage) {
|
|
1052
|
+
const last = tokenUsage?.last;
|
|
1053
|
+
const total = Number(last?.totalTokens);
|
|
1054
|
+
if (!last || !Number.isFinite(total) || total <= 0) return null;
|
|
1055
|
+
const input = Number(last.inputTokens) || 0;
|
|
1056
|
+
const cachedInput = Number(last.cachedInputTokens) || 0;
|
|
1057
|
+
const output = Number(last.outputTokens) || 0;
|
|
1058
|
+
const reasoning = Number(last.reasoningOutputTokens) || 0;
|
|
1059
|
+
const contextWindow = Number(tokenUsage?.modelContextWindow) || 0;
|
|
1060
|
+
return {
|
|
1061
|
+
tokens: {
|
|
1062
|
+
input: Math.max(0, input - cachedInput),
|
|
1063
|
+
cachedInput,
|
|
1064
|
+
output,
|
|
1065
|
+
reasoning,
|
|
1066
|
+
total,
|
|
1067
|
+
},
|
|
1068
|
+
...(contextWindow > 0 ? { contextWindow } : {}),
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1051
1072
|
const noopNotificationHandler = () => {};
|
|
1052
1073
|
|
|
1053
1074
|
async function closeCodexClient(client) {
|
|
@@ -1076,6 +1097,7 @@ const codexLiveness = createSessionLiveness(codexSessions);
|
|
|
1076
1097
|
export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
1077
1098
|
const start = Date.now();
|
|
1078
1099
|
const resolved = options.model;
|
|
1100
|
+
const requestedReference = resolved?.reference || `codex:${resolved?.model || ""}`;
|
|
1079
1101
|
// Resolve every credential-bearing value before the app-server client is
|
|
1080
1102
|
// constructed. The same set protects transport errors and provider events,
|
|
1081
1103
|
// including MCP servers whose custom env/header names are not recognizable
|
|
@@ -1105,8 +1127,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1105
1127
|
const events = [];
|
|
1106
1128
|
const texts = [];
|
|
1107
1129
|
const agentTextByItem = new Map();
|
|
1130
|
+
const compactionStatuses = new Map();
|
|
1131
|
+
const activeCompactions = new Map();
|
|
1132
|
+
const nativeCompactionTurnKeys = new Set();
|
|
1133
|
+
const legacyCompactionTurnKeys = new Set();
|
|
1108
1134
|
let threadId = null;
|
|
1109
1135
|
let activeTurnId = null;
|
|
1136
|
+
let actualModel = resolved?.model || null;
|
|
1110
1137
|
let turnCompleted = false;
|
|
1111
1138
|
let errorMessage = null;
|
|
1112
1139
|
let failureKind = null;
|
|
@@ -1116,7 +1143,9 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1116
1143
|
let serverRequestViolation = null;
|
|
1117
1144
|
let resolveTurn;
|
|
1118
1145
|
let resolveTurnReady;
|
|
1146
|
+
let resolveLiveInputStop;
|
|
1119
1147
|
let turnReadyResolved = false;
|
|
1148
|
+
let liveInputStopped = false;
|
|
1120
1149
|
const fileChangeSnapshots = new Map();
|
|
1121
1150
|
const codexItemContext = {
|
|
1122
1151
|
fileChangePayload: (raw) => createFileChangePayload(raw, {
|
|
@@ -1126,6 +1155,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1126
1155
|
};
|
|
1127
1156
|
const turnDone = new Promise((resolve) => { resolveTurn = resolve; });
|
|
1128
1157
|
const turnReady = new Promise((resolve) => { resolveTurnReady = resolve; });
|
|
1158
|
+
const liveInputStop = new Promise((resolve) => { resolveLiveInputStop = resolve; });
|
|
1159
|
+
|
|
1160
|
+
function stopLiveInput() {
|
|
1161
|
+
if (liveInputStopped) return;
|
|
1162
|
+
liveInputStopped = true;
|
|
1163
|
+
resolveLiveInputStop();
|
|
1164
|
+
}
|
|
1129
1165
|
|
|
1130
1166
|
function setActiveTurnId(turnId, { steerReady = false } = {}) {
|
|
1131
1167
|
activeTurnId = turnId || activeTurnId;
|
|
@@ -1148,6 +1184,92 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1148
1184
|
options.onEvent?.(safeEvent);
|
|
1149
1185
|
}
|
|
1150
1186
|
|
|
1187
|
+
function invokeLiveInputCallback(message, callbackName, ...args) {
|
|
1188
|
+
const callback = message?.[callbackName];
|
|
1189
|
+
if (typeof callback !== "function") return;
|
|
1190
|
+
try {
|
|
1191
|
+
callback.apply(message, args);
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
const detail = safeDiagnostic(err, 512);
|
|
1194
|
+
emitEvent({
|
|
1195
|
+
type: "runtime_warning",
|
|
1196
|
+
warning_kind: "live_input_callback_failed",
|
|
1197
|
+
message: safeDiagnostic(`Live-input ${callbackName} callback failed: ${detail}`, 1_024),
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const compactionTurnKey = (params = {}) => `${params.threadId || threadId || "thread"}:${params.turnId || activeTurnId || "turn"}`;
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* @param {{operationId: string, status: string, turnKey?: string, reason?: string, message?: string}} event
|
|
1206
|
+
*/
|
|
1207
|
+
function emitCompaction({
|
|
1208
|
+
operationId,
|
|
1209
|
+
status,
|
|
1210
|
+
turnKey,
|
|
1211
|
+
reason,
|
|
1212
|
+
message,
|
|
1213
|
+
}) {
|
|
1214
|
+
const previous = compactionStatuses.get(operationId);
|
|
1215
|
+
if (previous === status || previous === "succeeded" || previous === "failed" || previous === "skipped") return;
|
|
1216
|
+
compactionStatuses.set(operationId, status);
|
|
1217
|
+
if (status === "running") activeCompactions.set(operationId, { turnKey: turnKey || compactionTurnKey() });
|
|
1218
|
+
else activeCompactions.delete(operationId);
|
|
1219
|
+
emitEvent({
|
|
1220
|
+
type: "context_compaction",
|
|
1221
|
+
operationId,
|
|
1222
|
+
status,
|
|
1223
|
+
sdk: "codex",
|
|
1224
|
+
trigger: "automatic",
|
|
1225
|
+
timestamp: Date.now(),
|
|
1226
|
+
model: actualModel ? `codex:${actualModel}` : requestedReference,
|
|
1227
|
+
...(reason ? { reason } : {}),
|
|
1228
|
+
...(message ? { message } : {}),
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function finalizeOpenCompactions(reason, message) {
|
|
1233
|
+
for (const [operationId, active] of [...activeCompactions]) {
|
|
1234
|
+
emitCompaction({
|
|
1235
|
+
operationId,
|
|
1236
|
+
status: "failed",
|
|
1237
|
+
turnKey: active.turnKey,
|
|
1238
|
+
reason,
|
|
1239
|
+
message,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function handleContextCompactionItem(method, params) {
|
|
1245
|
+
const item = params.item;
|
|
1246
|
+
const turnKey = compactionTurnKey(params);
|
|
1247
|
+
nativeCompactionTurnKeys.add(turnKey);
|
|
1248
|
+
if (legacyCompactionTurnKeys.has(turnKey)) return;
|
|
1249
|
+
const operationId = `codex:${item.id}`;
|
|
1250
|
+
emitCompaction({
|
|
1251
|
+
operationId,
|
|
1252
|
+
status: method === "item/started" ? "running" : "succeeded",
|
|
1253
|
+
turnKey,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function handleLegacyCompaction(params) {
|
|
1258
|
+
const turnKey = compactionTurnKey(params);
|
|
1259
|
+
const active = [...activeCompactions].find(([, value]) => value.turnKey === turnKey);
|
|
1260
|
+
if (active) {
|
|
1261
|
+
emitCompaction({ operationId: active[0], status: "succeeded", turnKey });
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (nativeCompactionTurnKeys.has(turnKey) || legacyCompactionTurnKeys.has(turnKey)) return;
|
|
1265
|
+
legacyCompactionTurnKeys.add(turnKey);
|
|
1266
|
+
emitCompaction({
|
|
1267
|
+
operationId: `codex:${turnKey}:legacy`,
|
|
1268
|
+
status: "succeeded",
|
|
1269
|
+
turnKey,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1151
1273
|
function handleAgentText(text) {
|
|
1152
1274
|
const safeText = redactCodexDiagnostic(text, sensitiveValues);
|
|
1153
1275
|
pushUniqueText(texts, safeText);
|
|
@@ -1171,6 +1293,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1171
1293
|
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
1172
1294
|
}
|
|
1173
1295
|
turnCompleted = true;
|
|
1296
|
+
stopLiveInput();
|
|
1174
1297
|
resolveTurn({ id: activeTurnId, status: "interrupted" });
|
|
1175
1298
|
}
|
|
1176
1299
|
|
|
@@ -1195,6 +1318,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1195
1318
|
message: errorMessage,
|
|
1196
1319
|
});
|
|
1197
1320
|
turnCompleted = true;
|
|
1321
|
+
stopLiveInput();
|
|
1198
1322
|
resolveTurn({ id: activeTurnId, status: "interrupted" });
|
|
1199
1323
|
}
|
|
1200
1324
|
|
|
@@ -1225,10 +1349,18 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1225
1349
|
if (method === "turn/completed") {
|
|
1226
1350
|
setActiveTurnId(params.turn?.id);
|
|
1227
1351
|
turnCompleted = true;
|
|
1352
|
+
stopLiveInput();
|
|
1228
1353
|
if (params.turn?.status === "failed") {
|
|
1229
1354
|
errorMessage = safeDiagnostic(params.turn?.error?.message || params.turn?.error || "Codex turn failed");
|
|
1230
1355
|
failureKind = "provider_unavailable";
|
|
1231
1356
|
}
|
|
1357
|
+
if (activeCompactions.size > 0) {
|
|
1358
|
+
const cancelled = params.turn?.status === "cancelled" || params.turn?.status === "interrupted";
|
|
1359
|
+
finalizeOpenCompactions(
|
|
1360
|
+
cancelled ? "cancelled" : "incomplete",
|
|
1361
|
+
cancelled ? "Compaction was interrupted." : "Compaction ended without a completion event.",
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1232
1364
|
const safeTurn = params.turn?.error === undefined
|
|
1233
1365
|
? params.turn
|
|
1234
1366
|
: { ...params.turn, error: safeResponseError(params.turn.error) };
|
|
@@ -1238,6 +1370,27 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1238
1370
|
}
|
|
1239
1371
|
if (method === "thread/tokenUsage/updated") {
|
|
1240
1372
|
usage = usageFromTokenUsage(params.tokenUsage);
|
|
1373
|
+
const contextUsage = contextUsageFromTokenUsage(params.tokenUsage);
|
|
1374
|
+
if (contextUsage) {
|
|
1375
|
+
emitEvent({
|
|
1376
|
+
type: "context_usage",
|
|
1377
|
+
sdk: "codex",
|
|
1378
|
+
model: actualModel ? `codex:${actualModel}` : requestedReference,
|
|
1379
|
+
timestamp: Date.now(),
|
|
1380
|
+
...(typeof params.turnId === "string" && params.turnId.length > 0
|
|
1381
|
+
? { measurementId: params.turnId }
|
|
1382
|
+
: {}),
|
|
1383
|
+
...contextUsage,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (method === "model/rerouted") {
|
|
1389
|
+
if (typeof params.toModel === "string" && params.toModel.trim().length > 0) actualModel = params.toModel;
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (method === "thread/compacted") {
|
|
1393
|
+
handleLegacyCompaction(params);
|
|
1241
1394
|
return;
|
|
1242
1395
|
}
|
|
1243
1396
|
if (method === "item/agentMessage/delta") {
|
|
@@ -1258,6 +1411,10 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1258
1411
|
return;
|
|
1259
1412
|
}
|
|
1260
1413
|
if (method === "item/started" || method === "item/completed") {
|
|
1414
|
+
if (params.item?.type === "contextCompaction") {
|
|
1415
|
+
handleContextCompactionItem(method, params);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1261
1418
|
const raw = mapThreadItem(method, params.item);
|
|
1262
1419
|
if (params.item?.type === "agentMessage") {
|
|
1263
1420
|
const text = params.item.text || agentTextByItem.get(params.item.id) || "";
|
|
@@ -1352,6 +1509,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1352
1509
|
|
|
1353
1510
|
const abortHandler = () => {
|
|
1354
1511
|
abortRequested = true;
|
|
1512
|
+
stopLiveInput();
|
|
1355
1513
|
if (threadId && activeTurnId && !interruptSent) {
|
|
1356
1514
|
interruptSent = true;
|
|
1357
1515
|
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
@@ -1363,60 +1521,77 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1363
1521
|
|
|
1364
1522
|
async function steerLiveInput() {
|
|
1365
1523
|
if (!options.liveInput) return;
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
await Promise.race([
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
client.closed.then((err) => { throw err; }),
|
|
1524
|
+
const iterator = options.liveInput[Symbol.asyncIterator]();
|
|
1525
|
+
try {
|
|
1526
|
+
while (!turnCompleted && !liveInputStopped) {
|
|
1527
|
+
const next = await Promise.race([
|
|
1528
|
+
iterator.next(),
|
|
1529
|
+
liveInputStop.then(() => ({ done: true, value: undefined })),
|
|
1373
1530
|
]);
|
|
1374
|
-
if (turnCompleted ||
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
try {
|
|
1378
|
-
const response = await client.request("turn/steer", {
|
|
1379
|
-
threadId,
|
|
1380
|
-
expectedTurnId: activeTurnId,
|
|
1381
|
-
input,
|
|
1382
|
-
});
|
|
1383
|
-
activeTurnId = response?.turnId || activeTurnId;
|
|
1384
|
-
} catch (err) {
|
|
1385
|
-
const providerError = err?.responseError;
|
|
1386
|
-
if (isNoActiveTurnToSteer(providerError || err)) {
|
|
1531
|
+
if (next.done || turnCompleted || liveInputStopped) break;
|
|
1532
|
+
const message = next.value;
|
|
1533
|
+
if (!threadId || !activeTurnId || !turnReadyResolved) {
|
|
1387
1534
|
await Promise.race([
|
|
1388
1535
|
turnReady,
|
|
1389
|
-
|
|
1390
|
-
client.closed.then((closedErr) => { throw closedErr; }),
|
|
1536
|
+
liveInputStop,
|
|
1391
1537
|
]);
|
|
1392
|
-
if (turnCompleted) break;
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1538
|
+
if (turnCompleted || liveInputStopped || !turnReadyResolved) break;
|
|
1539
|
+
}
|
|
1540
|
+
const input = userTextInput(formatLiveInputGuidance(message.body, options.prompts));
|
|
1541
|
+
try {
|
|
1542
|
+
const response = await client.request("turn/steer", {
|
|
1543
|
+
threadId,
|
|
1544
|
+
expectedTurnId: activeTurnId,
|
|
1545
|
+
input,
|
|
1546
|
+
});
|
|
1547
|
+
activeTurnId = response?.turnId || activeTurnId;
|
|
1548
|
+
invokeLiveInputCallback(message, "acknowledge");
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
const providerError = err?.responseError;
|
|
1551
|
+
if (isNoActiveTurnToSteer(providerError || err)) {
|
|
1552
|
+
await Promise.race([
|
|
1553
|
+
turnReady,
|
|
1554
|
+
liveInputStop,
|
|
1555
|
+
]);
|
|
1556
|
+
if (turnCompleted || liveInputStopped) break;
|
|
1557
|
+
try {
|
|
1558
|
+
const response = await client.request("turn/steer", {
|
|
1559
|
+
threadId,
|
|
1560
|
+
expectedTurnId: activeTurnId,
|
|
1561
|
+
input,
|
|
1562
|
+
});
|
|
1563
|
+
activeTurnId = response?.turnId || activeTurnId;
|
|
1564
|
+
invokeLiveInputCallback(message, "acknowledge");
|
|
1565
|
+
continue;
|
|
1566
|
+
} catch (retryErr) {
|
|
1567
|
+
invokeLiveInputCallback(message, "reject", retryErr);
|
|
1568
|
+
const retryProviderError = retryErr?.responseError
|
|
1569
|
+
? safeResponseError(retryErr.responseError)
|
|
1570
|
+
: null;
|
|
1571
|
+
emitEvent({
|
|
1572
|
+
type: "runtime_warning",
|
|
1573
|
+
warning_kind: isActiveTurnNotSteerable(retryProviderError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
1574
|
+
message: safeDiagnostic(codexErrorMessage(retryProviderError || retryErr)),
|
|
1575
|
+
});
|
|
1576
|
+
// Preserve FIFO fallback: once one message is rejected, later
|
|
1577
|
+
// entries must not overtake it inside this provider attempt.
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1411
1580
|
}
|
|
1581
|
+
invokeLiveInputCallback(message, "reject", err);
|
|
1582
|
+
emitEvent({
|
|
1583
|
+
type: "runtime_warning",
|
|
1584
|
+
warning_kind: isActiveTurnNotSteerable(providerError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
1585
|
+
message: safeDiagnostic(codexErrorMessage(
|
|
1586
|
+
providerError ? safeResponseError(providerError) : err,
|
|
1587
|
+
)),
|
|
1588
|
+
});
|
|
1589
|
+
break;
|
|
1412
1590
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
providerError ? safeResponseError(providerError) : err,
|
|
1418
|
-
)),
|
|
1419
|
-
});
|
|
1591
|
+
}
|
|
1592
|
+
} finally {
|
|
1593
|
+
if (typeof iterator.return === "function") {
|
|
1594
|
+
try { void Promise.resolve(iterator.return()).catch(() => {}); } catch { /* best-effort */ }
|
|
1420
1595
|
}
|
|
1421
1596
|
}
|
|
1422
1597
|
}
|
|
@@ -1640,6 +1815,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1640
1815
|
closedSignal.then((err) => {
|
|
1641
1816
|
if (!turnCompleted) {
|
|
1642
1817
|
prematureClose = true;
|
|
1818
|
+
stopLiveInput();
|
|
1643
1819
|
throw err || new Error("codex app-server closed");
|
|
1644
1820
|
}
|
|
1645
1821
|
return null;
|
|
@@ -1654,9 +1830,10 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1654
1830
|
}
|
|
1655
1831
|
} finally {
|
|
1656
1832
|
abortRaceCleanup();
|
|
1833
|
+
stopLiveInput();
|
|
1657
1834
|
}
|
|
1658
1835
|
turnCompleted = true;
|
|
1659
|
-
await
|
|
1836
|
+
await steerTask;
|
|
1660
1837
|
|
|
1661
1838
|
const text = texts[texts.length - 1] || "";
|
|
1662
1839
|
let codexErrorCode = prematureClose ? "codex_app_server_closed" : null;
|
|
@@ -1683,7 +1860,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1683
1860
|
});
|
|
1684
1861
|
}
|
|
1685
1862
|
const hadPartialProgress = events.length > 0 || texts.length > 0;
|
|
1686
|
-
const reference =
|
|
1863
|
+
const reference = requestedReference;
|
|
1687
1864
|
const inputTokens = usage?.input_tokens ?? usage?.inputTokens ?? 0;
|
|
1688
1865
|
const outputTokens = usage?.output_tokens ?? usage?.outputTokens ?? 0;
|
|
1689
1866
|
const cachedTokens = usage?.cache_read_tokens ?? usage?.cachedInputTokens ?? 0;
|
|
@@ -1774,6 +1951,14 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1774
1951
|
}),
|
|
1775
1952
|
};
|
|
1776
1953
|
} finally {
|
|
1954
|
+
stopLiveInput();
|
|
1955
|
+
if (activeCompactions.size > 0) {
|
|
1956
|
+
const cancelled = !!options.abortSignal?.aborted;
|
|
1957
|
+
finalizeOpenCompactions(
|
|
1958
|
+
cancelled ? "cancelled" : "incomplete",
|
|
1959
|
+
cancelled ? "Compaction was interrupted." : "Compaction ended without a completion event.",
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1777
1962
|
options.abortSignal?.removeEventListener?.("abort", abortHandler);
|
|
1778
1963
|
if (resumeEntry) {
|
|
1779
1964
|
resumeEntry.busy = false;
|
|
@@ -1784,12 +1969,6 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1784
1969
|
}
|
|
1785
1970
|
}
|
|
1786
1971
|
|
|
1787
|
-
export const codexAppBackend = {
|
|
1788
|
-
kind: "codex-app",
|
|
1789
|
-
capabilities: CODEX_APP_CAPABILITIES,
|
|
1790
|
-
execute: generateCodexAppResponse,
|
|
1791
|
-
};
|
|
1792
|
-
|
|
1793
1972
|
// CLI bridge for sdk='codex' agents that opt into execution_mode='cli'. The
|
|
1794
1973
|
// codex `app-server` is more capable than `codex exec` (better event
|
|
1795
1974
|
// streaming, MCP support), so this is the default CLI path for Codex.
|
|
@@ -319,6 +319,57 @@ function usageFromInfo(info) {
|
|
|
319
319
|
};
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
async function opencodeContextWindows(client, directoryParams) {
|
|
323
|
+
try {
|
|
324
|
+
if (typeof client?.provider?.list !== "function") return new Map();
|
|
325
|
+
const listed = unwrap(await client.provider.list(directoryParams));
|
|
326
|
+
const providers = Array.isArray(listed?.all) ? listed.all : [];
|
|
327
|
+
const windows = new Map();
|
|
328
|
+
for (const provider of providers) {
|
|
329
|
+
const models = provider?.models && typeof provider.models === "object"
|
|
330
|
+
? Object.values(provider.models)
|
|
331
|
+
: [];
|
|
332
|
+
for (const model of models) {
|
|
333
|
+
const providerID = model?.providerID || provider?.id;
|
|
334
|
+
const modelID = model?.id;
|
|
335
|
+
const contextWindow = Number(model?.limit?.context) || 0;
|
|
336
|
+
if (providerID && modelID && contextWindow > 0) {
|
|
337
|
+
windows.set(`${providerID}:${modelID}`, contextWindow);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return windows;
|
|
342
|
+
} catch {
|
|
343
|
+
return new Map();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function contextUsageFromInfo(info, contextWindows, fallbackProviderID, fallbackModelID) {
|
|
348
|
+
if (info?.role !== "assistant" || info.error) return null;
|
|
349
|
+
const total = num(info?.tokens?.total);
|
|
350
|
+
if (total === null || total <= 0) return null;
|
|
351
|
+
const providerID = info.providerID || fallbackProviderID;
|
|
352
|
+
const modelID = info.modelID || fallbackModelID;
|
|
353
|
+
const input = num(info.tokens?.input) || 0;
|
|
354
|
+
const output = num(info.tokens?.output) || 0;
|
|
355
|
+
const reasoning = num(info.tokens?.reasoning) || 0;
|
|
356
|
+
const cachedInput = num(info.tokens?.cache?.read) || 0;
|
|
357
|
+
const cacheCreation = num(info.tokens?.cache?.write) || 0;
|
|
358
|
+
const contextWindow = contextWindows.get(`${providerID}:${modelID}`);
|
|
359
|
+
return {
|
|
360
|
+
model: `opencode:${providerID}:${modelID}`,
|
|
361
|
+
tokens: {
|
|
362
|
+
input: Math.max(0, input - cachedInput),
|
|
363
|
+
cachedInput,
|
|
364
|
+
cacheCreation,
|
|
365
|
+
output,
|
|
366
|
+
reasoning,
|
|
367
|
+
total,
|
|
368
|
+
},
|
|
369
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
322
373
|
function aggregateAssistantInfos(infos) {
|
|
323
374
|
const entries = [...infos];
|
|
324
375
|
const totals = {
|
|
@@ -587,11 +638,71 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
587
638
|
const textDeltaPartIds = new Set();
|
|
588
639
|
const reasoningDeltaPartIds = new Set();
|
|
589
640
|
const assistantInfos = new Map();
|
|
590
|
-
const
|
|
641
|
+
const contextUsageSignatures = new Map();
|
|
642
|
+
const compactionStatuses = new Map();
|
|
643
|
+
const activeCompactions = new Map();
|
|
644
|
+
const seenLegacyCompactionIds = new Set();
|
|
645
|
+
let nativeCompactionSeen = false;
|
|
646
|
+
let legacyCompactionMode = false;
|
|
647
|
+
let contextWindows = new Map();
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* @param {{operationId: string, status: string, trigger?: string, timestamp?: number, reason?: string, message?: string}} event
|
|
651
|
+
*/
|
|
652
|
+
const emitCompaction = ({ operationId, status, trigger = "automatic", timestamp, reason, message }) => {
|
|
653
|
+
const previous = compactionStatuses.get(operationId);
|
|
654
|
+
if (previous === status || previous === "succeeded" || previous === "failed" || previous === "skipped") return;
|
|
655
|
+
compactionStatuses.set(operationId, status);
|
|
656
|
+
if (status === "running") activeCompactions.set(operationId, { trigger });
|
|
657
|
+
else activeCompactions.delete(operationId);
|
|
658
|
+
emit({
|
|
659
|
+
type: "context_compaction",
|
|
660
|
+
operationId,
|
|
661
|
+
status,
|
|
662
|
+
sdk: "opencode",
|
|
663
|
+
trigger,
|
|
664
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : Date.now(),
|
|
665
|
+
model: reference,
|
|
666
|
+
...(reason ? { reason } : {}),
|
|
667
|
+
...(message ? { message } : {}),
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const finalizeOpenCompactions = (reason, message) => {
|
|
672
|
+
for (const [operationId, active] of [...activeCompactions]) {
|
|
673
|
+
emitCompaction({
|
|
674
|
+
operationId,
|
|
675
|
+
status: "failed",
|
|
676
|
+
trigger: active.trigger,
|
|
677
|
+
reason,
|
|
678
|
+
message,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const recordAssistantInfo = (info, fallbackKey, { terminal = false } = {}) => {
|
|
591
684
|
if (info?.role !== "assistant") return;
|
|
592
685
|
const key = typeof info.id === "string" && info.id.length > 0 ? info.id : fallbackKey;
|
|
593
686
|
assistantInfos.set(key, info);
|
|
594
687
|
usage = aggregateAssistantInfos(assistantInfos.values()).usage;
|
|
688
|
+
const completed = terminal
|
|
689
|
+
|| Number.isFinite(info?.time?.completed)
|
|
690
|
+
|| (typeof info?.finish === "string" && info.finish.length > 0);
|
|
691
|
+
if (!completed) return;
|
|
692
|
+
const contextUsage = contextUsageFromInfo(info, contextWindows, providerID, modelID);
|
|
693
|
+
if (!contextUsage) return;
|
|
694
|
+
const signature = JSON.stringify([contextUsage.model, contextUsage.contextWindow, contextUsage.tokens]);
|
|
695
|
+
if (contextUsageSignatures.get(key) === signature) return;
|
|
696
|
+
contextUsageSignatures.set(key, signature);
|
|
697
|
+
emit({
|
|
698
|
+
type: "context_usage",
|
|
699
|
+
sdk: "opencode",
|
|
700
|
+
model: contextUsage.model,
|
|
701
|
+
timestamp: Number.isFinite(info?.time?.completed) ? info.time.completed : Date.now(),
|
|
702
|
+
measurementId: key,
|
|
703
|
+
...(contextUsage.contextWindow === undefined ? {} : { contextWindow: contextUsage.contextWindow }),
|
|
704
|
+
tokens: contextUsage.tokens,
|
|
705
|
+
});
|
|
595
706
|
};
|
|
596
707
|
|
|
597
708
|
const abortHandler = () => {
|
|
@@ -618,6 +729,7 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
618
729
|
client = opencode.client;
|
|
619
730
|
server = opencode.server;
|
|
620
731
|
options.abortSignal?.addEventListener?.("abort", abortHandler, { once: true });
|
|
732
|
+
contextWindows = await opencodeContextWindows(client, directoryParams);
|
|
621
733
|
|
|
622
734
|
if (!sessionId) {
|
|
623
735
|
const created = unwrap(await client.session.create(directoryParams));
|
|
@@ -758,6 +870,47 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
758
870
|
recordAssistantInfo(info, "event-anonymous");
|
|
759
871
|
return;
|
|
760
872
|
}
|
|
873
|
+
case "session.next.compaction.started": {
|
|
874
|
+
if (props.sessionID && props.sessionID !== sessionId) return;
|
|
875
|
+
nativeCompactionSeen = true;
|
|
876
|
+
if (legacyCompactionMode) return;
|
|
877
|
+
const operationId = `opencode:${sessionId}:${props.messageID || event.id || "compaction"}`;
|
|
878
|
+
emitCompaction({
|
|
879
|
+
operationId,
|
|
880
|
+
status: "running",
|
|
881
|
+
trigger: props.reason === "manual" ? "manual" : "automatic",
|
|
882
|
+
timestamp: props.timestamp,
|
|
883
|
+
});
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
case "session.next.compaction.ended": {
|
|
887
|
+
if (props.sessionID && props.sessionID !== sessionId) return;
|
|
888
|
+
nativeCompactionSeen = true;
|
|
889
|
+
if (legacyCompactionMode) return;
|
|
890
|
+
const operationId = `opencode:${sessionId}:${props.messageID || event.id || "compaction"}`;
|
|
891
|
+
emitCompaction({
|
|
892
|
+
operationId,
|
|
893
|
+
status: "succeeded",
|
|
894
|
+
trigger: props.reason === "manual" ? "manual" : "automatic",
|
|
895
|
+
timestamp: props.timestamp,
|
|
896
|
+
});
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
case "session.compacted": {
|
|
900
|
+
if (props.sessionID && props.sessionID !== sessionId) return;
|
|
901
|
+
if (nativeCompactionSeen) return;
|
|
902
|
+
legacyCompactionMode = true;
|
|
903
|
+
const legacyId = typeof event.id === "string" && event.id.length > 0
|
|
904
|
+
? event.id
|
|
905
|
+
: randomUUID();
|
|
906
|
+
if (seenLegacyCompactionIds.has(legacyId)) return;
|
|
907
|
+
seenLegacyCompactionIds.add(legacyId);
|
|
908
|
+
emitCompaction({
|
|
909
|
+
operationId: `opencode:${sessionId}:legacy:${legacyId}`,
|
|
910
|
+
status: "succeeded",
|
|
911
|
+
});
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
761
914
|
case "permission.asked":
|
|
762
915
|
await respondToPermission(props);
|
|
763
916
|
return;
|
|
@@ -765,10 +918,14 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
765
918
|
if (props.sessionID && props.sessionID !== sessionId) return;
|
|
766
919
|
errorMessage = safeOpenCodeErrorMessage(props.error, "OpenCode session error.");
|
|
767
920
|
failureKind = mapErrorFailureKind(props.error);
|
|
921
|
+
finalizeOpenCompactions("provider_error", "Compaction was interrupted by a provider error.");
|
|
768
922
|
pumpDone = true;
|
|
769
923
|
return;
|
|
770
924
|
case "session.idle":
|
|
771
|
-
if (props.sessionID === sessionId)
|
|
925
|
+
if (props.sessionID === sessionId) {
|
|
926
|
+
finalizeOpenCompactions("incomplete", "Compaction ended without a completion event.");
|
|
927
|
+
pumpDone = true;
|
|
928
|
+
}
|
|
772
929
|
return;
|
|
773
930
|
default:
|
|
774
931
|
return;
|
|
@@ -815,7 +972,7 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
815
972
|
}
|
|
816
973
|
|
|
817
974
|
const info = promptResult?.info || {};
|
|
818
|
-
recordAssistantInfo(info, "prompt-final");
|
|
975
|
+
recordAssistantInfo(info, "prompt-final", { terminal: true });
|
|
819
976
|
if (info.error && !errorMessage) {
|
|
820
977
|
errorMessage = safeOpenCodeErrorMessage(info.error, "OpenCode turn failed.");
|
|
821
978
|
failureKind = mapErrorFailureKind(info.error);
|
|
@@ -882,6 +1039,10 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
882
1039
|
}),
|
|
883
1040
|
};
|
|
884
1041
|
} catch (err) {
|
|
1042
|
+
finalizeOpenCompactions(
|
|
1043
|
+
options.abortSignal?.aborted ? "cancelled" : "provider_error",
|
|
1044
|
+
options.abortSignal?.aborted ? "Compaction was interrupted." : "Compaction was interrupted by a provider error.",
|
|
1045
|
+
);
|
|
885
1046
|
const partialAggregate = aggregateAssistantInfos(assistantInfos.values());
|
|
886
1047
|
const partialUsage = partialAggregate.usage ?? usage;
|
|
887
1048
|
const partialCost = partialAggregate.reportedCost !== null
|
|
@@ -929,6 +1090,10 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
929
1090
|
}),
|
|
930
1091
|
};
|
|
931
1092
|
} finally {
|
|
1093
|
+
finalizeOpenCompactions(
|
|
1094
|
+
options.abortSignal?.aborted ? "cancelled" : "incomplete",
|
|
1095
|
+
options.abortSignal?.aborted ? "Compaction was interrupted." : "Compaction ended without a completion event.",
|
|
1096
|
+
);
|
|
932
1097
|
options.abortSignal?.removeEventListener?.("abort", abortHandler);
|
|
933
1098
|
try { await server?.close?.(); } catch { /* best effort */ }
|
|
934
1099
|
}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { EMPTY_USAGE } from "./pi-models.js";
|
|
2
2
|
|
|
3
|
-
export function promptTextFromMessages(messages) {
|
|
4
|
-
if (!Array.isArray(messages) || !messages.length) return "";
|
|
5
|
-
return messages
|
|
6
|
-
.filter((message) => message?.role === "user")
|
|
7
|
-
.map((message) => typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? ""))
|
|
8
|
-
.join("\n");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
3
|
function messageContent(value) {
|
|
12
4
|
if (typeof value === "string") return value;
|
|
13
5
|
if (Array.isArray(value)) {
|