@agentex/agent 0.0.34 → 0.0.36
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/CHANGELOG.md +227 -0
- package/README.md +2 -1
- package/dist/providers/claude/discovery.d.ts +64 -0
- package/dist/providers/claude/discovery.d.ts.map +1 -0
- package/dist/providers/claude/discovery.js +147 -0
- package/dist/providers/claude/discovery.js.map +1 -0
- package/dist/providers/claude/effort.d.ts +20 -0
- package/dist/providers/claude/effort.d.ts.map +1 -0
- package/dist/providers/claude/effort.js +32 -0
- package/dist/providers/claude/effort.js.map +1 -0
- package/dist/providers/claude/execute.d.ts.map +1 -1
- package/dist/providers/claude/execute.js +2 -1
- package/dist/providers/claude/execute.js.map +1 -1
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +2 -1
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/parse.d.ts.map +1 -1
- package/dist/providers/claude/parse.js +26 -4
- package/dist/providers/claude/parse.js.map +1 -1
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +2 -1
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/discovery.d.ts +20 -0
- package/dist/providers/codex/discovery.d.ts.map +1 -0
- package/dist/providers/codex/discovery.js +76 -0
- package/dist/providers/codex/discovery.js.map +1 -0
- package/dist/providers/codex/execute.d.ts.map +1 -1
- package/dist/providers/codex/execute.js +45 -3
- package/dist/providers/codex/execute.js.map +1 -1
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +2 -1
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/parse.d.ts +14 -6
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +51 -16
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +29 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +255 -57
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/cursor/discovery.d.ts.map +1 -1
- package/dist/providers/cursor/discovery.js +4 -0
- package/dist/providers/cursor/discovery.js.map +1 -1
- package/dist/providers/opencode/discovery.d.ts.map +1 -1
- package/dist/providers/opencode/discovery.js +11 -8
- package/dist/providers/opencode/discovery.js.map +1 -1
- package/dist/providers/opencode/event-parse.d.ts +29 -0
- package/dist/providers/opencode/event-parse.d.ts.map +1 -1
- package/dist/providers/opencode/event-parse.js +55 -0
- package/dist/providers/opencode/event-parse.js.map +1 -1
- package/dist/providers/opencode/history.d.ts.map +1 -1
- package/dist/providers/opencode/history.js +25 -4
- package/dist/providers/opencode/history.js.map +1 -1
- package/dist/providers/opencode/http-session.d.ts.map +1 -1
- package/dist/providers/opencode/http-session.js +72 -8
- package/dist/providers/opencode/http-session.js.map +1 -1
- package/dist/types.d.ts +19 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/model-cache.d.ts +33 -0
- package/dist/utils/model-cache.d.ts.map +1 -0
- package/dist/utils/model-cache.js +57 -0
- package/dist/utils/model-cache.js.map +1 -0
- package/package.json +1 -1
- package/src/providers/claude/discovery.ts +215 -0
- package/src/providers/claude/effort.ts +34 -0
- package/src/providers/claude/execute.ts +2 -1
- package/src/providers/claude/index.ts +2 -1
- package/src/providers/claude/parse.ts +27 -5
- package/src/providers/claude/session.ts +2 -1
- package/src/providers/codex/discovery.ts +91 -0
- package/src/providers/codex/execute.ts +42 -3
- package/src/providers/codex/index.ts +2 -1
- package/src/providers/codex/parse.ts +60 -18
- package/src/providers/codex/session.ts +285 -59
- package/src/providers/cursor/discovery.ts +5 -0
- package/src/providers/opencode/discovery.ts +10 -7
- package/src/providers/opencode/event-parse.ts +77 -0
- package/src/providers/opencode/history.ts +32 -4
- package/src/providers/opencode/http-session.ts +72 -8
- package/src/types.ts +19 -5
- package/src/utils/model-cache.ts +67 -0
|
@@ -27,7 +27,7 @@ import { translateEndpoint } from "../../utils/endpoint.js";
|
|
|
27
27
|
import { injectWorkspaceSkills } from "../../utils/skills.js";
|
|
28
28
|
import { resolveInstructions } from "../../utils/instructions.js";
|
|
29
29
|
import { createToolNameTracker } from "../../utils/tool-names.js";
|
|
30
|
-
import {
|
|
30
|
+
import { parseCodexStreamLines } from "./parse.js";
|
|
31
31
|
import { withPlanModePreamble } from "./plan-mode.js";
|
|
32
32
|
import { scanCodexSessionUsage } from "./usage-scanner.js";
|
|
33
33
|
import { codexSessionCodec } from "./codec.js";
|
|
@@ -130,6 +130,12 @@ interface PendingResult {
|
|
|
130
130
|
settled?: boolean;
|
|
131
131
|
/** Tear down this send's timeout timer / abort listener. */
|
|
132
132
|
cleanup?: () => void;
|
|
133
|
+
/**
|
|
134
|
+
* Turn generation this send joined. Result delivery drains only the
|
|
135
|
+
* generation it belongs to, so a send that starts a NEW turn while the
|
|
136
|
+
* previous turn's result is still draining cannot be settled by it.
|
|
137
|
+
*/
|
|
138
|
+
generation: number;
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
interface TrackedBackgroundTask {
|
|
@@ -152,11 +158,28 @@ const BACKGROUND_TASK_POLL_INTERVAL_MS = 2_000;
|
|
|
152
158
|
const BACKGROUND_TASK_READ_TIMEOUT_MS = 5_000;
|
|
153
159
|
|
|
154
160
|
/** Identity latch for the root turn currently represented by this session. */
|
|
161
|
+
/**
|
|
162
|
+
* One turn's outcome, frozen at the terminal frame.
|
|
163
|
+
*
|
|
164
|
+
* Delivery is asynchronous (event chain, and an on-disk usage scan when the
|
|
165
|
+
* stream carried no usage), so reading the live accumulators at delivery time
|
|
166
|
+
* reads whatever turn is running *then*. Snapshotting decouples the two: the
|
|
167
|
+
* accumulators reset immediately and belong to the next turn from that moment.
|
|
168
|
+
*/
|
|
169
|
+
interface TurnOutcome {
|
|
170
|
+
summary: string | null;
|
|
171
|
+
isError: boolean;
|
|
172
|
+
errorMessage: string | null;
|
|
173
|
+
wasInterrupted: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
interface ActiveTurnReady {
|
|
156
177
|
promise: Promise<string | null>;
|
|
157
178
|
resolve: (turnId: string | null) => void;
|
|
158
179
|
reject: (err: Error) => void;
|
|
159
180
|
settled: boolean;
|
|
181
|
+
/** Monotonic id for this turn, used to fence late result delivery. */
|
|
182
|
+
generation: number;
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
// ---------------------------------------------------------------------------
|
|
@@ -403,6 +426,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
403
426
|
*/
|
|
404
427
|
private _activeTurnId: string | null = null;
|
|
405
428
|
private _activeTurnReady: ActiveTurnReady | null = null;
|
|
429
|
+
/** Incremented per turn latch. Never reused, so staleness is decidable. */
|
|
430
|
+
private _turnGeneration = 0;
|
|
406
431
|
/** Successful repeated interrupts coalesce until the terminal notification. */
|
|
407
432
|
private _interruptPromise: Promise<void> | null = null;
|
|
408
433
|
/** Prevents a late interrupt request after the terminal frame was observed. */
|
|
@@ -555,6 +580,22 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
555
580
|
}
|
|
556
581
|
}
|
|
557
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Remove and return the pending sends belonging to one turn generation.
|
|
585
|
+
*
|
|
586
|
+
* `undefined` matches entries with no generation, which is how tests that
|
|
587
|
+
* drive the session without `send()` enqueue resolvers.
|
|
588
|
+
*/
|
|
589
|
+
private takePendingResults(generation: number | undefined): PendingResult[] {
|
|
590
|
+
const mine: PendingResult[] = [];
|
|
591
|
+
const others: PendingResult[] = [];
|
|
592
|
+
for (const entry of this._pendingResults) {
|
|
593
|
+
(entry.generation === generation ? mine : others).push(entry);
|
|
594
|
+
}
|
|
595
|
+
this._pendingResults = others;
|
|
596
|
+
return mine;
|
|
597
|
+
}
|
|
598
|
+
|
|
558
599
|
/** Reject every pending send() Promise and outgoing JSON-RPC call. */
|
|
559
600
|
private rejectAllPending(err: Error): void {
|
|
560
601
|
const pending = this._pendingResults.splice(0);
|
|
@@ -589,6 +630,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
589
630
|
resolve: resolveFn,
|
|
590
631
|
reject: rejectFn,
|
|
591
632
|
settled: false,
|
|
633
|
+
generation: ++this._turnGeneration,
|
|
592
634
|
};
|
|
593
635
|
this._activeTurnId = null;
|
|
594
636
|
this._activeTurnReady = ready;
|
|
@@ -614,9 +656,16 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
614
656
|
expected.reject(err);
|
|
615
657
|
}
|
|
616
658
|
|
|
617
|
-
/**
|
|
618
|
-
|
|
659
|
+
/**
|
|
660
|
+
* Clear the current turn and release an interrupt waiting for its id.
|
|
661
|
+
*
|
|
662
|
+
* `expected` fences the clear against a turn that started after the one being
|
|
663
|
+
* torn down. Without it, a late teardown nulls the latch a newer, running
|
|
664
|
+
* turn is relying on, which is the same silent-Stop failure by another route.
|
|
665
|
+
*/
|
|
666
|
+
private clearActiveTurn(err?: Error, expected?: ActiveTurnReady): void {
|
|
619
667
|
const ready = this._activeTurnReady;
|
|
668
|
+
if (expected && ready !== expected) return;
|
|
620
669
|
if (ready && !ready.settled) {
|
|
621
670
|
ready.settled = true;
|
|
622
671
|
if (err) ready.reject(err);
|
|
@@ -834,9 +883,17 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
834
883
|
// and pass through. If the second `turn/start` lands during the first
|
|
835
884
|
// turn, the per-turn accumulators continue collecting until the result
|
|
836
885
|
// event fires; the result then drains all pending resolvers.
|
|
886
|
+
// Joining the active turn requires that it still be active. The latch
|
|
887
|
+
// survives the terminal frame until deliverTurnResult() clears it, and that
|
|
888
|
+
// gap is real time: it spans `await this._eventChain` (a host persisting
|
|
889
|
+
// events) and, when turn/completed carried no usage, an on-disk usage scan.
|
|
890
|
+
// A send landing in that gap belongs to a NEW turn — joining the dead one
|
|
891
|
+
// would leave it with no interrupt latch and no turn/start capture, so Stop
|
|
892
|
+
// would find nothing to interrupt and return as if it had succeeded.
|
|
837
893
|
const existingTurnReady = this._activeTurnReady;
|
|
838
|
-
const
|
|
839
|
-
const
|
|
894
|
+
const canJoinActiveTurn = existingTurnReady !== null && !this._turnTerminalObserved;
|
|
895
|
+
const isTurnLeader = !canJoinActiveTurn;
|
|
896
|
+
const turnReady = canJoinActiveTurn ? existingTurnReady! : this.beginActiveTurn();
|
|
840
897
|
|
|
841
898
|
if (this._state === "idle") {
|
|
842
899
|
this._state = "thinking";
|
|
@@ -868,7 +925,11 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
868
925
|
rejectFn = reject;
|
|
869
926
|
});
|
|
870
927
|
|
|
871
|
-
const entry: PendingResult = {
|
|
928
|
+
const entry: PendingResult = {
|
|
929
|
+
resolve: resolveFn,
|
|
930
|
+
reject: rejectFn,
|
|
931
|
+
generation: turnReady.generation,
|
|
932
|
+
};
|
|
872
933
|
this._pendingResults.push(entry);
|
|
873
934
|
|
|
874
935
|
// Track the in-flight turn so drain() can await it; drop it on settle.
|
|
@@ -1072,6 +1133,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1072
1133
|
method: "session/closed",
|
|
1073
1134
|
});
|
|
1074
1135
|
this.cancelBackgroundTaskPollTimers();
|
|
1136
|
+
// finalizeActiveBackgroundTasks above has already emitted the terminal
|
|
1137
|
+
// edges, so nothing downstream needs these records any more.
|
|
1138
|
+
this._backgroundTasks.clear();
|
|
1139
|
+
this._backgroundTaskIdsByPath.clear();
|
|
1075
1140
|
this.rejectAllPending(new Error("Codex session closed"));
|
|
1076
1141
|
|
|
1077
1142
|
this.proc.stdin!.end();
|
|
@@ -1162,6 +1227,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1162
1227
|
method === "item/fileChange/requestApproval"
|
|
1163
1228
|
) {
|
|
1164
1229
|
void this.handleApproval(id, method, params);
|
|
1230
|
+
} else if (method === "item/permissions/requestApproval") {
|
|
1231
|
+
void this.handlePermissionsApproval(id, params);
|
|
1165
1232
|
} else if (
|
|
1166
1233
|
method === "item/tool/requestUserInput" ||
|
|
1167
1234
|
method === "tool/requestUserInput"
|
|
@@ -1169,7 +1236,13 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1169
1236
|
// `tool/requestUserInput` is the legacy method name on older codex builds.
|
|
1170
1237
|
void this.handleUserInputRequest(id, params);
|
|
1171
1238
|
} else {
|
|
1172
|
-
// Unknown server request
|
|
1239
|
+
// Unknown server request. `{}` is a best-effort reply, NOT a guarantee
|
|
1240
|
+
// that the turn proceeds: every server request the protocol defines but
|
|
1241
|
+
// this file does not handle declares required response fields, so an
|
|
1242
|
+
// empty result is schema-invalid for all of them. A JSON-RPC error reply
|
|
1243
|
+
// would be the honest answer, but `rpcResponse` has no error path and
|
|
1244
|
+
// introducing one changes behavior (an error can abort a turn where this
|
|
1245
|
+
// limps along), so that is a deliberate follow-up rather than a fix here.
|
|
1173
1246
|
this.rpcResponse(id, {});
|
|
1174
1247
|
}
|
|
1175
1248
|
}
|
|
@@ -1186,14 +1259,23 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1186
1259
|
}
|
|
1187
1260
|
|
|
1188
1261
|
private async handleApproval(id: number, method: string, params: Record<string, unknown>): Promise<void> {
|
|
1189
|
-
|
|
1262
|
+
// A child thread's approval still has to be answered — somebody must decide
|
|
1263
|
+
// whether that subagent may run its command — but it is not the root turn's
|
|
1264
|
+
// business. Driving root state from it would show the session as blocked on
|
|
1265
|
+
// approval when the root turn is running fine, and `restoreStateAfter` would
|
|
1266
|
+
// then recompute root state from a child's lifecycle. The protocol makes
|
|
1267
|
+
// `threadId` mandatory on every approval, so the attribution is free.
|
|
1268
|
+
const requestThreadId = str(params, "threadId") || null;
|
|
1269
|
+
const foreign = this.isForeignThread(requestThreadId);
|
|
1270
|
+
if (!foreign) this._state = "waiting_for_approval";
|
|
1271
|
+
const restore = () => { if (!foreign) this.restoreStateAfter("waiting_for_approval"); };
|
|
1190
1272
|
|
|
1191
1273
|
// Codex's app-server expects `{ decision: "accept" | "decline" | "cancel" }`
|
|
1192
1274
|
// (NOT `{ approved: boolean }`). agentex's UserInputResponse has no interrupt
|
|
1193
1275
|
// concept, so allow → accept and deny → decline.
|
|
1194
1276
|
if (!this.ctx.onUserInputRequest) {
|
|
1195
1277
|
this.rpcResponse(id, { decision: "accept" });
|
|
1196
|
-
|
|
1278
|
+
restore();
|
|
1197
1279
|
return;
|
|
1198
1280
|
}
|
|
1199
1281
|
|
|
@@ -1205,15 +1287,67 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1205
1287
|
const resp = await this.ctx.onUserInputRequest({
|
|
1206
1288
|
toolName,
|
|
1207
1289
|
input: params,
|
|
1208
|
-
|
|
1209
|
-
|
|
1290
|
+
// `itemId`, not `id` — the protocol has no `id` here, so this was
|
|
1291
|
+
// always "". It is the field that lets a host line an approval up with
|
|
1292
|
+
// the tool_call event carrying the same item id.
|
|
1293
|
+
toolUseId: str(params, "itemId"),
|
|
1294
|
+
// FileChangeRequestApprovalParams carries `grantRoot`, never `path`,
|
|
1295
|
+
// so file-change approvals reached hosts with no description at all.
|
|
1296
|
+
description: str(params, "command")
|
|
1297
|
+
|| str(params, "grantRoot")
|
|
1298
|
+
|| str(params, "reason")
|
|
1299
|
+
|| undefined,
|
|
1300
|
+
// Which subagent is asking. The child thread id is also its
|
|
1301
|
+
// `background_task.taskId`, so a host can line the approval up with
|
|
1302
|
+
// the task it already renders. Absent for the root turn.
|
|
1303
|
+
...(foreign && requestThreadId ? { agentId: requestThreadId } : {}),
|
|
1210
1304
|
});
|
|
1211
1305
|
this.rpcResponse(id, { decision: resp.allow ? "accept" : "decline" });
|
|
1212
1306
|
} catch {
|
|
1213
1307
|
this.rpcResponse(id, { decision: "decline" });
|
|
1214
1308
|
}
|
|
1215
1309
|
|
|
1216
|
-
|
|
1310
|
+
restore();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Handle `item/permissions/requestApproval`.
|
|
1315
|
+
*
|
|
1316
|
+
* Unlike the accept/decline approvals, the response here IS the grant: the
|
|
1317
|
+
* schema requires a `permissions` profile, so the generic "ack with `{}` to
|
|
1318
|
+
* unblock the turn" fallback this used to land in was schema-invalid.
|
|
1319
|
+
* Granting means echoing back the profile the agent asked for; refusing means
|
|
1320
|
+
* an empty profile, which is valid and grants nothing beyond what it already
|
|
1321
|
+
* has. With no host handler we refuse, because silently widening a sandbox is
|
|
1322
|
+
* not a safe default for an unattended session.
|
|
1323
|
+
*/
|
|
1324
|
+
private async handlePermissionsApproval(id: number, params: Record<string, unknown>): Promise<void> {
|
|
1325
|
+
const requestThreadId = str(params, "threadId") || null;
|
|
1326
|
+
const foreign = this.isForeignThread(requestThreadId);
|
|
1327
|
+
if (!foreign) this._state = "waiting_for_approval";
|
|
1328
|
+
const restore = () => { if (!foreign) this.restoreStateAfter("waiting_for_approval"); };
|
|
1329
|
+
const requested = asObj(params, "permissions");
|
|
1330
|
+
|
|
1331
|
+
if (!this.ctx.onUserInputRequest) {
|
|
1332
|
+
this.rpcResponse(id, { permissions: {} });
|
|
1333
|
+
restore();
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
try {
|
|
1338
|
+
const resp = await this.ctx.onUserInputRequest({
|
|
1339
|
+
toolName: "permissions",
|
|
1340
|
+
input: params,
|
|
1341
|
+
toolUseId: str(params, "itemId"),
|
|
1342
|
+
description: str(params, "reason") || undefined,
|
|
1343
|
+
...(foreign && requestThreadId ? { agentId: requestThreadId } : {}),
|
|
1344
|
+
});
|
|
1345
|
+
this.rpcResponse(id, { permissions: resp.allow ? requested : {} });
|
|
1346
|
+
} catch {
|
|
1347
|
+
this.rpcResponse(id, { permissions: {} });
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
restore();
|
|
1217
1351
|
}
|
|
1218
1352
|
|
|
1219
1353
|
/**
|
|
@@ -1223,9 +1357,17 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1223
1357
|
* back in Codex's `{ answers: { [questionId]: { answers: string[] } } }` shape.
|
|
1224
1358
|
*/
|
|
1225
1359
|
private async handleUserInputRequest(id: number, params: Record<string, unknown>): Promise<void> {
|
|
1360
|
+
// Same scoping as the approval handlers: a child subagent's question still
|
|
1361
|
+
// has to be answered, but it is not the root turn's business. Blocking root
|
|
1362
|
+
// state on it shows the session as waiting for input when the root turn is
|
|
1363
|
+
// running fine. `ToolRequestUserInputParams` requires `threadId`, so the
|
|
1364
|
+
// attribution is read from the wire rather than inferred.
|
|
1365
|
+
const requestThreadId = str(params, "threadId") || null;
|
|
1366
|
+
const foreign = this.isForeignThread(requestThreadId);
|
|
1226
1367
|
// Questions are user *input*, not a tool-permission gate — distinct state so a
|
|
1227
1368
|
// host UI can render a question form vs an approval prompt.
|
|
1228
|
-
this._state = "waiting_for_input";
|
|
1369
|
+
if (!foreign) this._state = "waiting_for_input";
|
|
1370
|
+
const restore = () => { if (!foreign) this.restoreStateAfter("waiting_for_input"); };
|
|
1229
1371
|
|
|
1230
1372
|
const questions = parseCodexQuestions(params);
|
|
1231
1373
|
|
|
@@ -1233,7 +1375,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1233
1375
|
// agent proceeds without hanging.
|
|
1234
1376
|
if (!this.ctx.onUserInputRequest || questions.length === 0) {
|
|
1235
1377
|
this.rpcResponse(id, { answers: {} });
|
|
1236
|
-
|
|
1378
|
+
restore();
|
|
1237
1379
|
return;
|
|
1238
1380
|
}
|
|
1239
1381
|
|
|
@@ -1241,14 +1383,16 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1241
1383
|
const resp = await this.ctx.onUserInputRequest({
|
|
1242
1384
|
toolName: "AskUserQuestion",
|
|
1243
1385
|
input: { questions },
|
|
1244
|
-
toolUseId: str(params, "
|
|
1386
|
+
toolUseId: str(params, "itemId") || "codex-user-input",
|
|
1387
|
+
// Which subagent is asking, matching its `background_task.taskId`.
|
|
1388
|
+
...(foreign && requestThreadId ? { agentId: requestThreadId } : {}),
|
|
1245
1389
|
});
|
|
1246
1390
|
this.rpcResponse(id, { answers: buildCodexUserInputAnswers(questions, resp) });
|
|
1247
1391
|
} catch {
|
|
1248
1392
|
this.rpcResponse(id, { answers: {} });
|
|
1249
1393
|
}
|
|
1250
1394
|
|
|
1251
|
-
|
|
1395
|
+
restore();
|
|
1252
1396
|
}
|
|
1253
1397
|
|
|
1254
1398
|
// -------------------------------------------------------------------------
|
|
@@ -1348,12 +1492,14 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1348
1492
|
* A completed spawn call only means the child was created. Register every
|
|
1349
1493
|
* receiver and then follow the child thread itself for the real terminus.
|
|
1350
1494
|
*/
|
|
1495
|
+
/** @returns whether the item produced at least one background-task edge. */
|
|
1351
1496
|
private handleCollabAgentToolCall(
|
|
1352
1497
|
item: Record<string, unknown>,
|
|
1353
1498
|
raw: Record<string, unknown>,
|
|
1354
1499
|
parentTaskId: string | null,
|
|
1355
|
-
):
|
|
1356
|
-
if (this._state === "closed" || !this.ctx.onEvent) return;
|
|
1500
|
+
): boolean {
|
|
1501
|
+
if (this._state === "closed" || !this.ctx.onEvent) return false;
|
|
1502
|
+
let dispatched = false;
|
|
1357
1503
|
|
|
1358
1504
|
const tool = str(item, "tool");
|
|
1359
1505
|
const states = asObj(item, "agentsStates");
|
|
@@ -1364,7 +1510,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1364
1510
|
const canStopWithoutState = tool === "closeAgent" && str(item, "status") === "completed";
|
|
1365
1511
|
|
|
1366
1512
|
for (const taskId of taskIds) {
|
|
1367
|
-
|
|
1513
|
+
const previous = this._backgroundTasks.get(taskId);
|
|
1368
1514
|
const description = str(item, "prompt") || previous?.description || null;
|
|
1369
1515
|
const resolvedParentTaskId = previous?.parentTaskId ?? parentTaskId;
|
|
1370
1516
|
|
|
@@ -1383,6 +1529,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1383
1529
|
: null,
|
|
1384
1530
|
raw,
|
|
1385
1531
|
}));
|
|
1532
|
+
dispatched = true;
|
|
1386
1533
|
}
|
|
1387
1534
|
continue;
|
|
1388
1535
|
}
|
|
@@ -1391,7 +1538,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1391
1538
|
if (!canStart && !canStopWithoutState && !(taskId in states)) continue;
|
|
1392
1539
|
|
|
1393
1540
|
const hasState = taskId in states;
|
|
1394
|
-
const state = !hasState &&
|
|
1541
|
+
const state = !hasState && canStopWithoutState
|
|
1395
1542
|
? { terminal: true, status: "stopped" as const, summary: null }
|
|
1396
1543
|
: codexAgentState(states[taskId]);
|
|
1397
1544
|
|
|
@@ -1405,7 +1552,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1405
1552
|
: null,
|
|
1406
1553
|
raw,
|
|
1407
1554
|
}));
|
|
1408
|
-
|
|
1555
|
+
dispatched = true;
|
|
1409
1556
|
} else if (previous.terminal) {
|
|
1410
1557
|
continue;
|
|
1411
1558
|
} else if (description && description !== previous.description) {
|
|
@@ -1420,6 +1567,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1420
1567
|
: null,
|
|
1421
1568
|
raw,
|
|
1422
1569
|
}));
|
|
1570
|
+
dispatched = true;
|
|
1423
1571
|
}
|
|
1424
1572
|
|
|
1425
1573
|
if (state.terminal) {
|
|
@@ -1432,10 +1580,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1432
1580
|
: null,
|
|
1433
1581
|
raw,
|
|
1434
1582
|
}));
|
|
1435
|
-
|
|
1436
|
-
this.startBackgroundTaskPoller(taskId);
|
|
1583
|
+
dispatched = true;
|
|
1437
1584
|
}
|
|
1438
1585
|
}
|
|
1586
|
+
return dispatched;
|
|
1439
1587
|
}
|
|
1440
1588
|
|
|
1441
1589
|
private waitForBackgroundTaskPoll(pollKey: string): Promise<void> {
|
|
@@ -1591,18 +1739,38 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1591
1739
|
event.summary = summary;
|
|
1592
1740
|
event.parentTaskId = parentTaskId;
|
|
1593
1741
|
|
|
1742
|
+
const terminal = event.phase === "completed";
|
|
1594
1743
|
this._backgroundTasks.set(event.taskId, {
|
|
1595
1744
|
taskId: event.taskId,
|
|
1745
|
+
// `description` survives termination: the reactivation path in
|
|
1746
|
+
// handleForeignNotification reads it precisely BECAUSE the record is
|
|
1747
|
+
// terminal, and dropping it lost a reactivated child's label forever.
|
|
1748
|
+
// It is one bounded string per child. `summary` is the unbounded one —
|
|
1749
|
+
// full agent messages, appended per item — and has no reader that
|
|
1750
|
+
// outlives the task, so it is released.
|
|
1596
1751
|
description,
|
|
1597
|
-
summary,
|
|
1752
|
+
summary: terminal ? null : summary,
|
|
1598
1753
|
parentTaskId,
|
|
1599
|
-
terminal
|
|
1754
|
+
terminal,
|
|
1600
1755
|
generation: previous?.generation ?? 0,
|
|
1601
|
-
activeTurnId: event.
|
|
1602
|
-
? null
|
|
1603
|
-
: event.turnId ?? previous?.activeTurnId ?? null,
|
|
1756
|
+
activeTurnId: terminal ? null : event.turnId ?? previous?.activeTurnId ?? null,
|
|
1604
1757
|
});
|
|
1758
|
+
// Kept past termination too: a grandchild can spawn after its parent has
|
|
1759
|
+
// finished, and lineage resolves through this index.
|
|
1605
1760
|
if (description) this._backgroundTaskIdsByPath.set(description, event.taskId);
|
|
1761
|
+
// Every registration route converges here, so this is the one place that
|
|
1762
|
+
// can guarantee a reconciliation poller exists. Registering by way of the
|
|
1763
|
+
// parser's subAgentActivity branch previously produced a task with no
|
|
1764
|
+
// safety net at all, wholly dependent on the child notification arriving.
|
|
1765
|
+
//
|
|
1766
|
+
// Correctness rests on `startBackgroundTaskPoller` and the poll loop both
|
|
1767
|
+
// re-reading the record and bailing when it is terminal — NOT on the
|
|
1768
|
+
// microtask. The deferral is an optimization: one wire item can emit
|
|
1769
|
+
// several edges in a tick (a collab spawn whose child is already finished
|
|
1770
|
+
// dispatches `started` then `completed`), and yielding first means the
|
|
1771
|
+
// common case declines before spawning a `thread/read` rather than after.
|
|
1772
|
+
// Do not remove the terminal checks on the grounds that this defers.
|
|
1773
|
+
if (!terminal) queueMicrotask(() => this.startBackgroundTaskPoller(event.taskId));
|
|
1606
1774
|
return true;
|
|
1607
1775
|
}
|
|
1608
1776
|
|
|
@@ -1625,7 +1793,23 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1625
1793
|
// App-server can publish child thread/started before the corresponding
|
|
1626
1794
|
// root subAgentActivity item. Register only descendants of this session,
|
|
1627
1795
|
// not unrelated foreign threads multiplexed by a future server version.
|
|
1628
|
-
if (parentThreadId !== rootThreadId && !parentTask)
|
|
1796
|
+
if (parentThreadId !== rootThreadId && !parentTask) {
|
|
1797
|
+
// Say so. A wire shape that omits parentThreadId takes this branch for
|
|
1798
|
+
// every child, which silently disables the whole registration and
|
|
1799
|
+
// reconciliation path — the safety net that exists precisely because
|
|
1800
|
+
// child notifications cannot be relied on. Degrading quietly here
|
|
1801
|
+
// hides the one failure this machinery was built to survive.
|
|
1802
|
+
if (!parentThreadId && this.ctx.onOutput) {
|
|
1803
|
+
try {
|
|
1804
|
+
void this.ctx.onOutput(
|
|
1805
|
+
"stderr",
|
|
1806
|
+
`agentex: codex child thread ${childThreadId} announced no parent thread id; `
|
|
1807
|
+
+ "background-task tracking and reconciliation are disabled for it\n",
|
|
1808
|
+
);
|
|
1809
|
+
} catch { /* swallow */ }
|
|
1810
|
+
}
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1629
1813
|
|
|
1630
1814
|
const source = asObj(thread, "source");
|
|
1631
1815
|
const subAgent = Object.keys(asObj(source, "subAgent")).length > 0
|
|
@@ -1661,7 +1845,6 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1661
1845
|
parentToolCallId: null,
|
|
1662
1846
|
raw: parseJson(rawLine) ?? params,
|
|
1663
1847
|
});
|
|
1664
|
-
this.startBackgroundTaskPoller(childThreadId);
|
|
1665
1848
|
return;
|
|
1666
1849
|
}
|
|
1667
1850
|
|
|
@@ -1696,7 +1879,6 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1696
1879
|
parentToolCallId: null,
|
|
1697
1880
|
raw: parseJson(rawLine) ?? params,
|
|
1698
1881
|
});
|
|
1699
|
-
this.startBackgroundTaskPoller(childThreadId);
|
|
1700
1882
|
return;
|
|
1701
1883
|
}
|
|
1702
1884
|
|
|
@@ -1807,7 +1989,13 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1807
1989
|
this.extractSummaryFromItem(params);
|
|
1808
1990
|
const item = asObj(params, "item");
|
|
1809
1991
|
if (str(item, "type") === "collabAgentToolCall") {
|
|
1810
|
-
|
|
1992
|
+
// Shapes we model become background_task edges. Anything that produced
|
|
1993
|
+
// no edge still reaches the host as a raw `unknown` event rather than
|
|
1994
|
+
// vanishing — that escape hatch is how unmodeled wire shapes stay
|
|
1995
|
+
// visible, and collab is exactly where new shapes keep appearing.
|
|
1996
|
+
if (!this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, null)) {
|
|
1997
|
+
this.emitStreamEvent(rawLine);
|
|
1998
|
+
}
|
|
1811
1999
|
return;
|
|
1812
2000
|
}
|
|
1813
2001
|
this.emitStreamEvent(rawLine);
|
|
@@ -1994,11 +2182,37 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1994
2182
|
}
|
|
1995
2183
|
|
|
1996
2184
|
private resolveTurn(): void {
|
|
2185
|
+
// Bind delivery to the turn that just terminated. Everything below this
|
|
2186
|
+
// point can yield (usage scan, event chain), and a send() arriving in the
|
|
2187
|
+
// meantime starts a new turn that must not be settled by this result.
|
|
2188
|
+
const delivering = this._activeTurnReady;
|
|
1997
2189
|
const resolvedModel = this._turnModel ?? this.model;
|
|
1998
2190
|
let usage = this._turnUsage && resolvedModel
|
|
1999
2191
|
? { [resolvedModel]: { inputTokens: this._turnUsage.inputTokens, outputTokens: this._turnUsage.outputTokens } }
|
|
2000
2192
|
: undefined;
|
|
2001
2193
|
|
|
2194
|
+
// Freeze this turn's outcome and clear the accumulators NOW, while we are
|
|
2195
|
+
// still synchronous with the terminal frame. Delivery reads the snapshot,
|
|
2196
|
+
// so a turn that starts during the async gap below neither inherits these
|
|
2197
|
+
// values nor has its own overwritten. Resetting inside beginActiveTurn()
|
|
2198
|
+
// instead would not work: the usage-scan path re-reads state after an
|
|
2199
|
+
// await, so the new turn's reset would eat the old turn's summary.
|
|
2200
|
+
const outcome: TurnOutcome = {
|
|
2201
|
+
summary: this._turnSummary,
|
|
2202
|
+
isError: this._turnIsError,
|
|
2203
|
+
errorMessage: this._turnErrorMessage,
|
|
2204
|
+
wasInterrupted: this._turnWasInterrupted,
|
|
2205
|
+
};
|
|
2206
|
+
this._turnSummary = null;
|
|
2207
|
+
this._turnUsage = null;
|
|
2208
|
+
this._turnModel = null;
|
|
2209
|
+
this._turnIsError = false;
|
|
2210
|
+
this._turnWasInterrupted = false;
|
|
2211
|
+
this._turnErrorMessage = null;
|
|
2212
|
+
// Captured before clearing: the usage-scan fallback below still needs it.
|
|
2213
|
+
const turnStartedAt = this._turnStartedAt;
|
|
2214
|
+
this._turnStartedAt = null;
|
|
2215
|
+
|
|
2002
2216
|
// Usage precedence: the `turn.completed` payload is authoritative when
|
|
2003
2217
|
// present (captured above into _turnUsage). Only when the stream carried no
|
|
2004
2218
|
// usage do we fall back to scanning Codex's on-disk session logs.
|
|
@@ -2008,8 +2222,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
2008
2222
|
// latest turn or read partially-written totals. We therefore scan ONLY when
|
|
2009
2223
|
// there is no in-band usage, and never let a scan failure fail the turn
|
|
2010
2224
|
// (usage simply stays undefined). Prefer the in-band payload always.
|
|
2011
|
-
if (!usage &&
|
|
2012
|
-
const startedAt =
|
|
2225
|
+
if (!usage && turnStartedAt) {
|
|
2226
|
+
const startedAt = turnStartedAt;
|
|
2013
2227
|
const threadId = this._threadId ?? undefined;
|
|
2014
2228
|
// Fire-and-forget: scan logs then deliver result
|
|
2015
2229
|
void scanCodexSessionUsage({ startedAfter: startedAt, threadId }).then((scanned) => {
|
|
@@ -2017,22 +2231,26 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
2017
2231
|
}).catch(() => {
|
|
2018
2232
|
// Non-fatal — usage stays undefined
|
|
2019
2233
|
}).finally(() => {
|
|
2020
|
-
void this.deliverTurnResult(usage);
|
|
2234
|
+
void this.deliverTurnResult(usage, delivering, outcome);
|
|
2021
2235
|
});
|
|
2022
2236
|
return;
|
|
2023
2237
|
}
|
|
2024
2238
|
|
|
2025
|
-
void this.deliverTurnResult(usage);
|
|
2239
|
+
void this.deliverTurnResult(usage, delivering, outcome);
|
|
2026
2240
|
}
|
|
2027
2241
|
|
|
2028
|
-
private async deliverTurnResult(
|
|
2242
|
+
private async deliverTurnResult(
|
|
2243
|
+
usage: Record<string, import("../../types.js").TokenUsage> | undefined,
|
|
2244
|
+
delivering: ActiveTurnReady | null,
|
|
2245
|
+
outcome: TurnOutcome,
|
|
2246
|
+
): Promise<void> {
|
|
2029
2247
|
const result: TurnResult = {
|
|
2030
|
-
summary:
|
|
2248
|
+
summary: outcome.summary,
|
|
2031
2249
|
usage,
|
|
2032
2250
|
costUsd: null,
|
|
2033
|
-
status:
|
|
2034
|
-
errorCode:
|
|
2035
|
-
errorMessage:
|
|
2251
|
+
status: outcome.wasInterrupted ? "aborted" : outcome.isError ? "failed" : "completed",
|
|
2252
|
+
errorCode: outcome.wasInterrupted ? "aborted" : outcome.isError ? "execution_error" : null,
|
|
2253
|
+
errorMessage: outcome.wasInterrupted ? "Turn was interrupted" : outcome.errorMessage,
|
|
2036
2254
|
};
|
|
2037
2255
|
|
|
2038
2256
|
// Drain pending onEvent handlers so callers awaiting send() see a settled
|
|
@@ -2048,22 +2266,29 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
2048
2266
|
// session whose stdin is dead.
|
|
2049
2267
|
if (this._state === "closed") return;
|
|
2050
2268
|
|
|
2051
|
-
this
|
|
2052
|
-
|
|
2053
|
-
//
|
|
2054
|
-
//
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
//
|
|
2058
|
-
//
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
this.
|
|
2063
|
-
|
|
2064
|
-
this.
|
|
2065
|
-
|
|
2066
|
-
|
|
2269
|
+
// A send() that landed while this delivery was suspended has already begun
|
|
2270
|
+
// a new turn and owns the session. Its result is still owed to it, so this
|
|
2271
|
+
// delivery settles only its OWN generation and touches neither the state
|
|
2272
|
+
// nor the interrupt latch the new turn needs to stay stoppable.
|
|
2273
|
+
//
|
|
2274
|
+
// `delivering === null` must take this branch too. A duplicate terminal
|
|
2275
|
+
// frame for an already-delivered turn (codex can report failure through
|
|
2276
|
+
// both `turn/completed` with a failed status and a separate `turn/failed`)
|
|
2277
|
+
// reaches here with a null latch, and exempting it restored every symptom
|
|
2278
|
+
// the fence exists to prevent: silent Stop, idle-while-running, and a
|
|
2279
|
+
// stale settle.
|
|
2280
|
+
const superseded = this._activeTurnReady !== delivering;
|
|
2281
|
+
|
|
2282
|
+
// Drain the pending send() resolvers belonging to this turn. Several
|
|
2283
|
+
// concurrent sends coalesced into one turn share the same TurnResult; a
|
|
2284
|
+
// send from a later turn is left in place for its own delivery.
|
|
2285
|
+
const pending = this.takePendingResults(delivering?.generation);
|
|
2286
|
+
|
|
2287
|
+
if (!superseded) {
|
|
2288
|
+
this._state = "idle";
|
|
2289
|
+
// Accumulators were already snapshotted and cleared in resolveTurn().
|
|
2290
|
+
this.clearActiveTurn(undefined, delivering ?? undefined);
|
|
2291
|
+
}
|
|
2067
2292
|
|
|
2068
2293
|
for (const p of pending) {
|
|
2069
2294
|
// Skip sends already settled early by timeout / abort.
|
|
@@ -2084,8 +2309,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
2084
2309
|
// Pass current threadId so NDJSON-shaped events (via codex/event wrapper)
|
|
2085
2310
|
// carry sessionId. v2 notifications parse threadId from params directly
|
|
2086
2311
|
// and ignore this arg.
|
|
2087
|
-
const event
|
|
2088
|
-
|
|
2312
|
+
for (const event of parseCodexStreamLines(rawLine, this._threadId)) {
|
|
2313
|
+
this.dispatchEvent(event);
|
|
2314
|
+
}
|
|
2089
2315
|
}
|
|
2090
2316
|
|
|
2091
2317
|
/**
|
|
@@ -2,6 +2,7 @@ import type { AgentMode, ListModelsOptions, ListModesOptions, ProviderModel } fr
|
|
|
2
2
|
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
3
3
|
import { runChildProcess } from "../../utils/process.js";
|
|
4
4
|
import { findCursorBinary } from "./runtime.js";
|
|
5
|
+
import { withModelCache } from "../../utils/model-cache.js";
|
|
5
6
|
|
|
6
7
|
function rec(value: unknown): Record<string, unknown> {
|
|
7
8
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -71,6 +72,10 @@ export function cursorModesFromHelp(output: string): AgentMode[] {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export async function listCursorModels(options: ListModelsOptions = {}): Promise<ProviderModel[]> {
|
|
75
|
+
return withModelCache("cursor", options, options.cacheTtlMs, () => discoverCursorModels(options));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function discoverCursorModels(options: ListModelsOptions): Promise<ProviderModel[]> {
|
|
74
79
|
const resolved = await findCursorBinary(options);
|
|
75
80
|
const env = buildEnv(options.env);
|
|
76
81
|
ensurePathInEnv(env);
|