@agentex/agent 0.0.31 → 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 +63 -0
- package/README.md +30 -5
- 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/history.d.ts.map +1 -1
- package/dist/providers/codex/history.js +3 -1
- package/dist/providers/codex/history.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 +37 -5
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +56 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +400 -18
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.js +10 -1
- package/dist/providers/codex/transcript-normalize.js.map +1 -1
- package/dist/types.d.ts +35 -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/history.ts +3 -1
- package/src/providers/codex/index.ts +1 -0
- package/src/providers/codex/parse.ts +38 -5
- package/src/providers/codex/session.ts +431 -18
- package/src/providers/codex/transcript-normalize.ts +11 -1
- package/src/types.ts +48 -1
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -307,9 +323,16 @@ export { codexGoalCapability } from "./goal-capability.js";
|
|
|
307
323
|
|
|
308
324
|
export class CodexSessionImpl implements AgentSession {
|
|
309
325
|
private _state: SessionState = "idle";
|
|
326
|
+
/**
|
|
327
|
+
* The root thread represented by this AgentSession. Codex app-server also
|
|
328
|
+
* reports child-agent threads on the same stdout connection, so this id is
|
|
329
|
+
* pinned once discovered and must never be promoted to a child thread.
|
|
330
|
+
*/
|
|
310
331
|
private _threadId: string | null = null;
|
|
311
332
|
/** Thread id to resume (from ctx.sessionParams); null starts a fresh thread. */
|
|
312
333
|
private readonly _resumeThreadId: string | null;
|
|
334
|
+
/** Expected root during handshake, cleared when resume falls back to fresh. */
|
|
335
|
+
private _expectedThreadId: string | null;
|
|
313
336
|
private _lineBuffer = "";
|
|
314
337
|
private _nextId = 1;
|
|
315
338
|
|
|
@@ -335,6 +358,19 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
335
358
|
/** Shared promise so concurrent / repeated `drain()` calls coalesce. */
|
|
336
359
|
private _drainPromise: Promise<void> | null = null;
|
|
337
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
|
+
|
|
338
374
|
/** Stamps `tool_result.toolName` by correlating with prior `tool_call`s. */
|
|
339
375
|
private readonly _trackToolName = createToolNameTracker();
|
|
340
376
|
|
|
@@ -344,9 +380,14 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
344
380
|
private _turnUsage: { inputTokens: number; outputTokens: number } | null = null;
|
|
345
381
|
private _turnModel: string | null = null;
|
|
346
382
|
private _turnIsError = false;
|
|
383
|
+
private _turnWasInterrupted = false;
|
|
347
384
|
private _turnErrorMessage: string | null = null;
|
|
348
385
|
private _turnStartedAt: Date | null = null;
|
|
349
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
|
+
|
|
350
391
|
/**
|
|
351
392
|
* Serial dispatch chain for `onEvent`. Each dispatched event appends a
|
|
352
393
|
* handler invocation; the chain enforces in-order delivery and lets
|
|
@@ -366,6 +407,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
366
407
|
private readonly instructions: string | null,
|
|
367
408
|
) {
|
|
368
409
|
this._resumeThreadId = readCodexResumeId(ctx.sessionParams);
|
|
410
|
+
this._expectedThreadId = this._resumeThreadId;
|
|
369
411
|
|
|
370
412
|
this._goals = new GoalController({
|
|
371
413
|
providerType: "codex",
|
|
@@ -438,11 +480,78 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
438
480
|
}
|
|
439
481
|
for (const [, p] of this._pendingRpc) p.reject(err);
|
|
440
482
|
this._pendingRpc.clear();
|
|
483
|
+
this.clearActiveTurn(err);
|
|
441
484
|
}
|
|
442
485
|
|
|
443
486
|
get sessionId(): string | null { return this._threadId; }
|
|
444
487
|
get state(): SessionState { return this._state; }
|
|
445
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
|
+
|
|
446
555
|
/**
|
|
447
556
|
* Durable identity for persistence + later `attachSession`. Null until Codex
|
|
448
557
|
* has assigned a thread id; serializes `{sessionId, cwd}` through the codec so
|
|
@@ -527,12 +636,18 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
527
636
|
// thread/resume may echo the thread back or return {}; fall back to the
|
|
528
637
|
// id we resumed with so `sessionId` is always populated.
|
|
529
638
|
this._threadId = str(thread, "id") || str(thread, "sessionId") || this._resumeThreadId;
|
|
639
|
+
this._expectedThreadId = this._threadId;
|
|
530
640
|
// Rehydrate a durable Codex goal so getGoal() reflects it immediately
|
|
531
641
|
// (goals live in SQLite, not the transcript, so a resumed thread would
|
|
532
642
|
// otherwise report null until the next goal notification).
|
|
533
643
|
await this.hydrateGoalFromThread();
|
|
534
644
|
return;
|
|
535
645
|
} catch (err) {
|
|
646
|
+
// A failed resume can emit thread/started before its error response.
|
|
647
|
+
// Clear that provisional identity so the fresh thread's init event is
|
|
648
|
+
// accepted instead of being mistaken for a foreign child thread.
|
|
649
|
+
if (this._threadId === this._expectedThreadId) this._threadId = null;
|
|
650
|
+
this._expectedThreadId = null;
|
|
536
651
|
// The thread is unknown to this codex install (different machine, pruned
|
|
537
652
|
// history). Don't fail the whole session — fall back to a fresh thread
|
|
538
653
|
// and surface the downgrade on stderr. The new id flows back out via
|
|
@@ -586,6 +701,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
586
701
|
// codex-cli 0.130.0+ shape: { thread: { id, sessionId, ... }, model, ... }
|
|
587
702
|
const thread = asObj(res, "thread");
|
|
588
703
|
this._threadId = str(thread, "id") || str(thread, "sessionId") || null;
|
|
704
|
+
this._expectedThreadId = this._threadId;
|
|
589
705
|
}
|
|
590
706
|
|
|
591
707
|
// -------------------------------------------------------------------------
|
|
@@ -603,6 +719,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
603
719
|
// and pass through. If the second `turn/start` lands during the first
|
|
604
720
|
// turn, the per-turn accumulators continue collecting until the result
|
|
605
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
|
+
|
|
606
726
|
if (this._state === "idle") {
|
|
607
727
|
this._state = "thinking";
|
|
608
728
|
this._turnStartedAt = new Date();
|
|
@@ -644,16 +764,34 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
644
764
|
// ProviderConfig.timeoutSec default when no per-call timeout is given.
|
|
645
765
|
this.armSendDeadline(entry, options);
|
|
646
766
|
|
|
647
|
-
this.rpcRequest("turn/start", turnParams)
|
|
648
|
-
|
|
649
|
-
|
|
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
|
+
}
|
|
650
788
|
|
|
651
789
|
return { uuid, result };
|
|
652
790
|
}
|
|
653
791
|
|
|
654
792
|
/**
|
|
655
793
|
* Wire up this send's timeout and/or abort signal. On fire, the active turn
|
|
656
|
-
* is
|
|
794
|
+
* is interrupted (`turn/interrupt`) and the send settles with `timeout` /
|
|
657
795
|
* `aborted`. No-op when neither a timeout nor a signal applies.
|
|
658
796
|
*/
|
|
659
797
|
private armSendDeadline(entry: PendingResult, options?: SendOptions): void {
|
|
@@ -696,7 +834,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
696
834
|
|
|
697
835
|
// Best-effort cancel of the active turn. With concurrent sends this ends
|
|
698
836
|
// the single shared turn for all of them — see SendOptions JSDoc.
|
|
699
|
-
void this.interrupt();
|
|
837
|
+
void this.interrupt().catch(() => {});
|
|
700
838
|
|
|
701
839
|
entry.resolve({
|
|
702
840
|
summary: null,
|
|
@@ -712,7 +850,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
712
850
|
|
|
713
851
|
async cancel(_uuid: string): Promise<CancelResult> {
|
|
714
852
|
// Codex's JSON-RPC protocol exposes no per-message cancel — only
|
|
715
|
-
// turn-wide `turn/
|
|
853
|
+
// turn-wide `turn/interrupt` (which is what `interrupt()` calls).
|
|
716
854
|
// capabilities.cancelQueuedMessage is false; this is a documented no-op.
|
|
717
855
|
return { cancelled: false };
|
|
718
856
|
}
|
|
@@ -765,11 +903,37 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
765
903
|
}
|
|
766
904
|
|
|
767
905
|
async interrupt(): Promise<void> {
|
|
768
|
-
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
|
+
}
|
|
769
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
|
+
|
|
770
926
|
try {
|
|
771
|
-
await
|
|
772
|
-
} 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
|
+
}
|
|
773
937
|
}
|
|
774
938
|
|
|
775
939
|
async drain(): Promise<void> {
|
|
@@ -789,6 +953,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
789
953
|
async close(): Promise<void> {
|
|
790
954
|
if (this._state === "closed") return;
|
|
791
955
|
this._state = "closed";
|
|
956
|
+
this.rejectAllPending(new Error("Codex session closed"));
|
|
792
957
|
|
|
793
958
|
this.proc.stdin!.end();
|
|
794
959
|
|
|
@@ -971,6 +1136,218 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
971
1136
|
// Notification handling (v2 format)
|
|
972
1137
|
// -------------------------------------------------------------------------
|
|
973
1138
|
|
|
1139
|
+
/** Extract the thread scope carried by a v2 app-server notification. */
|
|
1140
|
+
private notificationThreadId(params: Record<string, unknown>): string | null {
|
|
1141
|
+
const thread = asObj(params, "thread");
|
|
1142
|
+
return str(params, "threadId") || str(thread, "id") || str(thread, "sessionId") || null;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Whether an explicitly-scoped event belongs to another app-server thread.
|
|
1147
|
+
* `_expectedThreadId` protects the resume handshake window before `_threadId`
|
|
1148
|
+
* has been populated and is cleared if resume falls back to a fresh thread.
|
|
1149
|
+
* Unscoped global notifications remain eligible.
|
|
1150
|
+
*/
|
|
1151
|
+
private isForeignThread(threadId: string | null): boolean {
|
|
1152
|
+
const rootThreadId = this._threadId ?? this._expectedThreadId;
|
|
1153
|
+
return !!threadId && !!rootThreadId && threadId !== rootThreadId;
|
|
1154
|
+
}
|
|
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
|
+
|
|
974
1351
|
private handleNotification(method: string, params: Record<string, unknown>, rawLine: string): void {
|
|
975
1352
|
// codex/event — legacy wrapper
|
|
976
1353
|
if (method === "codex/event") {
|
|
@@ -983,11 +1360,27 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
983
1360
|
return;
|
|
984
1361
|
}
|
|
985
1362
|
|
|
1363
|
+
// One Codex app-server connection multiplexes notifications for the root
|
|
1364
|
+
// thread and any child agents it spawns. An AgentSession represents only
|
|
1365
|
+
// its root thread, so foreign items must not change root state/summary and,
|
|
1366
|
+
// most importantly, a child turn/completed must not resolve the root send.
|
|
1367
|
+
const notificationThreadId = this.notificationThreadId(params);
|
|
1368
|
+
if (this.isForeignThread(notificationThreadId)) {
|
|
1369
|
+
this.handleForeignNotification(method, params, rawLine, notificationThreadId!);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
986
1373
|
// Map v2 notification methods to processing
|
|
987
1374
|
if (method === "thread/started") {
|
|
988
1375
|
// codex-cli 0.130.0+ shape: { thread: { id, sessionId, ... } }
|
|
989
|
-
|
|
990
|
-
this.
|
|
1376
|
+
if (!this._threadId) this._threadId = notificationThreadId;
|
|
1377
|
+
this.emitStreamEvent(rawLine);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
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.
|
|
991
1384
|
this.emitStreamEvent(rawLine);
|
|
992
1385
|
return;
|
|
993
1386
|
}
|
|
@@ -1011,6 +1404,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1011
1404
|
}
|
|
1012
1405
|
|
|
1013
1406
|
if (method === "turn/failed") {
|
|
1407
|
+
this.markTurnTerminalObserved();
|
|
1014
1408
|
this._turnIsError = true;
|
|
1015
1409
|
this._turnErrorMessage = str(params, "message") || str(params, "error") || "Turn failed";
|
|
1016
1410
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -1041,9 +1435,15 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1041
1435
|
|
|
1042
1436
|
private handleLegacyEvent(event: Record<string, unknown>, rawLine: string): void {
|
|
1043
1437
|
const type = str(event, "type");
|
|
1438
|
+
const eventThreadId =
|
|
1439
|
+
str(event, "thread_id") || str(event, "threadId") || str(event, "session_id") || null;
|
|
1440
|
+
|
|
1441
|
+
// Older NDJSON-shaped events can also carry explicit thread scope. Keep
|
|
1442
|
+
// the same root-only invariant when that scope is available.
|
|
1443
|
+
if (this.isForeignThread(eventThreadId)) return;
|
|
1044
1444
|
|
|
1045
1445
|
if (type === "thread.started") {
|
|
1046
|
-
this._threadId
|
|
1446
|
+
if (!this._threadId) this._threadId = eventThreadId;
|
|
1047
1447
|
this.emitStreamEvent(rawLine);
|
|
1048
1448
|
return;
|
|
1049
1449
|
}
|
|
@@ -1067,6 +1467,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1067
1467
|
}
|
|
1068
1468
|
|
|
1069
1469
|
if (type === "turn.failed" || type === "error") {
|
|
1470
|
+
this.markTurnTerminalObserved();
|
|
1070
1471
|
this._turnIsError = true;
|
|
1071
1472
|
this._turnErrorMessage = str(event, "message") || str(event, "error") || "Turn failed";
|
|
1072
1473
|
// Emit before resolve so the result event is queued onto _eventChain
|
|
@@ -1093,6 +1494,11 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1093
1494
|
const itemType = str(item, "type");
|
|
1094
1495
|
if (itemType !== "agent_message" && itemType !== "agentMessage") return;
|
|
1095
1496
|
|
|
1497
|
+
// Commentary is progress, not the terminal answer. Keep phase-absent
|
|
1498
|
+
// legacy events as a compatibility fallback, while known final_answer
|
|
1499
|
+
// items remain eligible for TurnResult.summary.
|
|
1500
|
+
if (str(item, "phase") === "commentary") return;
|
|
1501
|
+
|
|
1096
1502
|
// Direct text (Codex 0.30+)
|
|
1097
1503
|
const directText = str(item, "text");
|
|
1098
1504
|
if (directText) {
|
|
@@ -1113,6 +1519,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1113
1519
|
}
|
|
1114
1520
|
|
|
1115
1521
|
private handleTurnCompleted(params: Record<string, unknown>): void {
|
|
1522
|
+
this.markTurnTerminalObserved();
|
|
1116
1523
|
const usage = typeof params["usage"] === "object" && params["usage"] !== null
|
|
1117
1524
|
? (params["usage"] as Record<string, unknown>)
|
|
1118
1525
|
: null;
|
|
@@ -1132,7 +1539,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1132
1539
|
// the error instead of a false "completed".
|
|
1133
1540
|
const turn = asObj(params, "turn");
|
|
1134
1541
|
const turnStatus = str(turn, "status");
|
|
1135
|
-
if (turnStatus === "
|
|
1542
|
+
if (turnStatus === "interrupted" || turnStatus === "cancelled") {
|
|
1543
|
+
this._turnWasInterrupted = true;
|
|
1544
|
+
} else if (turnStatus === "failed") {
|
|
1136
1545
|
this._turnIsError = true;
|
|
1137
1546
|
const msg = str(asObj(turn, "error"), "message");
|
|
1138
1547
|
this._turnErrorMessage = msg || this._turnErrorMessage || `Turn ${turnStatus}`;
|
|
@@ -1204,9 +1613,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1204
1613
|
summary: this._turnSummary,
|
|
1205
1614
|
usage,
|
|
1206
1615
|
costUsd: null,
|
|
1207
|
-
status: this._turnIsError ? "failed" : "completed",
|
|
1208
|
-
errorCode: this._turnIsError ? "execution_error" : null,
|
|
1209
|
-
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,
|
|
1210
1619
|
};
|
|
1211
1620
|
|
|
1212
1621
|
// Drain pending onEvent handlers so callers awaiting send() see a settled
|
|
@@ -1234,8 +1643,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1234
1643
|
this._turnUsage = null;
|
|
1235
1644
|
this._turnModel = null;
|
|
1236
1645
|
this._turnIsError = false;
|
|
1646
|
+
this._turnWasInterrupted = false;
|
|
1237
1647
|
this._turnErrorMessage = null;
|
|
1238
1648
|
this._turnStartedAt = null;
|
|
1649
|
+
this.clearActiveTurn();
|
|
1239
1650
|
|
|
1240
1651
|
for (const p of pending) {
|
|
1241
1652
|
// Skip sends already settled early by timeout / abort.
|
|
@@ -1267,6 +1678,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1267
1678
|
* throwing handler does not break delivery of subsequent events.
|
|
1268
1679
|
*/
|
|
1269
1680
|
private dispatchEvent(event: StreamEvent): void {
|
|
1681
|
+
if (event.type === "background_task" && !this.observeBackgroundTask(event)) return;
|
|
1270
1682
|
// Track native goal_status transitions (keeps getGoal() accurate).
|
|
1271
1683
|
this._goals.observe(event);
|
|
1272
1684
|
const cb = this.ctx.onEvent;
|
|
@@ -1280,8 +1692,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1280
1692
|
// share an id; the last write wins. It also does NOT match the transcript
|
|
1281
1693
|
// reader's `codex:<sessionId>:<offset>` scheme (different wire vocabulary
|
|
1282
1694
|
// on disk) — cross-shape dedup remains a host concern.
|
|
1283
|
-
|
|
1284
|
-
|
|
1695
|
+
const eventThreadId = event.sessionId ?? this._threadId;
|
|
1696
|
+
if (!event.eventId && eventThreadId && event.turnId && event.messageId) {
|
|
1697
|
+
event.eventId = `codex:${eventThreadId}:${event.turnId}:${event.messageId}:${event.type}`;
|
|
1285
1698
|
}
|
|
1286
1699
|
// Enrich synchronously (in stream order) so tool_result events carry the
|
|
1287
1700
|
// name of the tool_call they answer.
|
|
@@ -61,7 +61,13 @@ function mapLine(line: CodexTranscriptLine, sessionId: string | null): StreamEve
|
|
|
61
61
|
// Only assistant messages surface; developer/user messages are
|
|
62
62
|
// system-prompt material we don't replay.
|
|
63
63
|
if (payload["role"] !== "assistant") return [];
|
|
64
|
-
|
|
64
|
+
const phase = messagePhase(payload["phase"]);
|
|
65
|
+
return [{
|
|
66
|
+
type: "assistant",
|
|
67
|
+
text: extractMessageText(payload["content"]) ?? "",
|
|
68
|
+
...(phase ? { phase } : {}),
|
|
69
|
+
...base,
|
|
70
|
+
}];
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
if (innerType === "reasoning") {
|
|
@@ -129,6 +135,10 @@ function str(v: unknown): string | null {
|
|
|
129
135
|
return typeof v === "string" && v.length > 0 ? v : null;
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
function messagePhase(v: unknown): "commentary" | "final_answer" | undefined {
|
|
139
|
+
return v === "commentary" || v === "final_answer" ? v : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
/**
|
|
133
143
|
* `response_item/message.content` is an array of typed parts (`output_text`
|
|
134
144
|
* for assistant replies). Concat the text parts with a blank-line separator.
|