@mono-agent/agent-runtime 0.13.0 → 0.15.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/ARCHITECTURE.md +62 -20
- package/MIGRATION.md +44 -36
- package/README.md +181 -114
- package/package.json +3 -6
- package/src/agent/approval.js +4 -2
- package/src/ai/index.js +0 -1
- package/src/ai/observer.js +48 -13
- package/src/ai/providers/claude-cli.js +2 -13
- package/src/ai/providers/claude-sdk.js +12 -7
- package/src/ai/providers/codex-app.js +205 -55
- 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/model-refs.js +1 -1
- package/src/ai/runtime/registry.js +8 -1
- package/src/ai/types.js +1 -1
- package/src/runtime.js +4 -4
- package/types/ai/index.d.ts +0 -1
- package/types/ai/observer.d.ts +4 -2
- 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/types.d.ts +9 -3
- package/src/ai/backend.js +0 -17
- package/src/ai/registry.js +0 -5
|
@@ -83,7 +83,8 @@ const DORMANT_CLI_CAPABILITIES = {
|
|
|
83
83
|
supports_mcp: true,
|
|
84
84
|
supports_skills: true,
|
|
85
85
|
supports_builtin_tools: true,
|
|
86
|
-
|
|
86
|
+
// The one-shot CLI bridge cannot add stdin messages after process launch.
|
|
87
|
+
supports_live_input: false,
|
|
87
88
|
supports_native_subagents: true,
|
|
88
89
|
};
|
|
89
90
|
|
|
@@ -776,18 +777,6 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
776
777
|
}
|
|
777
778
|
}
|
|
778
779
|
|
|
779
|
-
export const claudeCodeBackend = {
|
|
780
|
-
kind: "claude-code",
|
|
781
|
-
capabilities: { kind: "claude-code", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
|
|
782
|
-
execute: generateCliResponse,
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
export const codexCliBackend = {
|
|
786
|
-
kind: "codex-cli",
|
|
787
|
-
capabilities: { kind: "codex-cli", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
|
|
788
|
-
execute: generateCliResponse,
|
|
789
|
-
};
|
|
790
|
-
|
|
791
780
|
// CLI bridge for sdk='claude' agents that opt into execution_mode='cli'.
|
|
792
781
|
// generateCliResponse internally branches on resolved.sdk; the SDK shape
|
|
793
782
|
// from parseModelReference uses 'claude', the CLI builder expects
|
|
@@ -563,7 +563,18 @@ function createClaudeCanUseTool(approvalManager, modelName) {
|
|
|
563
563
|
async function* livePromptMessages({ initialPrompt, liveInput, sessionId, prompts }) {
|
|
564
564
|
yield makeSdkUserMessage(initialPrompt, sessionId);
|
|
565
565
|
for await (const message of liveInput) {
|
|
566
|
-
|
|
566
|
+
try {
|
|
567
|
+
const sdkMessage = makeSdkUserMessage(
|
|
568
|
+
formatLiveInputGuidance(message.body, prompts),
|
|
569
|
+
sessionId,
|
|
570
|
+
message.id || randomUUID(),
|
|
571
|
+
);
|
|
572
|
+
message.acknowledge?.();
|
|
573
|
+
yield sdkMessage;
|
|
574
|
+
} catch (err) {
|
|
575
|
+
message.reject?.(err);
|
|
576
|
+
throw err;
|
|
577
|
+
}
|
|
567
578
|
}
|
|
568
579
|
}
|
|
569
580
|
|
|
@@ -1051,12 +1062,6 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
1051
1062
|
};
|
|
1052
1063
|
}
|
|
1053
1064
|
|
|
1054
|
-
export const claudeSdkBackend = {
|
|
1055
|
-
kind: "claude",
|
|
1056
|
-
capabilities: runtimeCapabilities("claude"),
|
|
1057
|
-
execute: generateClaudeResponse,
|
|
1058
|
-
};
|
|
1059
|
-
|
|
1060
1065
|
export const claudeRuntimeBridge = {
|
|
1061
1066
|
id: "claude",
|
|
1062
1067
|
kind: "claude",
|
|
@@ -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;
|
|
@@ -1148,6 +1175,77 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1148
1175
|
options.onEvent?.(safeEvent);
|
|
1149
1176
|
}
|
|
1150
1177
|
|
|
1178
|
+
const compactionTurnKey = (params = {}) => `${params.threadId || threadId || "thread"}:${params.turnId || activeTurnId || "turn"}`;
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* @param {{operationId: string, status: string, turnKey?: string, reason?: string, message?: string}} event
|
|
1182
|
+
*/
|
|
1183
|
+
function emitCompaction({
|
|
1184
|
+
operationId,
|
|
1185
|
+
status,
|
|
1186
|
+
turnKey,
|
|
1187
|
+
reason,
|
|
1188
|
+
message,
|
|
1189
|
+
}) {
|
|
1190
|
+
const previous = compactionStatuses.get(operationId);
|
|
1191
|
+
if (previous === status || previous === "succeeded" || previous === "failed" || previous === "skipped") return;
|
|
1192
|
+
compactionStatuses.set(operationId, status);
|
|
1193
|
+
if (status === "running") activeCompactions.set(operationId, { turnKey: turnKey || compactionTurnKey() });
|
|
1194
|
+
else activeCompactions.delete(operationId);
|
|
1195
|
+
emitEvent({
|
|
1196
|
+
type: "context_compaction",
|
|
1197
|
+
operationId,
|
|
1198
|
+
status,
|
|
1199
|
+
sdk: "codex",
|
|
1200
|
+
trigger: "automatic",
|
|
1201
|
+
timestamp: Date.now(),
|
|
1202
|
+
model: actualModel ? `codex:${actualModel}` : requestedReference,
|
|
1203
|
+
...(reason ? { reason } : {}),
|
|
1204
|
+
...(message ? { message } : {}),
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function finalizeOpenCompactions(reason, message) {
|
|
1209
|
+
for (const [operationId, active] of [...activeCompactions]) {
|
|
1210
|
+
emitCompaction({
|
|
1211
|
+
operationId,
|
|
1212
|
+
status: "failed",
|
|
1213
|
+
turnKey: active.turnKey,
|
|
1214
|
+
reason,
|
|
1215
|
+
message,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function handleContextCompactionItem(method, params) {
|
|
1221
|
+
const item = params.item;
|
|
1222
|
+
const turnKey = compactionTurnKey(params);
|
|
1223
|
+
nativeCompactionTurnKeys.add(turnKey);
|
|
1224
|
+
if (legacyCompactionTurnKeys.has(turnKey)) return;
|
|
1225
|
+
const operationId = `codex:${item.id}`;
|
|
1226
|
+
emitCompaction({
|
|
1227
|
+
operationId,
|
|
1228
|
+
status: method === "item/started" ? "running" : "succeeded",
|
|
1229
|
+
turnKey,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function handleLegacyCompaction(params) {
|
|
1234
|
+
const turnKey = compactionTurnKey(params);
|
|
1235
|
+
const active = [...activeCompactions].find(([, value]) => value.turnKey === turnKey);
|
|
1236
|
+
if (active) {
|
|
1237
|
+
emitCompaction({ operationId: active[0], status: "succeeded", turnKey });
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (nativeCompactionTurnKeys.has(turnKey) || legacyCompactionTurnKeys.has(turnKey)) return;
|
|
1241
|
+
legacyCompactionTurnKeys.add(turnKey);
|
|
1242
|
+
emitCompaction({
|
|
1243
|
+
operationId: `codex:${turnKey}:legacy`,
|
|
1244
|
+
status: "succeeded",
|
|
1245
|
+
turnKey,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1151
1249
|
function handleAgentText(text) {
|
|
1152
1250
|
const safeText = redactCodexDiagnostic(text, sensitiveValues);
|
|
1153
1251
|
pushUniqueText(texts, safeText);
|
|
@@ -1229,6 +1327,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1229
1327
|
errorMessage = safeDiagnostic(params.turn?.error?.message || params.turn?.error || "Codex turn failed");
|
|
1230
1328
|
failureKind = "provider_unavailable";
|
|
1231
1329
|
}
|
|
1330
|
+
if (activeCompactions.size > 0) {
|
|
1331
|
+
const cancelled = params.turn?.status === "cancelled" || params.turn?.status === "interrupted";
|
|
1332
|
+
finalizeOpenCompactions(
|
|
1333
|
+
cancelled ? "cancelled" : "incomplete",
|
|
1334
|
+
cancelled ? "Compaction was interrupted." : "Compaction ended without a completion event.",
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1232
1337
|
const safeTurn = params.turn?.error === undefined
|
|
1233
1338
|
? params.turn
|
|
1234
1339
|
: { ...params.turn, error: safeResponseError(params.turn.error) };
|
|
@@ -1238,6 +1343,27 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1238
1343
|
}
|
|
1239
1344
|
if (method === "thread/tokenUsage/updated") {
|
|
1240
1345
|
usage = usageFromTokenUsage(params.tokenUsage);
|
|
1346
|
+
const contextUsage = contextUsageFromTokenUsage(params.tokenUsage);
|
|
1347
|
+
if (contextUsage) {
|
|
1348
|
+
emitEvent({
|
|
1349
|
+
type: "context_usage",
|
|
1350
|
+
sdk: "codex",
|
|
1351
|
+
model: actualModel ? `codex:${actualModel}` : requestedReference,
|
|
1352
|
+
timestamp: Date.now(),
|
|
1353
|
+
...(typeof params.turnId === "string" && params.turnId.length > 0
|
|
1354
|
+
? { measurementId: params.turnId }
|
|
1355
|
+
: {}),
|
|
1356
|
+
...contextUsage,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (method === "model/rerouted") {
|
|
1362
|
+
if (typeof params.toModel === "string" && params.toModel.trim().length > 0) actualModel = params.toModel;
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (method === "thread/compacted") {
|
|
1366
|
+
handleLegacyCompaction(params);
|
|
1241
1367
|
return;
|
|
1242
1368
|
}
|
|
1243
1369
|
if (method === "item/agentMessage/delta") {
|
|
@@ -1258,6 +1384,10 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1258
1384
|
return;
|
|
1259
1385
|
}
|
|
1260
1386
|
if (method === "item/started" || method === "item/completed") {
|
|
1387
|
+
if (params.item?.type === "contextCompaction") {
|
|
1388
|
+
handleContextCompactionItem(method, params);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1261
1391
|
const raw = mapThreadItem(method, params.item);
|
|
1262
1392
|
if (params.item?.type === "agentMessage") {
|
|
1263
1393
|
const text = params.item.text || agentTextByItem.get(params.item.id) || "";
|
|
@@ -1363,60 +1493,79 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1363
1493
|
|
|
1364
1494
|
async function steerLiveInput() {
|
|
1365
1495
|
if (!options.liveInput) return;
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
await Promise.race([
|
|
1370
|
-
|
|
1371
|
-
turnDone,
|
|
1372
|
-
client.closed.then((err) => { throw err; }),
|
|
1496
|
+
const iterator = options.liveInput[Symbol.asyncIterator]();
|
|
1497
|
+
try {
|
|
1498
|
+
while (!turnCompleted) {
|
|
1499
|
+
const next = await Promise.race([
|
|
1500
|
+
iterator.next(),
|
|
1501
|
+
turnDone.then(() => ({ done: true, value: undefined })),
|
|
1373
1502
|
]);
|
|
1374
|
-
if (
|
|
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)) {
|
|
1503
|
+
if (next.done || turnCompleted) break;
|
|
1504
|
+
const message = next.value;
|
|
1505
|
+
if (!threadId || !activeTurnId || !turnReadyResolved) {
|
|
1387
1506
|
await Promise.race([
|
|
1388
1507
|
turnReady,
|
|
1389
1508
|
turnDone,
|
|
1390
|
-
client.closed.then((
|
|
1509
|
+
client.closed.then((err) => { throw err; }),
|
|
1391
1510
|
]);
|
|
1392
|
-
if (turnCompleted) break;
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1511
|
+
if (turnCompleted || !turnReadyResolved) break;
|
|
1512
|
+
}
|
|
1513
|
+
const input = userTextInput(formatLiveInputGuidance(message.body, options.prompts));
|
|
1514
|
+
try {
|
|
1515
|
+
const response = await client.request("turn/steer", {
|
|
1516
|
+
threadId,
|
|
1517
|
+
expectedTurnId: activeTurnId,
|
|
1518
|
+
input,
|
|
1519
|
+
});
|
|
1520
|
+
activeTurnId = response?.turnId || activeTurnId;
|
|
1521
|
+
message.acknowledge?.();
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
const providerError = err?.responseError;
|
|
1524
|
+
if (isNoActiveTurnToSteer(providerError || err)) {
|
|
1525
|
+
await Promise.race([
|
|
1526
|
+
turnReady,
|
|
1527
|
+
turnDone,
|
|
1528
|
+
client.closed.then((closedErr) => { throw closedErr; }),
|
|
1529
|
+
]);
|
|
1530
|
+
if (turnCompleted) break;
|
|
1531
|
+
try {
|
|
1532
|
+
const response = await client.request("turn/steer", {
|
|
1533
|
+
threadId,
|
|
1534
|
+
expectedTurnId: activeTurnId,
|
|
1535
|
+
input,
|
|
1536
|
+
});
|
|
1537
|
+
activeTurnId = response?.turnId || activeTurnId;
|
|
1538
|
+
message.acknowledge?.();
|
|
1539
|
+
continue;
|
|
1540
|
+
} catch (retryErr) {
|
|
1541
|
+
message.reject?.(retryErr);
|
|
1542
|
+
const retryProviderError = retryErr?.responseError
|
|
1543
|
+
? safeResponseError(retryErr.responseError)
|
|
1544
|
+
: null;
|
|
1545
|
+
emitEvent({
|
|
1546
|
+
type: "runtime_warning",
|
|
1547
|
+
warning_kind: isActiveTurnNotSteerable(retryProviderError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
1548
|
+
message: safeDiagnostic(codexErrorMessage(retryProviderError || retryErr)),
|
|
1549
|
+
});
|
|
1550
|
+
// Preserve FIFO fallback: once one message is rejected, later
|
|
1551
|
+
// entries must not overtake it inside this provider attempt.
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1411
1554
|
}
|
|
1555
|
+
message.reject?.(err);
|
|
1556
|
+
emitEvent({
|
|
1557
|
+
type: "runtime_warning",
|
|
1558
|
+
warning_kind: isActiveTurnNotSteerable(providerError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
1559
|
+
message: safeDiagnostic(codexErrorMessage(
|
|
1560
|
+
providerError ? safeResponseError(providerError) : err,
|
|
1561
|
+
)),
|
|
1562
|
+
});
|
|
1563
|
+
break;
|
|
1412
1564
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
providerError ? safeResponseError(providerError) : err,
|
|
1418
|
-
)),
|
|
1419
|
-
});
|
|
1565
|
+
}
|
|
1566
|
+
} finally {
|
|
1567
|
+
if (typeof iterator.return === "function") {
|
|
1568
|
+
try { void Promise.resolve(iterator.return()).catch(() => {}); } catch { /* best-effort */ }
|
|
1420
1569
|
}
|
|
1421
1570
|
}
|
|
1422
1571
|
}
|
|
@@ -1656,7 +1805,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1656
1805
|
abortRaceCleanup();
|
|
1657
1806
|
}
|
|
1658
1807
|
turnCompleted = true;
|
|
1659
|
-
await
|
|
1808
|
+
await steerTask;
|
|
1660
1809
|
|
|
1661
1810
|
const text = texts[texts.length - 1] || "";
|
|
1662
1811
|
let codexErrorCode = prematureClose ? "codex_app_server_closed" : null;
|
|
@@ -1683,7 +1832,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1683
1832
|
});
|
|
1684
1833
|
}
|
|
1685
1834
|
const hadPartialProgress = events.length > 0 || texts.length > 0;
|
|
1686
|
-
const reference =
|
|
1835
|
+
const reference = requestedReference;
|
|
1687
1836
|
const inputTokens = usage?.input_tokens ?? usage?.inputTokens ?? 0;
|
|
1688
1837
|
const outputTokens = usage?.output_tokens ?? usage?.outputTokens ?? 0;
|
|
1689
1838
|
const cachedTokens = usage?.cache_read_tokens ?? usage?.cachedInputTokens ?? 0;
|
|
@@ -1774,6 +1923,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1774
1923
|
}),
|
|
1775
1924
|
};
|
|
1776
1925
|
} finally {
|
|
1926
|
+
if (activeCompactions.size > 0) {
|
|
1927
|
+
const cancelled = !!options.abortSignal?.aborted;
|
|
1928
|
+
finalizeOpenCompactions(
|
|
1929
|
+
cancelled ? "cancelled" : "incomplete",
|
|
1930
|
+
cancelled ? "Compaction was interrupted." : "Compaction ended without a completion event.",
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1777
1933
|
options.abortSignal?.removeEventListener?.("abort", abortHandler);
|
|
1778
1934
|
if (resumeEntry) {
|
|
1779
1935
|
resumeEntry.busy = false;
|
|
@@ -1784,12 +1940,6 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1784
1940
|
}
|
|
1785
1941
|
}
|
|
1786
1942
|
|
|
1787
|
-
export const codexAppBackend = {
|
|
1788
|
-
kind: "codex-app",
|
|
1789
|
-
capabilities: CODEX_APP_CAPABILITIES,
|
|
1790
|
-
execute: generateCodexAppResponse,
|
|
1791
|
-
};
|
|
1792
|
-
|
|
1793
1943
|
// CLI bridge for sdk='codex' agents that opt into execution_mode='cli'. The
|
|
1794
1944
|
// codex `app-server` is more capable than `codex exec` (better event
|
|
1795
1945
|
// 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)) {
|