@agentex/agent 0.0.32 → 0.0.33
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 +38 -0
- package/README.md +29 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +1 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/parse.d.ts +4 -6
- package/dist/providers/claude/parse.d.ts.map +1 -1
- package/dist/providers/claude/parse.js +79 -25
- package/dist/providers/claude/parse.js.map +1 -1
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +1 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +22 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +40 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +346 -14
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/providers/claude/index.ts +1 -0
- package/src/providers/claude/parse.ts +81 -23
- package/src/providers/codex/index.ts +1 -0
- package/src/providers/codex/parse.ts +22 -0
- package/src/providers/codex/session.ts +377 -14
- package/src/types.ts +42 -0
|
@@ -132,6 +132,22 @@ interface PendingResult {
|
|
|
132
132
|
cleanup?: () => void;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
interface TrackedBackgroundTask {
|
|
136
|
+
taskId: string;
|
|
137
|
+
description: string | null;
|
|
138
|
+
summary: string | null;
|
|
139
|
+
parentTaskId: string | null;
|
|
140
|
+
terminal: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Identity latch for the root turn currently represented by this session. */
|
|
144
|
+
interface ActiveTurnReady {
|
|
145
|
+
promise: Promise<string | null>;
|
|
146
|
+
resolve: (turnId: string | null) => void;
|
|
147
|
+
reject: (err: Error) => void;
|
|
148
|
+
settled: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
135
151
|
// ---------------------------------------------------------------------------
|
|
136
152
|
// JSON-RPC 2.0 helpers
|
|
137
153
|
// ---------------------------------------------------------------------------
|
|
@@ -342,6 +358,19 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
342
358
|
/** Shared promise so concurrent / repeated `drain()` calls coalesce. */
|
|
343
359
|
private _drainPromise: Promise<void> | null = null;
|
|
344
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Root turn targeted by `interrupt()`. The id is learned asynchronously from
|
|
363
|
+
* the leader `turn/start` response or the root `turn/started` notification.
|
|
364
|
+
* Concurrent sends reuse this latch so a queued response cannot replace the
|
|
365
|
+
* actual active turn.
|
|
366
|
+
*/
|
|
367
|
+
private _activeTurnId: string | null = null;
|
|
368
|
+
private _activeTurnReady: ActiveTurnReady | null = null;
|
|
369
|
+
/** Successful repeated interrupts coalesce until the terminal notification. */
|
|
370
|
+
private _interruptPromise: Promise<void> | null = null;
|
|
371
|
+
/** Prevents a late interrupt request after the terminal frame was observed. */
|
|
372
|
+
private _turnTerminalObserved = false;
|
|
373
|
+
|
|
345
374
|
/** Stamps `tool_result.toolName` by correlating with prior `tool_call`s. */
|
|
346
375
|
private readonly _trackToolName = createToolNameTracker();
|
|
347
376
|
|
|
@@ -351,9 +380,14 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
351
380
|
private _turnUsage: { inputTokens: number; outputTokens: number } | null = null;
|
|
352
381
|
private _turnModel: string | null = null;
|
|
353
382
|
private _turnIsError = false;
|
|
383
|
+
private _turnWasInterrupted = false;
|
|
354
384
|
private _turnErrorMessage: string | null = null;
|
|
355
385
|
private _turnStartedAt: Date | null = null;
|
|
356
386
|
|
|
387
|
+
/** Child-agent lifecycle is informational and never participates in root turn settlement. */
|
|
388
|
+
private readonly _backgroundTasks = new Map<string, TrackedBackgroundTask>();
|
|
389
|
+
private readonly _backgroundTaskIdsByPath = new Map<string, string>();
|
|
390
|
+
|
|
357
391
|
/**
|
|
358
392
|
* Serial dispatch chain for `onEvent`. Each dispatched event appends a
|
|
359
393
|
* handler invocation; the chain enforces in-order delivery and lets
|
|
@@ -446,11 +480,78 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
446
480
|
}
|
|
447
481
|
for (const [, p] of this._pendingRpc) p.reject(err);
|
|
448
482
|
this._pendingRpc.clear();
|
|
483
|
+
this.clearActiveTurn(err);
|
|
449
484
|
}
|
|
450
485
|
|
|
451
486
|
get sessionId(): string | null { return this._threadId; }
|
|
452
487
|
get state(): SessionState { return this._state; }
|
|
453
488
|
|
|
489
|
+
/** Start the identity latch before writing the leader `turn/start` request. */
|
|
490
|
+
private beginActiveTurn(): ActiveTurnReady {
|
|
491
|
+
let resolveFn!: (turnId: string | null) => void;
|
|
492
|
+
let rejectFn!: (err: Error) => void;
|
|
493
|
+
const promise = new Promise<string | null>((resolve, reject) => {
|
|
494
|
+
resolveFn = resolve;
|
|
495
|
+
rejectFn = reject;
|
|
496
|
+
});
|
|
497
|
+
// A turn can finish without anyone calling interrupt(). Keep a later
|
|
498
|
+
// process-exit rejection from becoming an unhandled promise rejection.
|
|
499
|
+
void promise.catch(() => {});
|
|
500
|
+
|
|
501
|
+
const ready: ActiveTurnReady = {
|
|
502
|
+
promise,
|
|
503
|
+
resolve: resolveFn,
|
|
504
|
+
reject: rejectFn,
|
|
505
|
+
settled: false,
|
|
506
|
+
};
|
|
507
|
+
this._activeTurnId = null;
|
|
508
|
+
this._activeTurnReady = ready;
|
|
509
|
+
this._interruptPromise = null;
|
|
510
|
+
this._turnTerminalObserved = false;
|
|
511
|
+
this._turnWasInterrupted = false;
|
|
512
|
+
return ready;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** First root turn id wins for the current latch. */
|
|
516
|
+
private captureActiveTurnId(turnId: string, expected?: ActiveTurnReady): void {
|
|
517
|
+
const ready = this._activeTurnReady;
|
|
518
|
+
if (!turnId || !ready || ready.settled) return;
|
|
519
|
+
if (expected && ready !== expected) return;
|
|
520
|
+
this._activeTurnId = turnId;
|
|
521
|
+
ready.settled = true;
|
|
522
|
+
ready.resolve(turnId);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private rejectActiveTurnReady(err: Error, expected: ActiveTurnReady): void {
|
|
526
|
+
if (this._activeTurnReady !== expected || expected.settled) return;
|
|
527
|
+
expected.settled = true;
|
|
528
|
+
expected.reject(err);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Clear the current turn and release an interrupt waiting for its id. */
|
|
532
|
+
private clearActiveTurn(err?: Error): void {
|
|
533
|
+
const ready = this._activeTurnReady;
|
|
534
|
+
if (ready && !ready.settled) {
|
|
535
|
+
ready.settled = true;
|
|
536
|
+
if (err) ready.reject(err);
|
|
537
|
+
else ready.resolve(null);
|
|
538
|
+
}
|
|
539
|
+
this._activeTurnId = null;
|
|
540
|
+
this._activeTurnReady = null;
|
|
541
|
+
this._interruptPromise = null;
|
|
542
|
+
this._turnTerminalObserved = false;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Mark root turn termination and release an interrupt still awaiting its id. */
|
|
546
|
+
private markTurnTerminalObserved(): void {
|
|
547
|
+
this._turnTerminalObserved = true;
|
|
548
|
+
const ready = this._activeTurnReady;
|
|
549
|
+
if (ready && !ready.settled) {
|
|
550
|
+
ready.settled = true;
|
|
551
|
+
ready.resolve(null);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
454
555
|
/**
|
|
455
556
|
* Durable identity for persistence + later `attachSession`. Null until Codex
|
|
456
557
|
* has assigned a thread id; serializes `{sessionId, cwd}` through the codec so
|
|
@@ -618,6 +719,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
618
719
|
// and pass through. If the second `turn/start` lands during the first
|
|
619
720
|
// turn, the per-turn accumulators continue collecting until the result
|
|
620
721
|
// event fires; the result then drains all pending resolvers.
|
|
722
|
+
const existingTurnReady = this._activeTurnReady;
|
|
723
|
+
const isTurnLeader = existingTurnReady === null;
|
|
724
|
+
const turnReady = existingTurnReady ?? this.beginActiveTurn();
|
|
725
|
+
|
|
621
726
|
if (this._state === "idle") {
|
|
622
727
|
this._state = "thinking";
|
|
623
728
|
this._turnStartedAt = new Date();
|
|
@@ -659,16 +764,34 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
659
764
|
// ProviderConfig.timeoutSec default when no per-call timeout is given.
|
|
660
765
|
this.armSendDeadline(entry, options);
|
|
661
766
|
|
|
662
|
-
this.rpcRequest("turn/start", turnParams)
|
|
663
|
-
|
|
664
|
-
|
|
767
|
+
const turnStart = this.rpcRequest("turn/start", turnParams);
|
|
768
|
+
if (isTurnLeader) {
|
|
769
|
+
void turnStart.then((response) => {
|
|
770
|
+
const turnId = str(asObj(response, "turn"), "id");
|
|
771
|
+
if (turnId) {
|
|
772
|
+
this.captureActiveTurnId(turnId, turnReady);
|
|
773
|
+
}
|
|
774
|
+
// Some app-server versions may omit the id from the response and send
|
|
775
|
+
// it only in turn/started. Keep the latch open for that notification.
|
|
776
|
+
}).catch((err: unknown) => {
|
|
777
|
+
this.rejectActiveTurnReady(
|
|
778
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
779
|
+
turnReady,
|
|
780
|
+
);
|
|
781
|
+
// Turn-level failures may also arrive via turn/failed notifications.
|
|
782
|
+
});
|
|
783
|
+
} else {
|
|
784
|
+
void turnStart.catch(() => {
|
|
785
|
+
// Turn-level failures arrive via turn/failed notifications.
|
|
786
|
+
});
|
|
787
|
+
}
|
|
665
788
|
|
|
666
789
|
return { uuid, result };
|
|
667
790
|
}
|
|
668
791
|
|
|
669
792
|
/**
|
|
670
793
|
* Wire up this send's timeout and/or abort signal. On fire, the active turn
|
|
671
|
-
* is
|
|
794
|
+
* is interrupted (`turn/interrupt`) and the send settles with `timeout` /
|
|
672
795
|
* `aborted`. No-op when neither a timeout nor a signal applies.
|
|
673
796
|
*/
|
|
674
797
|
private armSendDeadline(entry: PendingResult, options?: SendOptions): void {
|
|
@@ -711,7 +834,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
711
834
|
|
|
712
835
|
// Best-effort cancel of the active turn. With concurrent sends this ends
|
|
713
836
|
// the single shared turn for all of them — see SendOptions JSDoc.
|
|
714
|
-
void this.interrupt();
|
|
837
|
+
void this.interrupt().catch(() => {});
|
|
715
838
|
|
|
716
839
|
entry.resolve({
|
|
717
840
|
summary: null,
|
|
@@ -727,7 +850,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
727
850
|
|
|
728
851
|
async cancel(_uuid: string): Promise<CancelResult> {
|
|
729
852
|
// Codex's JSON-RPC protocol exposes no per-message cancel — only
|
|
730
|
-
// turn-wide `turn/
|
|
853
|
+
// turn-wide `turn/interrupt` (which is what `interrupt()` calls).
|
|
731
854
|
// capabilities.cancelQueuedMessage is false; this is a documented no-op.
|
|
732
855
|
return { cancelled: false };
|
|
733
856
|
}
|
|
@@ -780,11 +903,37 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
780
903
|
}
|
|
781
904
|
|
|
782
905
|
async interrupt(): Promise<void> {
|
|
783
|
-
if (this._state === "
|
|
906
|
+
if (this._state === "closed") return;
|
|
907
|
+
const ready = this._activeTurnReady;
|
|
908
|
+
if (!ready || this._turnTerminalObserved) return;
|
|
909
|
+
if (this._interruptPromise) return this._interruptPromise;
|
|
910
|
+
|
|
911
|
+
const threadId = this._threadId;
|
|
912
|
+
if (!threadId) {
|
|
913
|
+
throw new Error("Cannot interrupt Codex turn before the root thread id is known");
|
|
914
|
+
}
|
|
784
915
|
this._goals.notifyInterrupted(); // don't let an emulated goal auto-continue
|
|
916
|
+
|
|
917
|
+
const interruptPromise = (async () => {
|
|
918
|
+
const turnId = this._activeTurnId ?? await ready.promise;
|
|
919
|
+
// The turn may have completed while interrupt() was waiting for the
|
|
920
|
+
// leader turn/start response. In that race, completion is the success.
|
|
921
|
+
if (!turnId || this._activeTurnReady !== ready || this._turnTerminalObserved) return;
|
|
922
|
+
await this.rpcRequest("turn/interrupt", { threadId, turnId });
|
|
923
|
+
})();
|
|
924
|
+
this._interruptPromise = interruptPromise;
|
|
925
|
+
|
|
785
926
|
try {
|
|
786
|
-
await
|
|
787
|
-
} catch {
|
|
927
|
+
await interruptPromise;
|
|
928
|
+
} catch (err) {
|
|
929
|
+
// A rejected control request must reach the host instead of becoming a
|
|
930
|
+
// false successful Stop. Clear only this turn's failed attempt so a
|
|
931
|
+
// subsequent click can retry.
|
|
932
|
+
if (this._activeTurnReady === ready && this._interruptPromise === interruptPromise) {
|
|
933
|
+
this._interruptPromise = null;
|
|
934
|
+
}
|
|
935
|
+
throw err;
|
|
936
|
+
}
|
|
788
937
|
}
|
|
789
938
|
|
|
790
939
|
async drain(): Promise<void> {
|
|
@@ -804,6 +953,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
804
953
|
async close(): Promise<void> {
|
|
805
954
|
if (this._state === "closed") return;
|
|
806
955
|
this._state = "closed";
|
|
956
|
+
this.rejectAllPending(new Error("Codex session closed"));
|
|
807
957
|
|
|
808
958
|
this.proc.stdin!.end();
|
|
809
959
|
|
|
@@ -1003,6 +1153,201 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1003
1153
|
return !!threadId && !!rootThreadId && threadId !== rootThreadId;
|
|
1004
1154
|
}
|
|
1005
1155
|
|
|
1156
|
+
private backgroundTaskParentIdForPath(agentPath: string | null): string | null {
|
|
1157
|
+
if (!agentPath) return null;
|
|
1158
|
+
const separator = agentPath.lastIndexOf("/");
|
|
1159
|
+
if (separator <= 0) return null;
|
|
1160
|
+
return this._backgroundTaskIdsByPath.get(agentPath.slice(0, separator)) ?? null;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private agentMessageText(item: Record<string, unknown>): string | null {
|
|
1164
|
+
const direct = str(item, "text");
|
|
1165
|
+
if (direct) return direct;
|
|
1166
|
+
const content = Array.isArray(item["content"]) ? item["content"] : [];
|
|
1167
|
+
for (const entry of content) {
|
|
1168
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
1169
|
+
const block = entry as Record<string, unknown>;
|
|
1170
|
+
const text = str(block, "text");
|
|
1171
|
+
if (text && (str(block, "type") === "output_text" || str(block, "type") === "text")) {
|
|
1172
|
+
return text;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Maintain just enough child metadata to turn a later foreign-thread
|
|
1180
|
+
* terminal notification into one provider-neutral task event. This reducer
|
|
1181
|
+
* is deliberately separate from every root turn accumulator.
|
|
1182
|
+
*/
|
|
1183
|
+
private observeBackgroundTask(event: Extract<StreamEvent, { type: "background_task" }>): boolean {
|
|
1184
|
+
const previous = this._backgroundTasks.get(event.taskId);
|
|
1185
|
+
// Codex reports `subAgentActivity:interacted` after it forwards a child's
|
|
1186
|
+
// final answer to the parent. The authoritative child turn/completed can
|
|
1187
|
+
// arrive first, so suppress that late progress edge instead of resurrecting
|
|
1188
|
+
// a task that already reached a terminal state. A later child turn/started
|
|
1189
|
+
// explicitly reactivates the record below in handleForeignNotification.
|
|
1190
|
+
if (previous?.terminal) return false;
|
|
1191
|
+
|
|
1192
|
+
const description = event.description ?? previous?.description ?? null;
|
|
1193
|
+
const summary = event.summary ?? previous?.summary ?? null;
|
|
1194
|
+
const parentTaskId = event.parentTaskId
|
|
1195
|
+
?? previous?.parentTaskId
|
|
1196
|
+
?? this.backgroundTaskParentIdForPath(description);
|
|
1197
|
+
|
|
1198
|
+
event.description = description;
|
|
1199
|
+
event.summary = summary;
|
|
1200
|
+
event.parentTaskId = parentTaskId;
|
|
1201
|
+
|
|
1202
|
+
this._backgroundTasks.set(event.taskId, {
|
|
1203
|
+
taskId: event.taskId,
|
|
1204
|
+
description,
|
|
1205
|
+
summary,
|
|
1206
|
+
parentTaskId,
|
|
1207
|
+
terminal: event.phase === "completed",
|
|
1208
|
+
});
|
|
1209
|
+
if (description) this._backgroundTaskIdsByPath.set(description, event.taskId);
|
|
1210
|
+
return true;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* A Codex app-server connection also publishes child thread notifications.
|
|
1215
|
+
* They are useful only as background-task metadata. They must never flow
|
|
1216
|
+
* through root state, summary, usage, or `resolveTurn()`.
|
|
1217
|
+
*/
|
|
1218
|
+
private handleForeignNotification(
|
|
1219
|
+
method: string,
|
|
1220
|
+
params: Record<string, unknown>,
|
|
1221
|
+
rawLine: string,
|
|
1222
|
+
childThreadId: string,
|
|
1223
|
+
): void {
|
|
1224
|
+
if (method === "thread/started") {
|
|
1225
|
+
const thread = asObj(params, "thread");
|
|
1226
|
+
const parentThreadId = str(thread, "parentThreadId") || str(thread, "parent_thread_id");
|
|
1227
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1228
|
+
const parentTask = this._backgroundTasks.get(parentThreadId);
|
|
1229
|
+
// App-server can publish child thread/started before the corresponding
|
|
1230
|
+
// root subAgentActivity item. Register only descendants of this session,
|
|
1231
|
+
// not unrelated foreign threads multiplexed by a future server version.
|
|
1232
|
+
if (parentThreadId !== rootThreadId && !parentTask) return;
|
|
1233
|
+
|
|
1234
|
+
const source = asObj(thread, "source");
|
|
1235
|
+
const subAgent = Object.keys(asObj(source, "subAgent")).length > 0
|
|
1236
|
+
? asObj(source, "subAgent")
|
|
1237
|
+
: asObj(source, "subagent");
|
|
1238
|
+
const spawnSource = Object.keys(asObj(subAgent, "threadSpawn")).length > 0
|
|
1239
|
+
? asObj(subAgent, "threadSpawn")
|
|
1240
|
+
: asObj(subAgent, "thread_spawn");
|
|
1241
|
+
const description = str(spawnSource, "agentPath")
|
|
1242
|
+
|| str(spawnSource, "agent_path")
|
|
1243
|
+
|| str(thread, "name")
|
|
1244
|
+
|| str(thread, "agentNickname")
|
|
1245
|
+
|| str(thread, "agentRole")
|
|
1246
|
+
|| null;
|
|
1247
|
+
|
|
1248
|
+
this.dispatchEvent({
|
|
1249
|
+
type: "background_task",
|
|
1250
|
+
taskId: childThreadId,
|
|
1251
|
+
taskType: "subagent",
|
|
1252
|
+
phase: "started",
|
|
1253
|
+
status: "running",
|
|
1254
|
+
description,
|
|
1255
|
+
summary: null,
|
|
1256
|
+
parentTaskId: parentThreadId === rootThreadId ? null : parentThreadId,
|
|
1257
|
+
timestamp: new Date().toISOString(),
|
|
1258
|
+
providerType: "codex",
|
|
1259
|
+
sessionId: rootThreadId,
|
|
1260
|
+
messageId: null,
|
|
1261
|
+
eventId: rootThreadId
|
|
1262
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:started`
|
|
1263
|
+
: null,
|
|
1264
|
+
turnId: null,
|
|
1265
|
+
parentToolCallId: null,
|
|
1266
|
+
raw: parseJson(rawLine) ?? params,
|
|
1267
|
+
});
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const task = this._backgroundTasks.get(childThreadId);
|
|
1272
|
+
if (!task) return;
|
|
1273
|
+
|
|
1274
|
+
if (method === "turn/started") {
|
|
1275
|
+
if (!task.terminal) return;
|
|
1276
|
+
task.terminal = false;
|
|
1277
|
+
task.summary = null;
|
|
1278
|
+
const turn = asObj(params, "turn");
|
|
1279
|
+
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1280
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1281
|
+
this.dispatchEvent({
|
|
1282
|
+
type: "background_task",
|
|
1283
|
+
taskId: childThreadId,
|
|
1284
|
+
taskType: "subagent",
|
|
1285
|
+
phase: "progress",
|
|
1286
|
+
status: "running",
|
|
1287
|
+
description: task.description,
|
|
1288
|
+
summary: null,
|
|
1289
|
+
parentTaskId: task.parentTaskId,
|
|
1290
|
+
timestamp: new Date().toISOString(),
|
|
1291
|
+
providerType: "codex",
|
|
1292
|
+
sessionId: rootThreadId,
|
|
1293
|
+
messageId: null,
|
|
1294
|
+
eventId: rootThreadId && turnId
|
|
1295
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:${turnId}:progress`
|
|
1296
|
+
: null,
|
|
1297
|
+
turnId,
|
|
1298
|
+
parentToolCallId: null,
|
|
1299
|
+
raw: parseJson(rawLine) ?? params,
|
|
1300
|
+
});
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (method === "item/completed") {
|
|
1305
|
+
const item = asObj(params, "item");
|
|
1306
|
+
const itemType = str(item, "type");
|
|
1307
|
+
if ((itemType === "agentMessage" || itemType === "agent_message") && str(item, "phase") !== "commentary") {
|
|
1308
|
+
const summary = this.agentMessageText(item);
|
|
1309
|
+
if (summary) task.summary = summary;
|
|
1310
|
+
}
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (method !== "turn/completed" && method !== "turn/failed") return;
|
|
1315
|
+
|
|
1316
|
+
const turn = asObj(params, "turn");
|
|
1317
|
+
const turnId = str(turn, "id") || str(params, "turnId") || null;
|
|
1318
|
+
const nativeStatus = method === "turn/failed" ? "failed" : str(turn, "status");
|
|
1319
|
+
const status = nativeStatus === "failed"
|
|
1320
|
+
? "failed"
|
|
1321
|
+
: nativeStatus === "interrupted" || nativeStatus === "cancelled"
|
|
1322
|
+
? "stopped"
|
|
1323
|
+
: "completed";
|
|
1324
|
+
const errorMessage = str(asObj(turn, "error"), "message")
|
|
1325
|
+
|| str(params, "message")
|
|
1326
|
+
|| str(params, "error");
|
|
1327
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1328
|
+
|
|
1329
|
+
this.dispatchEvent({
|
|
1330
|
+
type: "background_task",
|
|
1331
|
+
taskId: childThreadId,
|
|
1332
|
+
taskType: "subagent",
|
|
1333
|
+
phase: "completed",
|
|
1334
|
+
status,
|
|
1335
|
+
description: task.description,
|
|
1336
|
+
summary: task.summary ?? (errorMessage || null),
|
|
1337
|
+
parentTaskId: task.parentTaskId,
|
|
1338
|
+
timestamp: new Date().toISOString(),
|
|
1339
|
+
providerType: "codex",
|
|
1340
|
+
sessionId: rootThreadId,
|
|
1341
|
+
messageId: null,
|
|
1342
|
+
eventId: rootThreadId && turnId
|
|
1343
|
+
? `codex:${rootThreadId}:background-task:${childThreadId}:${turnId}:completed`
|
|
1344
|
+
: null,
|
|
1345
|
+
turnId,
|
|
1346
|
+
parentToolCallId: null,
|
|
1347
|
+
raw: parseJson(rawLine) ?? params,
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1006
1351
|
private handleNotification(method: string, params: Record<string, unknown>, rawLine: string): void {
|
|
1007
1352
|
// codex/event — legacy wrapper
|
|
1008
1353
|
if (method === "codex/event") {
|
|
@@ -1020,7 +1365,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1020
1365
|
// its root thread, so foreign items must not change root state/summary and,
|
|
1021
1366
|
// most importantly, a child turn/completed must not resolve the root send.
|
|
1022
1367
|
const notificationThreadId = this.notificationThreadId(params);
|
|
1023
|
-
if (this.isForeignThread(notificationThreadId))
|
|
1368
|
+
if (this.isForeignThread(notificationThreadId)) {
|
|
1369
|
+
this.handleForeignNotification(method, params, rawLine, notificationThreadId!);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1024
1372
|
|
|
1025
1373
|
// Map v2 notification methods to processing
|
|
1026
1374
|
if (method === "thread/started") {
|
|
@@ -1030,6 +1378,13 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1030
1378
|
return;
|
|
1031
1379
|
}
|
|
1032
1380
|
|
|
1381
|
+
if (method === "turn/started") {
|
|
1382
|
+
this.captureActiveTurnId(str(asObj(params, "turn"), "id") || str(params, "turnId"));
|
|
1383
|
+
// The parser intentionally suppresses this lifecycle-only event.
|
|
1384
|
+
this.emitStreamEvent(rawLine);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1033
1388
|
if (method === "item/started") {
|
|
1034
1389
|
this._state = "tool_executing";
|
|
1035
1390
|
this.emitStreamEvent(rawLine);
|
|
@@ -1049,6 +1404,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1049
1404
|
}
|
|
1050
1405
|
|
|
1051
1406
|
if (method === "turn/failed") {
|
|
1407
|
+
this.markTurnTerminalObserved();
|
|
1052
1408
|
this._turnIsError = true;
|
|
1053
1409
|
this._turnErrorMessage = str(params, "message") || str(params, "error") || "Turn failed";
|
|
1054
1410
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -1111,6 +1467,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1111
1467
|
}
|
|
1112
1468
|
|
|
1113
1469
|
if (type === "turn.failed" || type === "error") {
|
|
1470
|
+
this.markTurnTerminalObserved();
|
|
1114
1471
|
this._turnIsError = true;
|
|
1115
1472
|
this._turnErrorMessage = str(event, "message") || str(event, "error") || "Turn failed";
|
|
1116
1473
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -1162,6 +1519,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1162
1519
|
}
|
|
1163
1520
|
|
|
1164
1521
|
private handleTurnCompleted(params: Record<string, unknown>): void {
|
|
1522
|
+
this.markTurnTerminalObserved();
|
|
1165
1523
|
const usage = typeof params["usage"] === "object" && params["usage"] !== null
|
|
1166
1524
|
? (params["usage"] as Record<string, unknown>)
|
|
1167
1525
|
: null;
|
|
@@ -1181,7 +1539,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1181
1539
|
// the error instead of a false "completed".
|
|
1182
1540
|
const turn = asObj(params, "turn");
|
|
1183
1541
|
const turnStatus = str(turn, "status");
|
|
1184
|
-
if (turnStatus === "
|
|
1542
|
+
if (turnStatus === "interrupted" || turnStatus === "cancelled") {
|
|
1543
|
+
this._turnWasInterrupted = true;
|
|
1544
|
+
} else if (turnStatus === "failed") {
|
|
1185
1545
|
this._turnIsError = true;
|
|
1186
1546
|
const msg = str(asObj(turn, "error"), "message");
|
|
1187
1547
|
this._turnErrorMessage = msg || this._turnErrorMessage || `Turn ${turnStatus}`;
|
|
@@ -1253,9 +1613,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1253
1613
|
summary: this._turnSummary,
|
|
1254
1614
|
usage,
|
|
1255
1615
|
costUsd: null,
|
|
1256
|
-
status: this._turnIsError ? "failed" : "completed",
|
|
1257
|
-
errorCode: this._turnIsError ? "execution_error" : null,
|
|
1258
|
-
errorMessage: this._turnErrorMessage,
|
|
1616
|
+
status: this._turnWasInterrupted ? "aborted" : this._turnIsError ? "failed" : "completed",
|
|
1617
|
+
errorCode: this._turnWasInterrupted ? "aborted" : this._turnIsError ? "execution_error" : null,
|
|
1618
|
+
errorMessage: this._turnWasInterrupted ? "Turn was interrupted" : this._turnErrorMessage,
|
|
1259
1619
|
};
|
|
1260
1620
|
|
|
1261
1621
|
// Drain pending onEvent handlers so callers awaiting send() see a settled
|
|
@@ -1283,8 +1643,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1283
1643
|
this._turnUsage = null;
|
|
1284
1644
|
this._turnModel = null;
|
|
1285
1645
|
this._turnIsError = false;
|
|
1646
|
+
this._turnWasInterrupted = false;
|
|
1286
1647
|
this._turnErrorMessage = null;
|
|
1287
1648
|
this._turnStartedAt = null;
|
|
1649
|
+
this.clearActiveTurn();
|
|
1288
1650
|
|
|
1289
1651
|
for (const p of pending) {
|
|
1290
1652
|
// Skip sends already settled early by timeout / abort.
|
|
@@ -1316,6 +1678,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1316
1678
|
* throwing handler does not break delivery of subsequent events.
|
|
1317
1679
|
*/
|
|
1318
1680
|
private dispatchEvent(event: StreamEvent): void {
|
|
1681
|
+
if (event.type === "background_task" && !this.observeBackgroundTask(event)) return;
|
|
1319
1682
|
// Track native goal_status transitions (keeps getGoal() accurate).
|
|
1320
1683
|
this._goals.observe(event);
|
|
1321
1684
|
const cb = this.ctx.onEvent;
|
package/src/types.ts
CHANGED
|
@@ -63,6 +63,13 @@ export interface ProviderCapabilities {
|
|
|
63
63
|
* process — the model is not involved.
|
|
64
64
|
*/
|
|
65
65
|
stopTask: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Provider emits first-class `background_task` StreamEvents for work that
|
|
68
|
+
* can outlive the root turn, such as async subagents and backgrounded
|
|
69
|
+
* processes. Absent/false means callers must not infer task lifecycle from
|
|
70
|
+
* provider-specific `unknown` events.
|
|
71
|
+
*/
|
|
72
|
+
backgroundTaskEvents?: boolean;
|
|
66
73
|
/**
|
|
67
74
|
* Provider exposes selectable operating modes via `listModes()` — e.g. Codex
|
|
68
75
|
* collaboration modes, Copilot's allow-all/agent/plan. When `false`,
|
|
@@ -985,6 +992,21 @@ export interface BaseStreamEventFields {
|
|
|
985
992
|
raw: Record<string, unknown>;
|
|
986
993
|
}
|
|
987
994
|
|
|
995
|
+
/** Provider-neutral category for work that may outlive its launching turn. */
|
|
996
|
+
export type BackgroundTaskType = "subagent" | "process" | "unknown";
|
|
997
|
+
|
|
998
|
+
/** Lifecycle edge represented by one `background_task` StreamEvent. */
|
|
999
|
+
export type BackgroundTaskPhase = "started" | "progress" | "completed";
|
|
1000
|
+
|
|
1001
|
+
/** Current normalized state carried by a `background_task` event. */
|
|
1002
|
+
export type BackgroundTaskStatus =
|
|
1003
|
+
| "pending"
|
|
1004
|
+
| "running"
|
|
1005
|
+
| "paused"
|
|
1006
|
+
| "completed"
|
|
1007
|
+
| "failed"
|
|
1008
|
+
| "stopped";
|
|
1009
|
+
|
|
988
1010
|
/**
|
|
989
1011
|
* Categorical reason for an `auth_required` event. Derived from the
|
|
990
1012
|
* provider's user-facing error text. Stable across providers — new
|
|
@@ -1176,6 +1198,26 @@ export type StreamEvent =
|
|
|
1176
1198
|
tokenBudget?: number;
|
|
1177
1199
|
iterations?: number;
|
|
1178
1200
|
} & BaseStreamEventFields)
|
|
1201
|
+
/**
|
|
1202
|
+
* Lifecycle for work that is independent from the root turn. In particular,
|
|
1203
|
+
* `phase: "completed"` settles only this task. It is never a root turn
|
|
1204
|
+
* result and must not be used to resolve `SendHandle.result`.
|
|
1205
|
+
*
|
|
1206
|
+
* Events are reducer-friendly snapshots. `status` is always populated,
|
|
1207
|
+
* while description/summary may be null when the provider did not report
|
|
1208
|
+
* them. `parentTaskId` identifies a nested background task when that lineage
|
|
1209
|
+
* is available.
|
|
1210
|
+
*/
|
|
1211
|
+
| ({
|
|
1212
|
+
type: "background_task";
|
|
1213
|
+
taskId: string;
|
|
1214
|
+
taskType: BackgroundTaskType;
|
|
1215
|
+
phase: BackgroundTaskPhase;
|
|
1216
|
+
status: BackgroundTaskStatus;
|
|
1217
|
+
description: string | null;
|
|
1218
|
+
summary: string | null;
|
|
1219
|
+
parentTaskId: string | null;
|
|
1220
|
+
} & BaseStreamEventFields)
|
|
1179
1221
|
| ({
|
|
1180
1222
|
type: "result";
|
|
1181
1223
|
text: string;
|