@workerdeck/core 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/build/index.d.mts +200 -13
- package/build/index.mjs +1044 -111
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
package/build/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
-
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, replayCoalesceKey, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
|
|
4
|
+
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, contextReading, imagePartRef, replayCoalesceKey, replayRetains, snapshotRetains, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
|
|
5
5
|
import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
|
-
import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { createVfs, runScript } from "@workerdeck/sandbox";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { lookup } from "node:dns/promises";
|
|
@@ -593,6 +593,496 @@ function staleReplaySeqs(events, afterSeq) {
|
|
|
593
593
|
}
|
|
594
594
|
return stale;
|
|
595
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* The one replay body, and what a socket receives from it.
|
|
598
|
+
*
|
|
599
|
+
* Every runner had a byte-identical copy of this loop — three spellings of four
|
|
600
|
+
* rules, one of which ("never drop the highest-seq event, whatever the rule
|
|
601
|
+
* says") is load-bearing and was three copies of a comment. Not a base class:
|
|
602
|
+
* the runners share nothing else, and a base class would have to own `#emit`,
|
|
603
|
+
* the most engine-specific method each of them has.
|
|
604
|
+
*
|
|
605
|
+
* The rules, in the order they are applied:
|
|
606
|
+
*
|
|
607
|
+
* 1. `afterSeq` — the caller already holds everything at or below it.
|
|
608
|
+
* 2. `resetSeq` — transcript *content* strictly below the latest
|
|
609
|
+
* `conversation_reset` is skipped, so a re-attach cannot resurrect a cleared
|
|
610
|
+
* conversation while state events still replay. Claude's alone; the other
|
|
611
|
+
* engines pass 0.
|
|
612
|
+
* 3. `coalesceReplay` — last-write-wins state readings superseded later in the
|
|
613
|
+
* same replay (`staleReplaySeqs`), plus everything `replayRetains` says the
|
|
614
|
+
* reducer reads and discards. Opt-in, and only sound for a consumer whose
|
|
615
|
+
* handling of those events is last-write-wins.
|
|
616
|
+
* 4. `imageRefs` — a base64 image part is delivered as an `image_ref` address
|
|
617
|
+
* (protocol's {@link imagePartRef}), its bytes one REST fetch away. Applied
|
|
618
|
+
* **before** rule 5, because it stamps indices from the stored part array
|
|
619
|
+
* which rule 5 then reshapes. Unlike rule 5 this also applies to the live
|
|
620
|
+
* path (see `SubscriberSet`), which is the one place these two rules differ.
|
|
621
|
+
* 5. `truncateResults` — a huge `tool_result` block is delivered as its head
|
|
622
|
+
* plus the markers that say so. **Never mutates the stored event**: the live
|
|
623
|
+
* path, the parking snapshot and the fetch route all need the whole thing,
|
|
624
|
+
* so this builds a copy and the log stays the log.
|
|
625
|
+
*
|
|
626
|
+
* The highest-seq event is delivered whatever rules 2 and 3 say — a client's
|
|
627
|
+
* replay hold waits for `state.lastSeq` to reach the attach's and would
|
|
628
|
+
* otherwise hang forever — but it is still *truncated* when rule 4 applies. A
|
|
629
|
+
* session that ends on a `find /` puts its 641 KB frame exactly there.
|
|
630
|
+
*/
|
|
631
|
+
function replaySlice(events, options) {
|
|
632
|
+
const { afterSeq, resetSeq = 0, coalesceReplay, truncateResults, imageRefs } = options;
|
|
633
|
+
const stale = coalesceReplay ? staleReplaySeqs(events, afterSeq) : void 0;
|
|
634
|
+
const lastSeq = events[events.length - 1]?.seq ?? 0;
|
|
635
|
+
const out = [];
|
|
636
|
+
for (const event of events) {
|
|
637
|
+
if (event.seq <= afterSeq) continue;
|
|
638
|
+
if (event.seq < resetSeq && transcriptContent(event)) continue;
|
|
639
|
+
if (stale?.has(event.seq)) continue;
|
|
640
|
+
if (coalesceReplay && event.seq !== lastSeq && !replayRetains(event)) continue;
|
|
641
|
+
let delivered = event;
|
|
642
|
+
if (imageRefs) delivered = refImageParts(delivered);
|
|
643
|
+
if (truncateResults) delivered = truncateResultBlocks(delivered);
|
|
644
|
+
out.push(delivered);
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* A copy of `event` whose oversized `tool_result` blocks carry their head and
|
|
650
|
+
* say so — or `event` itself, unchanged and un-copied, when nothing is over the
|
|
651
|
+
* budget. That identity matters: an attach is mostly small events, and a fresh
|
|
652
|
+
* object for every one of them would cost more than the feature saves.
|
|
653
|
+
*
|
|
654
|
+
* Blocks are measured and cut **individually**. A message answering three calls
|
|
655
|
+
* where one is a `find /` keeps the two small results whole, which is what makes
|
|
656
|
+
* the per-block marker (rather than a per-event one) honest.
|
|
657
|
+
*/
|
|
658
|
+
function truncateResultBlocks(event) {
|
|
659
|
+
if (event.type !== "user_message") return event;
|
|
660
|
+
const content = event.message.content;
|
|
661
|
+
if (!Array.isArray(content)) return event;
|
|
662
|
+
let cut = false;
|
|
663
|
+
const blocks = content.map((block) => {
|
|
664
|
+
if (block.type !== "tool_result") return block;
|
|
665
|
+
const result = block;
|
|
666
|
+
if (result.truncated) return block;
|
|
667
|
+
const total = resultChars(result.content);
|
|
668
|
+
if (total <= TOOL_RESULT_HEAD_CHARS) return block;
|
|
669
|
+
cut = true;
|
|
670
|
+
return {
|
|
671
|
+
...result,
|
|
672
|
+
content: headOf(result.content, TOOL_RESULT_HEAD_CHARS),
|
|
673
|
+
truncated: true,
|
|
674
|
+
total_chars: total
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
if (!cut) return event;
|
|
678
|
+
return {
|
|
679
|
+
...event,
|
|
680
|
+
message: {
|
|
681
|
+
...event.message,
|
|
682
|
+
content: blocks
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
/** Characters in a result's content, in the same terms a reader sees it: the
|
|
687
|
+
* string itself, or every text part of a block list joined by newlines — which
|
|
688
|
+
* is exactly what `blockText` in the reducer builds. Non-text parts (an image
|
|
689
|
+
* block) contribute nothing, because they are not what is large here and
|
|
690
|
+
* slicing them would corrupt them. */
|
|
691
|
+
function resultChars(content) {
|
|
692
|
+
if (typeof content === "string") return content.length;
|
|
693
|
+
if (!Array.isArray(content)) return 0;
|
|
694
|
+
return content.reduce((total, part, index) => total + (typeof part.text === "string" ? part.text.length + (index > 0 ? 1 : 0) : 0), 0);
|
|
695
|
+
}
|
|
696
|
+
/** The first `chars` characters, in the content's own shape — a string stays a
|
|
697
|
+
* string, a block list stays a block list (cut at the part that crosses the
|
|
698
|
+
* budget, with the remaining parts dropped). Shape-preserving on purpose: the
|
|
699
|
+
* reducer, both renderers and the copy button all read this the same way they
|
|
700
|
+
* read a whole one, so truncation is a shorter result and never a different
|
|
701
|
+
* kind of one. */
|
|
702
|
+
function headOf(content, chars) {
|
|
703
|
+
if (typeof content === "string") return content.slice(0, chars);
|
|
704
|
+
if (!Array.isArray(content)) return content;
|
|
705
|
+
const parts = [];
|
|
706
|
+
let used = 0;
|
|
707
|
+
for (const part of content) {
|
|
708
|
+
if (part.type === "image_ref") {
|
|
709
|
+
parts.push(part);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (typeof part.text !== "string") continue;
|
|
713
|
+
if (used >= chars) continue;
|
|
714
|
+
const text = part.text.slice(0, chars - used);
|
|
715
|
+
parts.push({
|
|
716
|
+
...part,
|
|
717
|
+
text
|
|
718
|
+
});
|
|
719
|
+
used += text.length + 1;
|
|
720
|
+
}
|
|
721
|
+
return parts;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* A copy of `event` whose `tool_result` blocks carry `image_ref` addresses in
|
|
725
|
+
* place of their base64 image parts — or `event` itself, unchanged and
|
|
726
|
+
* un-copied, when it holds none. Same identity rule as
|
|
727
|
+
* {@link truncateResultBlocks}, and it matters more here: an event carrying an
|
|
728
|
+
* image at all is the exception, so the common path must not allocate.
|
|
729
|
+
*
|
|
730
|
+
* **Never mutates the stored event.** The log is what the parking snapshot
|
|
731
|
+
* embeds, what `Runner.eventAt` reads, and therefore what the fetch route
|
|
732
|
+
* serves the bytes back from — a drop that reached the log would 404 the very
|
|
733
|
+
* lazy-load this rule promises.
|
|
734
|
+
*
|
|
735
|
+
* Indices are stamped from the **stored** array, which is why this runs *before*
|
|
736
|
+
* truncation rather than after: `headOf` reshapes a block's parts, so an address
|
|
737
|
+
* computed on its output would name the wrong part of the stored block. That
|
|
738
|
+
* ordering is asserted in `replay-image-ref.test.ts`, not merely intended.
|
|
739
|
+
*/
|
|
740
|
+
function refImageParts(event) {
|
|
741
|
+
if (event.type !== "user_message") return event;
|
|
742
|
+
const content = event.message.content;
|
|
743
|
+
if (!Array.isArray(content)) return event;
|
|
744
|
+
let changed = false;
|
|
745
|
+
const blocks = content.map((block) => {
|
|
746
|
+
if (block.type !== "tool_result") return block;
|
|
747
|
+
const result = block;
|
|
748
|
+
const parts = result.content;
|
|
749
|
+
if (!Array.isArray(parts)) return block;
|
|
750
|
+
let blockChanged = false;
|
|
751
|
+
const mapped = parts.map((part, index) => {
|
|
752
|
+
const ref = imagePartRef(part, index);
|
|
753
|
+
if (!ref) return part;
|
|
754
|
+
blockChanged = true;
|
|
755
|
+
return ref;
|
|
756
|
+
});
|
|
757
|
+
if (!blockChanged) return block;
|
|
758
|
+
changed = true;
|
|
759
|
+
return {
|
|
760
|
+
...result,
|
|
761
|
+
content: mapped
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
if (!changed) return event;
|
|
765
|
+
return {
|
|
766
|
+
...event,
|
|
767
|
+
message: {
|
|
768
|
+
...event.message,
|
|
769
|
+
content: blocks
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/lib/subscribers.ts
|
|
775
|
+
var SubscriberSet = class {
|
|
776
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
777
|
+
/**
|
|
778
|
+
* Replay `events` to `listener` under `options`, then hold it for live
|
|
779
|
+
* delivery. Returns the unsubscribe.
|
|
780
|
+
*
|
|
781
|
+
* The replay runs *before* the listener joins the set, which is the ordering
|
|
782
|
+
* every runner already had and is load-bearing: joining first would deliver a
|
|
783
|
+
* live event emitted mid-replay ahead of the buffered events preceding it.
|
|
784
|
+
*/
|
|
785
|
+
subscribe(events, listener, afterSeq = 0, options, resetSeq = 0) {
|
|
786
|
+
const asked = options ?? {};
|
|
787
|
+
for (const event of replaySlice(events, {
|
|
788
|
+
...asked,
|
|
789
|
+
afterSeq,
|
|
790
|
+
resetSeq
|
|
791
|
+
})) listener(event);
|
|
792
|
+
this.#listeners.set(listener, asked);
|
|
793
|
+
return () => {
|
|
794
|
+
this.#listeners.delete(listener);
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
/** Drop every subscriber — a park, which ends the session's live stream. */
|
|
798
|
+
clear() {
|
|
799
|
+
this.#listeners.clear();
|
|
800
|
+
}
|
|
801
|
+
/** Fan one event out, transformed per subscriber. */
|
|
802
|
+
emit(event) {
|
|
803
|
+
for (const [listener, asked] of this.#listeners) try {
|
|
804
|
+
listener(asked.imageRefs ? refImageParts(event) : event);
|
|
805
|
+
} catch {}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/engines/claude/subagents.ts
|
|
810
|
+
/**
|
|
811
|
+
* The rollup behind `SessionInfo.subagents` — what a sessions list (which never
|
|
812
|
+
* attaches) can know about the sub-agents running inside a session. Fed from
|
|
813
|
+
* `SessionRunner.#emit`, the one chokepoint every event passes through, so the
|
|
814
|
+
* resume backfill — which replays history through the same path — reconstructs
|
|
815
|
+
* it with no persistence of its own. Grouping is by Task id throughout, never
|
|
816
|
+
* adjacency: parallel sub-agents interleave in the stream, the same fact that
|
|
817
|
+
* broke the terminal theme's positional row model.
|
|
818
|
+
*
|
|
819
|
+
* Three decisions live here rather than in the protocol doc:
|
|
820
|
+
*
|
|
821
|
+
* **What counts as a spawn.** A record opens when a *top-level* assistant
|
|
822
|
+
* message carries a `tool_use` named `Task` or `Agent` — the moment the
|
|
823
|
+
* sub-agent exists, so a just-spawned agent is visible before its first nested
|
|
824
|
+
* event, with the block's input in hand for its labels. Both names are observed
|
|
825
|
+
* SDK spellings (`Task` synchronous, `Agent` async), and the name is a
|
|
826
|
+
* convention, not a law — a session that spawned three `Agent`s under a tracker
|
|
827
|
+
* that only knew `Task` reported all three as label-less failures. So three
|
|
828
|
+
* more openers back the allowlist up: the CLI's own `task_started` system event
|
|
829
|
+
* (which positively names the `tool_use_id` an agent runs under, with the brief
|
|
830
|
+
* as labels), the launch acknowledgement (below), and — as before — any nested
|
|
831
|
+
* event whose `parentToolUseId` has no record: an id that events demonstrably
|
|
832
|
+
* nest under *is* a sub-agent, whatever the spawning call was named. A fallback
|
|
833
|
+
* record never saw an input, so it stays label-less until a named signal fills
|
|
834
|
+
* it in rather than resetting an accumulated count.
|
|
835
|
+
*
|
|
836
|
+
* **A background agent's `tool_result` is a launch receipt, not a verdict.**
|
|
837
|
+
* An async agent's spawn call resolves seconds after the spawn with "Async
|
|
838
|
+
* agent launched successfully. (This tool result is internal metadata …)" —
|
|
839
|
+
* long before the agent has done anything — and its actual outcome travels on
|
|
840
|
+
* a `task_notification` system event instead (`status: 'completed'` is `done`,
|
|
841
|
+
* any other way of stopping is `failed`: the report the notification exists to
|
|
842
|
+
* deliver never came). Settling on the receipt would read "0 of 3 agents
|
|
843
|
+
* running" while three agents burn tokens, so a non-error result on a record
|
|
844
|
+
* known to be background never settles it. Known how: the `task_started` event
|
|
845
|
+
* live, or the receipt's own wrapper text on a resume — the stored transcript
|
|
846
|
+
* carries none of the CLI's system events, so, exactly as
|
|
847
|
+
* `isSyntheticUserText` documents for the `<task-notification>` blob, the text
|
|
848
|
+
* is the only signal the replayed path has.
|
|
849
|
+
*
|
|
850
|
+
* **What an interrupted turn leaves behind.** A Task whose `tool_result` never
|
|
851
|
+
* arrives — interrupt, session error, a turn or budget cap — would otherwise
|
|
852
|
+
* read `running` on an idle session forever, a lie a list re-renders at every
|
|
853
|
+
* poll. So the end of a turn settles every still-running record as `failed`:
|
|
854
|
+
* the report never came, which is the one thing `done` could have claimed. The
|
|
855
|
+
* sweep keys on `turn_result`, on the status coming to rest (`idle` — which is
|
|
856
|
+
* how a resumed history that ends mid-Task settles, since the backfill replays
|
|
857
|
+
* no `turn_result` — or a terminal state), and on the session closing. A real
|
|
858
|
+
* verdict arriving anyway outranks the sweep's inference. The sweep's premise
|
|
859
|
+
* — "the turn ended, so anything still running was cut off" — is false for a
|
|
860
|
+
* background agent, which is *designed* to outlive its turn: the real session
|
|
861
|
+
* behind this file ended three turns while its agents ran, and every
|
|
862
|
+
* `turn_result` re-branded live, working agents as failures. So the turn and
|
|
863
|
+
* idle sweeps spare a record marked background by a **live** signal. They do
|
|
864
|
+
* not spare one whose only evidence is replayed: the backfill describes a
|
|
865
|
+
* process that is gone, and a background agent the old process died inside can
|
|
866
|
+
* never notify — `running` would be the forever-lie again. `session_closed`
|
|
867
|
+
* and the terminal statuses settle everything, background included, for the
|
|
868
|
+
* same reason: the process hosting those agents is gone.
|
|
869
|
+
*/
|
|
870
|
+
var SubagentTracker = class {
|
|
871
|
+
#records = /* @__PURE__ */ new Map();
|
|
872
|
+
#settleCounter = 0;
|
|
873
|
+
/** Fold one emitted event body into the rollup, in log order. */
|
|
874
|
+
observe(body, ts) {
|
|
875
|
+
switch (body.type) {
|
|
876
|
+
case "assistant_message":
|
|
877
|
+
if (body.parentToolUseId != null) {
|
|
878
|
+
const record = this.#recordFor(body.parentToolUseId, ts);
|
|
879
|
+
record.toolCount += toolUseBlocks(body.message.content).length;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
for (const block of toolUseBlocks(body.message.content)) {
|
|
883
|
+
if (!SPAWNER_NAMES.has(block.name)) continue;
|
|
884
|
+
this.#open(block, ts);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
case "user_message": {
|
|
888
|
+
if (body.parentToolUseId != null) {
|
|
889
|
+
this.#recordFor(body.parentToolUseId, ts);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const note = parseTaskNotification(firstText(body.message.content));
|
|
893
|
+
if (note) {
|
|
894
|
+
const record = this.#recordFor(note.toolUseId, ts);
|
|
895
|
+
const status = note.status === "completed" ? "done" : "failed";
|
|
896
|
+
if (record.status !== status) this.#settle(record, status);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const content = body.message.content;
|
|
900
|
+
if (typeof content === "string") return;
|
|
901
|
+
for (const block of content) {
|
|
902
|
+
if (block.type !== "tool_result") continue;
|
|
903
|
+
const result = block;
|
|
904
|
+
if (typeof result.tool_use_id !== "string") continue;
|
|
905
|
+
if (result.is_error !== true && isLaunchAck(result.content)) {
|
|
906
|
+
const record = this.#recordFor(result.tool_use_id, ts);
|
|
907
|
+
if (record.background !== "live") record.background = body.replay === true ? "replay" : "live";
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const record = this.#records.get(result.tool_use_id);
|
|
911
|
+
if (!record) continue;
|
|
912
|
+
if (result.is_error !== true && record.background !== void 0) continue;
|
|
913
|
+
const status = result.is_error === true ? "failed" : "done";
|
|
914
|
+
if (record.status === status) continue;
|
|
915
|
+
this.#settle(record, status);
|
|
916
|
+
}
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
case "sdk_event": {
|
|
920
|
+
const p = body.payload;
|
|
921
|
+
if (p.type !== "system" || typeof p.tool_use_id !== "string") return;
|
|
922
|
+
if (p.subtype === "task_started") {
|
|
923
|
+
const record = this.#recordFor(p.tool_use_id, ts);
|
|
924
|
+
record.background = "live";
|
|
925
|
+
record.agentType ??= cleaned(p.subagent_type);
|
|
926
|
+
record.description ??= cleaned(p.description);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (p.subtype === "task_notification") {
|
|
930
|
+
const record = this.#recordFor(p.tool_use_id, ts);
|
|
931
|
+
const status = p.status === "completed" ? "done" : "failed";
|
|
932
|
+
if (record.status !== status) this.#settle(record, status);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
case "turn_result":
|
|
938
|
+
this.#sweep(false);
|
|
939
|
+
return;
|
|
940
|
+
case "session_closed":
|
|
941
|
+
this.#sweep(true);
|
|
942
|
+
return;
|
|
943
|
+
case "status_changed":
|
|
944
|
+
if (body.status === "idle") this.#sweep(false);
|
|
945
|
+
else if (body.status === "failed" || body.status === "closed") this.#sweep(true);
|
|
946
|
+
return;
|
|
947
|
+
case "conversation_reset":
|
|
948
|
+
this.#records.clear();
|
|
949
|
+
return;
|
|
950
|
+
default: return;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* The rollup as `SessionInfo.subagents` serves it: spawn order (the
|
|
955
|
+
* transcript's own), fresh objects, and `undefined` when there is nothing to
|
|
956
|
+
* say — absent and empty mean the same thing to a client, and an empty array
|
|
957
|
+
* on every row of a 1.2s-polled list is bytes spent saying nothing.
|
|
958
|
+
*/
|
|
959
|
+
list() {
|
|
960
|
+
if (this.#records.size === 0) return void 0;
|
|
961
|
+
const out = [];
|
|
962
|
+
for (const r of this.#records.values()) out.push({
|
|
963
|
+
toolUseId: r.toolUseId,
|
|
964
|
+
agentType: r.agentType,
|
|
965
|
+
description: r.description,
|
|
966
|
+
status: r.status,
|
|
967
|
+
startedAt: r.startedAt,
|
|
968
|
+
toolCount: r.toolCount
|
|
969
|
+
});
|
|
970
|
+
return out;
|
|
971
|
+
}
|
|
972
|
+
#recordFor(toolUseId, ts) {
|
|
973
|
+
let record = this.#records.get(toolUseId);
|
|
974
|
+
if (!record) {
|
|
975
|
+
record = {
|
|
976
|
+
toolUseId,
|
|
977
|
+
status: "running",
|
|
978
|
+
startedAt: ts,
|
|
979
|
+
toolCount: 0
|
|
980
|
+
};
|
|
981
|
+
this.#records.set(toolUseId, record);
|
|
982
|
+
}
|
|
983
|
+
return record;
|
|
984
|
+
}
|
|
985
|
+
#open(block, ts) {
|
|
986
|
+
const record = this.#recordFor(block.id, ts);
|
|
987
|
+
const input = block.input;
|
|
988
|
+
record.agentType ??= cleaned(input?.subagent_type);
|
|
989
|
+
record.description ??= cleaned(input?.description);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* End of turn (`final: false`): anything still running was cut off before
|
|
993
|
+
* its report — except a background agent the live process still hosts, which
|
|
994
|
+
* is designed to outlive the turn and settles by notification instead. End
|
|
995
|
+
* of session (`final: true`): everything, background included, because the
|
|
996
|
+
* process those agents lived in is gone.
|
|
997
|
+
*/
|
|
998
|
+
#sweep(final) {
|
|
999
|
+
for (const record of this.#records.values()) {
|
|
1000
|
+
if (record.status !== "running") continue;
|
|
1001
|
+
if (!final && record.background === "live") continue;
|
|
1002
|
+
this.#settle(record, "failed");
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
#settle(record, status) {
|
|
1006
|
+
record.status = status;
|
|
1007
|
+
record.settledOrder = ++this.#settleCounter;
|
|
1008
|
+
let settled = 0;
|
|
1009
|
+
for (const r of this.#records.values()) if (r.settledOrder !== void 0) settled++;
|
|
1010
|
+
while (settled > SUBAGENT_HISTORY) {
|
|
1011
|
+
let oldestId;
|
|
1012
|
+
let oldestOrder = Infinity;
|
|
1013
|
+
for (const r of this.#records.values()) {
|
|
1014
|
+
if (r.settledOrder === void 0 || r.settledOrder >= oldestOrder) continue;
|
|
1015
|
+
oldestId = r.toolUseId;
|
|
1016
|
+
oldestOrder = r.settledOrder;
|
|
1017
|
+
}
|
|
1018
|
+
if (oldestId === void 0) break;
|
|
1019
|
+
this.#records.delete(oldestId);
|
|
1020
|
+
settled--;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
/** The spawner names observed in the wild: `Task` runs the agent inside the
|
|
1025
|
+
* turn, `Agent` launches it in the background. Deliberately just these two —
|
|
1026
|
+
* a third spelling is caught by `task_started`, the launch receipt, or the
|
|
1027
|
+
* nested-event fallback, so widening this to every tool would only turn
|
|
1028
|
+
* ordinary calls into phantom agents. */
|
|
1029
|
+
const SPAWNER_NAMES = new Set(["Task", "Agent"]);
|
|
1030
|
+
/** The async spawn's immediate `tool_result` — "Async agent launched
|
|
1031
|
+
* successfully. (This tool result is internal metadata …)" — recognized by its
|
|
1032
|
+
* wrapper text because on a resume that text is the only signal there is (the
|
|
1033
|
+
* `SYNTHETIC_USER_PREFIXES` argument; the CLI's system events are not stored).
|
|
1034
|
+
* Live, `task_started` marks the record first and this is redundant armor. */
|
|
1035
|
+
const isLaunchAck = (content) => {
|
|
1036
|
+
const text = typeof content === "string" ? content : firstText(Array.isArray(content) ? content : []);
|
|
1037
|
+
return typeof text === "string" && text.trimStart().startsWith("Async agent launched");
|
|
1038
|
+
};
|
|
1039
|
+
/** A background agent stopping, parsed from the `<task-notification>` wrapper
|
|
1040
|
+
* the CLI writes into the transcript. Field-tolerant on purpose: only the
|
|
1041
|
+
* `tool-use-id` (this rollup's key) and the `status` verdict are read. */
|
|
1042
|
+
const parseTaskNotification = (text) => {
|
|
1043
|
+
if (text === void 0 || !text.trimStart().startsWith("<task-notification>")) return void 0;
|
|
1044
|
+
const toolUseId = /<tool-use-id>\s*([^<\s]+)\s*<\/tool-use-id>/.exec(text)?.[1];
|
|
1045
|
+
if (toolUseId === void 0) return void 0;
|
|
1046
|
+
return {
|
|
1047
|
+
toolUseId,
|
|
1048
|
+
status: /<status>\s*([^<]*?)\s*<\/status>/.exec(text)?.[1] ?? ""
|
|
1049
|
+
};
|
|
1050
|
+
};
|
|
1051
|
+
/** The first text of a message body, however the content is spelled — the
|
|
1052
|
+
* stored transcript uses bare strings, the live stream uses blocks. */
|
|
1053
|
+
const firstText = (content) => {
|
|
1054
|
+
if (typeof content === "string") return content;
|
|
1055
|
+
for (const block of content) {
|
|
1056
|
+
const b = block;
|
|
1057
|
+
if (b?.type === "text" && typeof b.text === "string") return b.text;
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
/** Trim, drop blank, clip at the same 80 the terminal theme's `taskLabel` uses.
|
|
1061
|
+
* Model-authored input rides every row of a polled sessions list, so it is
|
|
1062
|
+
* bounded here rather than trusted — a 10KB `description` would be paid for at
|
|
1063
|
+
* every poll. */
|
|
1064
|
+
const cleaned = (value) => {
|
|
1065
|
+
if (typeof value !== "string") return void 0;
|
|
1066
|
+
const text = value.trim();
|
|
1067
|
+
if (text === "") return void 0;
|
|
1068
|
+
return text.length > 80 ? text.slice(0, 79) + "…" : text;
|
|
1069
|
+
};
|
|
1070
|
+
/** The `tool_use` blocks of a message body, however the content is spelled. */
|
|
1071
|
+
function toolUseBlocks(content) {
|
|
1072
|
+
if (typeof content === "string") return [];
|
|
1073
|
+
const blocks = [];
|
|
1074
|
+
for (const block of content) {
|
|
1075
|
+
if (block.type !== "tool_use") continue;
|
|
1076
|
+
const b = block;
|
|
1077
|
+
if (typeof b.id !== "string" || typeof b.name !== "string") continue;
|
|
1078
|
+
blocks.push({
|
|
1079
|
+
id: b.id,
|
|
1080
|
+
name: b.name,
|
|
1081
|
+
input: b.input
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
return blocks;
|
|
1085
|
+
}
|
|
596
1086
|
//#endregion
|
|
597
1087
|
//#region src/engines/claude/runner.ts
|
|
598
1088
|
const DEFAULT_APPROVAL_TIMEOUT_MS$1 = 3e5;
|
|
@@ -608,8 +1098,15 @@ var SessionRunner = class {
|
|
|
608
1098
|
/** {@link SessionRunnerConfig.cwd}, checked once in the constructor. */
|
|
609
1099
|
#cwd;
|
|
610
1100
|
#events = [];
|
|
611
|
-
#
|
|
1101
|
+
#subscribers = new SubscriberSet();
|
|
612
1102
|
#seq = 0;
|
|
1103
|
+
/**
|
|
1104
|
+
* Latest context-window reading, retained from the last `context_usage` this
|
|
1105
|
+
* runner emitted so `GET /sessions` can answer it without an attach — see
|
|
1106
|
+
* `SessionInfo.contextUsage`. Folded in the emit path, so it is by
|
|
1107
|
+
* construction the same number the transcript last drew.
|
|
1108
|
+
*/
|
|
1109
|
+
#contextUsage;
|
|
613
1110
|
#activityCount = 0;
|
|
614
1111
|
/**
|
|
615
1112
|
* Seq of the latest `conversation_reset` event, 0 when none. The log itself is
|
|
@@ -627,6 +1124,28 @@ var SessionRunner = class {
|
|
|
627
1124
|
#apiKeySource;
|
|
628
1125
|
#permissionMode;
|
|
629
1126
|
#pending = /* @__PURE__ */ new Map();
|
|
1127
|
+
/**
|
|
1128
|
+
* The turn ended while an approval was standing, and nothing has started a
|
|
1129
|
+
* new one since.
|
|
1130
|
+
*
|
|
1131
|
+
* `awaiting_approval` rightly outranks `idle` for display, so a turn-over
|
|
1132
|
+
* signal arriving under a standing approval cannot be applied when it lands.
|
|
1133
|
+
* It used to be **discarded** for that reason, which is a different thing
|
|
1134
|
+
* from outranked: the settle path then asserted `running` on the assumption
|
|
1135
|
+
* that an answered approval means work resumes, and when the turn was already
|
|
1136
|
+
* over — an interrupt, a timeout — the session claimed to be running one that
|
|
1137
|
+
* had produced its result. Status is purely edge-driven here, with no poll and
|
|
1138
|
+
* no reconciliation anywhere, so that single dropped edge never came back and
|
|
1139
|
+
* every client rendered it faithfully for the life of the session.
|
|
1140
|
+
*
|
|
1141
|
+
* So the fact is *deferred* rather than dropped, and it is deliberately
|
|
1142
|
+
* cleared the moment work genuinely resumes — a turn-over belongs to the turn
|
|
1143
|
+
* that produced it and must not settle the next one.
|
|
1144
|
+
*/
|
|
1145
|
+
#turnOverWhileBlocked = false;
|
|
1146
|
+
/** The read-time sub-agent rollup (`SessionInfo.subagents`), fed from #emit —
|
|
1147
|
+
* the one chokepoint — so the resume backfill reconstructs it for free. */
|
|
1148
|
+
#subagents = new SubagentTracker();
|
|
630
1149
|
#totalCostUsd;
|
|
631
1150
|
#numTurns;
|
|
632
1151
|
#lastActivityAt;
|
|
@@ -682,7 +1201,9 @@ var SessionRunner = class {
|
|
|
682
1201
|
createdAt: this.createdAt,
|
|
683
1202
|
lastSeq: this.#seq,
|
|
684
1203
|
activityCount: this.#activityCount,
|
|
1204
|
+
contextUsage: this.#contextUsage,
|
|
685
1205
|
pendingPermissionCount: this.#pending.size,
|
|
1206
|
+
subagents: this.#subagents.list(),
|
|
686
1207
|
meta: this.#config.meta,
|
|
687
1208
|
scope: this.#config.scope,
|
|
688
1209
|
title: this.#title(),
|
|
@@ -831,6 +1352,12 @@ var SessionRunner = class {
|
|
|
831
1352
|
});
|
|
832
1353
|
this.#setStatus("closed");
|
|
833
1354
|
}
|
|
1355
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
1356
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
1357
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
1358
|
+
eventAt(seq) {
|
|
1359
|
+
return this.#events.find((event) => event.seq === seq);
|
|
1360
|
+
}
|
|
834
1361
|
/**
|
|
835
1362
|
* Replay buffered events with seq > afterSeq, then deliver live events.
|
|
836
1363
|
* Returns an unsubscribe function.
|
|
@@ -845,15 +1372,7 @@ var SessionRunner = class {
|
|
|
845
1372
|
* content below the newer one and are skipped with what they cleared.
|
|
846
1373
|
*/
|
|
847
1374
|
subscribe(listener, afterSeq = 0, options) {
|
|
848
|
-
|
|
849
|
-
for (const event of this.#events) {
|
|
850
|
-
if (event.seq <= afterSeq) continue;
|
|
851
|
-
if (event.seq < this.#resetSeq && transcriptContent(event)) continue;
|
|
852
|
-
if (stale?.has(event.seq)) continue;
|
|
853
|
-
listener(event);
|
|
854
|
-
}
|
|
855
|
-
this.#listeners.add(listener);
|
|
856
|
-
return () => this.#listeners.delete(listener);
|
|
1375
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
|
|
857
1376
|
}
|
|
858
1377
|
async #run() {
|
|
859
1378
|
const queryFn = this.#config.queryFn ?? query;
|
|
@@ -944,6 +1463,7 @@ var SessionRunner = class {
|
|
|
944
1463
|
forkSession: c.forkSession,
|
|
945
1464
|
effort: c.reasoningEffort,
|
|
946
1465
|
includePartialMessages: c.includePartialMessages ?? true,
|
|
1466
|
+
forwardSubagentText: true,
|
|
947
1467
|
canUseTool: this.#canUseTool,
|
|
948
1468
|
env: c.env,
|
|
949
1469
|
pathToClaudeCodeExecutable: c.pathToClaudeCodeExecutable,
|
|
@@ -970,6 +1490,7 @@ var SessionRunner = class {
|
|
|
970
1490
|
claudeCodeVersion: msg.claude_code_version,
|
|
971
1491
|
mcpServers: msg.mcp_servers
|
|
972
1492
|
});
|
|
1493
|
+
this.#turnOverWhileBlocked = false;
|
|
973
1494
|
this.#setStatus("running");
|
|
974
1495
|
this.#fetchCapabilities();
|
|
975
1496
|
this.#fetchContextUsage();
|
|
@@ -978,7 +1499,11 @@ var SessionRunner = class {
|
|
|
978
1499
|
return;
|
|
979
1500
|
}
|
|
980
1501
|
if (msg.type === "system" && msg.subtype === "session_state_changed") {
|
|
981
|
-
if (this.#pending.size > 0)
|
|
1502
|
+
if (this.#pending.size > 0) {
|
|
1503
|
+
if (msg.state === "idle") this.#turnOverWhileBlocked = true;
|
|
1504
|
+
else if (msg.state === "running") this.#turnOverWhileBlocked = false;
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
982
1507
|
if (msg.state === "idle") this.#setStatus("idle");
|
|
983
1508
|
else if (msg.state === "running") this.#setStatus("running");
|
|
984
1509
|
return;
|
|
@@ -994,6 +1519,7 @@ var SessionRunner = class {
|
|
|
994
1519
|
this.#totalCostUsd = body.totalCostUsd;
|
|
995
1520
|
this.#numTurns = body.numTurns;
|
|
996
1521
|
if (this.#pending.size === 0) this.#setStatus("idle");
|
|
1522
|
+
else this.#turnOverWhileBlocked = true;
|
|
997
1523
|
this.#fetchContextUsage();
|
|
998
1524
|
this.#fetchRateLimits();
|
|
999
1525
|
this.#fetchEngineTitle();
|
|
@@ -1224,7 +1750,12 @@ var SessionRunner = class {
|
|
|
1224
1750
|
resolvedBy,
|
|
1225
1751
|
message: decision.behavior === "deny" ? decision.message ?? "Denied" : void 0
|
|
1226
1752
|
});
|
|
1227
|
-
if (this.#pending.size === 0
|
|
1753
|
+
if (this.#pending.size === 0) {
|
|
1754
|
+
const endedWhileBlocked = this.#turnOverWhileBlocked;
|
|
1755
|
+
this.#turnOverWhileBlocked = false;
|
|
1756
|
+
if (endedWhileBlocked) this.#setStatus("idle");
|
|
1757
|
+
else if (this.#status === "awaiting_approval") this.#setStatus("running");
|
|
1758
|
+
}
|
|
1228
1759
|
}
|
|
1229
1760
|
#setStatus(status, detail) {
|
|
1230
1761
|
if (this.#status === status && this.#statusDetail === detail) return;
|
|
@@ -1245,11 +1776,14 @@ var SessionRunner = class {
|
|
|
1245
1776
|
};
|
|
1246
1777
|
this.#lastActivityAt = event.ts;
|
|
1247
1778
|
this.#activityCount += transcriptActivity(body);
|
|
1248
|
-
|
|
1779
|
+
this.#contextUsage = contextReading(body) ?? this.#contextUsage;
|
|
1780
|
+
if (body.type === "conversation_reset") {
|
|
1781
|
+
this.#resetSeq = event.seq;
|
|
1782
|
+
this.#contextUsage = void 0;
|
|
1783
|
+
}
|
|
1784
|
+
this.#subagents.observe(body, event.ts);
|
|
1249
1785
|
this.#events.push(event);
|
|
1250
|
-
|
|
1251
|
-
listener(event);
|
|
1252
|
-
} catch {}
|
|
1786
|
+
this.#subscribers.emit(event);
|
|
1253
1787
|
}
|
|
1254
1788
|
};
|
|
1255
1789
|
/** Answer each AskUserQuestion question with its first option's label — the tool's
|
|
@@ -1292,8 +1826,15 @@ var AiSdkRunner = class {
|
|
|
1292
1826
|
#config;
|
|
1293
1827
|
#model;
|
|
1294
1828
|
#events = [];
|
|
1295
|
-
#
|
|
1829
|
+
#subscribers = new SubscriberSet();
|
|
1296
1830
|
#seq = 0;
|
|
1831
|
+
/**
|
|
1832
|
+
* Latest context-window reading, retained from the last `context_usage` this
|
|
1833
|
+
* runner emitted so `GET /sessions` can answer it without an attach — see
|
|
1834
|
+
* `SessionInfo.contextUsage`. Folded in the emit path, so it is by
|
|
1835
|
+
* construction the same number the transcript last drew.
|
|
1836
|
+
*/
|
|
1837
|
+
#contextUsage;
|
|
1297
1838
|
#activityCount = 0;
|
|
1298
1839
|
#status = "starting";
|
|
1299
1840
|
#permissionMode;
|
|
@@ -1343,7 +1884,12 @@ var AiSdkRunner = class {
|
|
|
1343
1884
|
if (!state || !Array.isArray(state.messages)) throw new Error("session snapshot is missing its provider-engine state");
|
|
1344
1885
|
this.#seq = snapshot.seq;
|
|
1345
1886
|
this.#events = [...snapshot.events];
|
|
1346
|
-
this.#activityCount =
|
|
1887
|
+
this.#activityCount = 0;
|
|
1888
|
+
for (const event of this.#events) {
|
|
1889
|
+
this.#activityCount += transcriptActivity(event);
|
|
1890
|
+
if (event.type === "conversation_reset") this.#contextUsage = void 0;
|
|
1891
|
+
else this.#contextUsage = contextReading(event) ?? this.#contextUsage;
|
|
1892
|
+
}
|
|
1347
1893
|
this.#messages = [...state.messages];
|
|
1348
1894
|
for (const call of state.pendingToolCalls) this.#pendingToolCalls.set(call.toolCallId, call);
|
|
1349
1895
|
this.#dispatched = new Set(state.dispatched);
|
|
@@ -1394,6 +1940,7 @@ var AiSdkRunner = class {
|
|
|
1394
1940
|
createdAt: this.createdAt,
|
|
1395
1941
|
lastSeq: this.#seq,
|
|
1396
1942
|
activityCount: this.#activityCount,
|
|
1943
|
+
contextUsage: this.#contextUsage,
|
|
1397
1944
|
pendingPermissionCount: 0,
|
|
1398
1945
|
meta: this.#config.meta,
|
|
1399
1946
|
scope: this.#config.scope,
|
|
@@ -1405,10 +1952,7 @@ var AiSdkRunner = class {
|
|
|
1405
1952
|
start() {
|
|
1406
1953
|
if (this.#started) return this.#turnChain;
|
|
1407
1954
|
this.#started = true;
|
|
1408
|
-
if (this.#config.restore)
|
|
1409
|
-
if (this.#pendingToolCalls.size === 0) this.#scheduleTurn();
|
|
1410
|
-
return this.#turnChain;
|
|
1411
|
-
}
|
|
1955
|
+
if (this.#config.restore) return this.#turnChain;
|
|
1412
1956
|
this.#setStatus("idle");
|
|
1413
1957
|
if (this.#config.prompt) this.sendMessage(this.#config.prompt);
|
|
1414
1958
|
return this.#turnChain;
|
|
@@ -1423,6 +1967,61 @@ var AiSdkRunner = class {
|
|
|
1423
1967
|
if (this.#closed || this.#parked) return void 0;
|
|
1424
1968
|
if (this.#abort || !this.#restingOnDeferred()) return void 0;
|
|
1425
1969
|
this.#setStatus("parked");
|
|
1970
|
+
const snapshot = this.#buildSnapshot();
|
|
1971
|
+
this.#parked = true;
|
|
1972
|
+
this.#subscribers.clear();
|
|
1973
|
+
try {
|
|
1974
|
+
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1975
|
+
} catch {}
|
|
1976
|
+
return snapshot;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* The same snapshot, taken without ending anything.
|
|
1980
|
+
*
|
|
1981
|
+
* `park()` and this are two operations that happen to produce the same value,
|
|
1982
|
+
* and the difference is the whole point: `park()` *ends* the live runner
|
|
1983
|
+
* (inert, listeners dropped, `onClose` called), which is right for deferred
|
|
1984
|
+
* execution — the session has nothing to do for possibly days — and wrong for
|
|
1985
|
+
* restart-survival, where the session is active and someone is mid-
|
|
1986
|
+
* conversation. This one changes nothing at all: no status emit, no listener
|
|
1987
|
+
* clear, no disposer. The host writes the value through to durable storage
|
|
1988
|
+
* after each turn and keeps the runner live and warm, so a restart rebuilds
|
|
1989
|
+
* from the last write through the existing `restore` path and the next message
|
|
1990
|
+
* costs no wake.
|
|
1991
|
+
*
|
|
1992
|
+
* The gate is `park()`'s minus the requirement that there be something parked:
|
|
1993
|
+
*
|
|
1994
|
+
* - `#abort` set is refused for the reason it always was — a `generate()` in
|
|
1995
|
+
* flight has produced messages that are not in the history yet, so the
|
|
1996
|
+
* snapshot would be of a turn that half-happened.
|
|
1997
|
+
* - Pending calls that are **not** all deferred are refused, which is
|
|
1998
|
+
* `park()`'s rule wearing a different hat. An in-process execution's result
|
|
1999
|
+
* is coming back to *this* runner and dies with the process; a restore would
|
|
2000
|
+
* wait on it forever, and `state.dispatched` is what would stop the rebuilt
|
|
2001
|
+
* runner from simply calling it again.
|
|
2002
|
+
* - Idle with nothing pending — the case `park()` exists to refuse — is
|
|
2003
|
+
* exactly the case this exists to allow.
|
|
2004
|
+
*/
|
|
2005
|
+
snapshot() {
|
|
2006
|
+
if (this.#closed || this.#parked || this.#abort) return void 0;
|
|
2007
|
+
if (this.#pendingToolCalls.size > 0 && !this.#restingOnDeferred()) return void 0;
|
|
2008
|
+
return this.#buildSnapshot();
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* The snapshot value itself, shared so a park and a write-through cannot
|
|
2012
|
+
* disagree about what a session *is*.
|
|
2013
|
+
*
|
|
2014
|
+
* The event log is filtered through {@link snapshotRetains} — the persisted
|
|
2015
|
+
* log drops stream deltas, which are superseded by the `assistant_message`
|
|
2016
|
+
* that flushes them and would otherwise be tens of times the size of the text
|
|
2017
|
+
* they spell. Parks get it too, and should: a park sits on disk for days.
|
|
2018
|
+
*
|
|
2019
|
+
* The `parked` list and `state.parkedAt` are honest under both callers. An
|
|
2020
|
+
* idle write-through has no pending calls, so `parked` is empty and the host
|
|
2021
|
+
* arms no watchdogs; `parkedAt` is "when this was taken", which is what
|
|
2022
|
+
* `#restore` needs to discount a turn's clock either way.
|
|
2023
|
+
*/
|
|
2024
|
+
#buildSnapshot() {
|
|
1426
2025
|
const parked = [...this.#pendingToolCalls.values()].map((call) => ({
|
|
1427
2026
|
executionId: call.toolCallId,
|
|
1428
2027
|
toolName: call.toolName,
|
|
@@ -1440,22 +2039,16 @@ var AiSdkRunner = class {
|
|
|
1440
2039
|
lastActivityAt: this.#lastActivityAt,
|
|
1441
2040
|
parkedAt: Date.now()
|
|
1442
2041
|
};
|
|
1443
|
-
|
|
2042
|
+
return {
|
|
1444
2043
|
engine: "provider",
|
|
1445
2044
|
id: this.id,
|
|
1446
2045
|
createdAt: this.createdAt,
|
|
1447
2046
|
seq: this.#seq,
|
|
1448
|
-
events:
|
|
2047
|
+
events: this.#events.filter((event) => snapshotRetains(event)),
|
|
1449
2048
|
vfs: this.#config.vfs?.snapshot(),
|
|
1450
2049
|
parked,
|
|
1451
2050
|
state
|
|
1452
2051
|
};
|
|
1453
|
-
this.#parked = true;
|
|
1454
|
-
this.#listeners.clear();
|
|
1455
|
-
try {
|
|
1456
|
-
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1457
|
-
} catch {}
|
|
1458
|
-
return snapshot;
|
|
1459
2052
|
}
|
|
1460
2053
|
sendMessage(text, attachments) {
|
|
1461
2054
|
if (this.#parked) throw new Error("session is parked");
|
|
@@ -1638,15 +2231,14 @@ var AiSdkRunner = class {
|
|
|
1638
2231
|
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1639
2232
|
} catch {}
|
|
1640
2233
|
}
|
|
2234
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
2235
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
2236
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
2237
|
+
eventAt(seq) {
|
|
2238
|
+
return this.#events.find((event) => event.seq === seq);
|
|
2239
|
+
}
|
|
1641
2240
|
subscribe(listener, afterSeq = 0, options) {
|
|
1642
|
-
|
|
1643
|
-
for (const event of this.#events) {
|
|
1644
|
-
if (event.seq <= afterSeq) continue;
|
|
1645
|
-
if (stale?.has(event.seq)) continue;
|
|
1646
|
-
listener(event);
|
|
1647
|
-
}
|
|
1648
|
-
this.#listeners.add(listener);
|
|
1649
|
-
return () => this.#listeners.delete(listener);
|
|
2241
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
|
|
1650
2242
|
}
|
|
1651
2243
|
#scheduleTurn() {
|
|
1652
2244
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
@@ -2035,10 +2627,10 @@ var AiSdkRunner = class {
|
|
|
2035
2627
|
};
|
|
2036
2628
|
this.#lastActivityAt = event.ts;
|
|
2037
2629
|
this.#activityCount += transcriptActivity(body);
|
|
2630
|
+
this.#contextUsage = contextReading(body) ?? this.#contextUsage;
|
|
2631
|
+
if (body.type === "conversation_reset") this.#contextUsage = void 0;
|
|
2038
2632
|
this.#events.push(event);
|
|
2039
|
-
|
|
2040
|
-
listener(event);
|
|
2041
|
-
} catch {}
|
|
2633
|
+
this.#subscribers.emit(event);
|
|
2042
2634
|
}
|
|
2043
2635
|
};
|
|
2044
2636
|
function turnUsage(accum) {
|
|
@@ -3257,8 +3849,6 @@ const claudeAdapter = {
|
|
|
3257
3849
|
}));
|
|
3258
3850
|
}
|
|
3259
3851
|
};
|
|
3260
|
-
//#endregion
|
|
3261
|
-
//#region src/engines/codex/jsonrpc.ts
|
|
3262
3852
|
/**
|
|
3263
3853
|
* A JSON-RPC error response from the peer, or one we return to it. `code`
|
|
3264
3854
|
* follows the JSON-RPC 2.0 reserved ranges (-32601 = method not found).
|
|
@@ -3290,9 +3880,12 @@ var JsonRpcStdioConnection = class {
|
|
|
3290
3880
|
#buffer = "";
|
|
3291
3881
|
#closed = false;
|
|
3292
3882
|
#notificationHandler;
|
|
3883
|
+
/** Where {@link CODEX_TRACE_ENV} pointed, or undefined — read once. */
|
|
3884
|
+
#trace;
|
|
3293
3885
|
#requestHandler;
|
|
3294
3886
|
constructor(options) {
|
|
3295
3887
|
this.#output = options.output;
|
|
3888
|
+
this.#trace = process.env["WORKERDECK_CODEX_TRACE"] || void 0;
|
|
3296
3889
|
options.input.on("data", (chunk) => this.#feed(String(chunk)));
|
|
3297
3890
|
options.input.on("error", () => {});
|
|
3298
3891
|
options.output.on("error", () => {});
|
|
@@ -3353,9 +3946,28 @@ var JsonRpcStdioConnection = class {
|
|
|
3353
3946
|
} catch {
|
|
3354
3947
|
continue;
|
|
3355
3948
|
}
|
|
3949
|
+
this.#traceLine(message);
|
|
3356
3950
|
this.#dispatch(message);
|
|
3357
3951
|
}
|
|
3358
3952
|
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Append one inbound message to the trace file, when the operator asked for
|
|
3955
|
+
* one. **Notifications and server→client requests only** — a response body is
|
|
3956
|
+
* not needed to answer the questions this exists for, and `account/*` results
|
|
3957
|
+
* are the one place app-server traffic can carry a masked credential
|
|
3958
|
+
* fragment, which nothing of ours writes to disk (see the auth red lines).
|
|
3959
|
+
* Best-effort and synchronous: a debug sink that loses lines proves nothing,
|
|
3960
|
+
* and a debug sink that throws must not take the session with it.
|
|
3961
|
+
*/
|
|
3962
|
+
#traceLine(message) {
|
|
3963
|
+
if (!this.#trace) return;
|
|
3964
|
+
const method = message.method;
|
|
3965
|
+
if (typeof method !== "string") return;
|
|
3966
|
+
if (method.startsWith("account/") || method.startsWith("login")) return;
|
|
3967
|
+
try {
|
|
3968
|
+
appendFileSync(this.#trace, JSON.stringify(message) + "\n");
|
|
3969
|
+
} catch {}
|
|
3970
|
+
}
|
|
3359
3971
|
#dispatch(message) {
|
|
3360
3972
|
const { id, method } = message;
|
|
3361
3973
|
if (typeof method === "string") {
|
|
@@ -3394,6 +4006,117 @@ var JsonRpcStdioConnection = class {
|
|
|
3394
4006
|
}
|
|
3395
4007
|
};
|
|
3396
4008
|
//#endregion
|
|
4009
|
+
//#region src/engines/codex/subagents.ts
|
|
4010
|
+
/**
|
|
4011
|
+
* The codex side of `SessionInfo.subagents` — and the attribution table that
|
|
4012
|
+
* gives every event a spawned agent produces its `parentToolUseId`.
|
|
4013
|
+
*
|
|
4014
|
+
* Codex's signal is stronger than the claude engine's, so this is deliberately
|
|
4015
|
+
* NOT that tracker generalised (`engines/claude/subagents.ts` infers spawns
|
|
4016
|
+
* from tool names and verdicts from result-text sniffing, ~290 lines of module
|
|
4017
|
+
* doc explaining the inference). Here nothing is inferred: `subAgentActivity
|
|
4018
|
+
* {kind: 'started'}` on the owning thread positively announces an agent, names
|
|
4019
|
+
* it (`agentPath`), keys it (`agentThreadId` — the id every one of its later
|
|
4020
|
+
* notifications carries) and hands over the model's own `spawn_agent` call id;
|
|
4021
|
+
* the agent's end is its own thread's `turn/completed`, status included. So a
|
|
4022
|
+
* record is keyed by **thread id** — the wire's handle — while exposing a
|
|
4023
|
+
* **tool-use id** — the protocol's: `parentToolUseId` on nested events must
|
|
4024
|
+
* equal the anchor `tool_use`'s id for `subagentItems` (the frame membership
|
|
4025
|
+
* rule every client shares) to reassemble the sidechain, and this map is where
|
|
4026
|
+
* the two vocabularies meet.
|
|
4027
|
+
*
|
|
4028
|
+
* Two decisions worth their prose:
|
|
4029
|
+
*
|
|
4030
|
+
* **A record survives the runner's turns.** Codex agents are designed to
|
|
4031
|
+
* outlive the root turn that spawned them (`sendInput`/`resumeAgent` address a
|
|
4032
|
+
* thread that kept existing), so — unlike a pending approval — nothing here is
|
|
4033
|
+
* swept when a root turn ends. What does end every agent is the app-server
|
|
4034
|
+
* process itself: the runner calls {@link sweep} when the child dies or the
|
|
4035
|
+
* session closes, because an agent whose host process is gone can never report,
|
|
4036
|
+
* and `running` on a closed session would be a lie a polled list re-renders
|
|
4037
|
+
* forever (the claude tracker's argument, inherited whole).
|
|
4038
|
+
*
|
|
4039
|
+
* **The settled tail is bounded, running records never are** — the same
|
|
4040
|
+
* {@link SUBAGENT_HISTORY} discipline as the claude tracker, and enforced at
|
|
4041
|
+
* settle time for the same reason: a settle happens once per agent, `list()`
|
|
4042
|
+
* once per row of a 1.2s-polled sessions list.
|
|
4043
|
+
*/
|
|
4044
|
+
var CodexAgentTracker = class {
|
|
4045
|
+
#byThread = /* @__PURE__ */ new Map();
|
|
4046
|
+
#settleCounter = 0;
|
|
4047
|
+
/** The record whose thread this is — the attribution lookup. */
|
|
4048
|
+
get(agentThreadId) {
|
|
4049
|
+
return this.#byThread.get(agentThreadId);
|
|
4050
|
+
}
|
|
4051
|
+
/** Open (or return) the record for a thread. Fill-in, never overwrite: a
|
|
4052
|
+
* label-less fallback record keeps its accumulated count and its already
|
|
4053
|
+
* published toolUseId when the announcing item arrives late. */
|
|
4054
|
+
open(agentThreadId, toolUseId, agentType, ts) {
|
|
4055
|
+
let record = this.#byThread.get(agentThreadId);
|
|
4056
|
+
if (!record) {
|
|
4057
|
+
record = {
|
|
4058
|
+
agentThreadId,
|
|
4059
|
+
toolUseId,
|
|
4060
|
+
status: "running",
|
|
4061
|
+
startedAt: ts,
|
|
4062
|
+
toolCount: 0,
|
|
4063
|
+
counted: /* @__PURE__ */ new Set()
|
|
4064
|
+
};
|
|
4065
|
+
this.#byThread.set(agentThreadId, record);
|
|
4066
|
+
}
|
|
4067
|
+
record.agentType ??= agentType;
|
|
4068
|
+
return record;
|
|
4069
|
+
}
|
|
4070
|
+
/** The agent's thread ran again (`kind: 'interacted'`, or a fresh
|
|
4071
|
+
* `turn/started` on its thread): a settled verdict no longer describes it. */
|
|
4072
|
+
revive(record) {
|
|
4073
|
+
record.status = "running";
|
|
4074
|
+
record.settledOrder = void 0;
|
|
4075
|
+
}
|
|
4076
|
+
#settle(record, status) {
|
|
4077
|
+
record.status = status;
|
|
4078
|
+
record.settledOrder = ++this.#settleCounter;
|
|
4079
|
+
let settled = 0;
|
|
4080
|
+
for (const r of this.#byThread.values()) if (r.settledOrder !== void 0) settled++;
|
|
4081
|
+
while (settled > SUBAGENT_HISTORY) {
|
|
4082
|
+
let oldest;
|
|
4083
|
+
for (const r of this.#byThread.values()) {
|
|
4084
|
+
if (r.settledOrder === void 0) continue;
|
|
4085
|
+
if (!oldest || r.settledOrder < oldest.settledOrder) oldest = r;
|
|
4086
|
+
}
|
|
4087
|
+
if (!oldest) break;
|
|
4088
|
+
this.#byThread.delete(oldest.agentThreadId);
|
|
4089
|
+
settled--;
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
/** A real verdict for one agent — its thread's `turn/completed`, or the
|
|
4093
|
+
* `interrupted` activity edge. */
|
|
4094
|
+
settle(record, status) {
|
|
4095
|
+
if (record.status === status) return;
|
|
4096
|
+
this.#settle(record, status);
|
|
4097
|
+
}
|
|
4098
|
+
/** The process the agents lived in is gone (child death, session close):
|
|
4099
|
+
* everything still running is settled as failed — the report can never come. */
|
|
4100
|
+
sweep() {
|
|
4101
|
+
for (const record of this.#byThread.values()) if (record.status === "running") this.#settle(record, "failed");
|
|
4102
|
+
}
|
|
4103
|
+
/** The rollup as `SessionInfo.subagents` serves it — spawn order, fresh
|
|
4104
|
+
* objects, and `undefined` when there is nothing to say (absent and empty
|
|
4105
|
+
* mean the same thing to a client, and bytes on a polled list are paid for). */
|
|
4106
|
+
list() {
|
|
4107
|
+
if (this.#byThread.size === 0) return void 0;
|
|
4108
|
+
const out = [];
|
|
4109
|
+
for (const r of this.#byThread.values()) out.push({
|
|
4110
|
+
toolUseId: r.toolUseId,
|
|
4111
|
+
agentType: r.agentType,
|
|
4112
|
+
status: r.status,
|
|
4113
|
+
startedAt: r.startedAt,
|
|
4114
|
+
toolCount: r.toolCount
|
|
4115
|
+
});
|
|
4116
|
+
return out;
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
4119
|
+
//#endregion
|
|
3397
4120
|
//#region src/engines/codex/runner.ts
|
|
3398
4121
|
/**
|
|
3399
4122
|
* thread/start's sandbox axis (string form) — our permission modes as codex
|
|
@@ -3434,16 +4157,27 @@ const GRANULAR_ASK = { granular: {
|
|
|
3434
4157
|
request_permissions: true,
|
|
3435
4158
|
skill_approval: true
|
|
3436
4159
|
} };
|
|
4160
|
+
const GRANULAR_NEVER = { granular: {
|
|
4161
|
+
sandbox_approval: false,
|
|
4162
|
+
rules: false,
|
|
4163
|
+
mcp_elicitations: false,
|
|
4164
|
+
request_permissions: false,
|
|
4165
|
+
skill_approval: false
|
|
4166
|
+
} };
|
|
4167
|
+
/**
|
|
4168
|
+
* Notifications whose meaning is scoped to ONE thread, and which are therefore
|
|
4169
|
+
* only ever read off the session's own. Everything else (items, deltas) is
|
|
4170
|
+
* accepted from any thread on the connection — see `#handleNotification`.
|
|
4171
|
+
*/
|
|
4172
|
+
const THREAD_SCOPED_NOTIFICATIONS = new Set([
|
|
4173
|
+
"turn/started",
|
|
4174
|
+
"turn/completed",
|
|
4175
|
+
"thread/tokenUsage/updated"
|
|
4176
|
+
]);
|
|
3437
4177
|
const APPROVAL_POLICY_BY_MODE = {
|
|
3438
4178
|
default: GRANULAR_ASK,
|
|
3439
4179
|
acceptEdits: GRANULAR_ASK,
|
|
3440
|
-
bypassPermissions:
|
|
3441
|
-
sandbox_approval: false,
|
|
3442
|
-
rules: false,
|
|
3443
|
-
mcp_elicitations: false,
|
|
3444
|
-
request_permissions: false,
|
|
3445
|
-
skill_approval: false
|
|
3446
|
-
} }
|
|
4180
|
+
bypassPermissions: GRANULAR_NEVER
|
|
3447
4181
|
};
|
|
3448
4182
|
/** Fallback timeout for a pending approval nobody answers — the SessionRunner
|
|
3449
4183
|
* default, so unattended codex sessions land the same way Claude ones do. */
|
|
@@ -3454,6 +4188,47 @@ const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
|
|
|
3454
4188
|
* host filesystem, an inline preview) off it.
|
|
3455
4189
|
*/
|
|
3456
4190
|
const CODEX_IMAGE_TOOL = "CodexImageGeneration";
|
|
4191
|
+
/**
|
|
4192
|
+
* Tool name for a spawned agent's anchor `tool_use` — the claude engine's
|
|
4193
|
+
* `Task` in this engine's vocabulary. Codex never sends such a call: the model's
|
|
4194
|
+
* `spawn_agent` surfaces only as the `subAgentActivity` marker item, so the
|
|
4195
|
+
* runner authors the call itself, because everything downstream is built on a
|
|
4196
|
+
* top-level `tool_use` existing — `terminalBlocks` absorbs a sidechain into the
|
|
4197
|
+
* call whose id its events carry as `parentToolUseId`, the takeover frames by
|
|
4198
|
+
* it, and `taskIdentity` labels it from the input's `subagent_type`. Not a new
|
|
4199
|
+
* wire idea, just a row: the same shape every other codex tool card uses.
|
|
4200
|
+
*/
|
|
4201
|
+
const CODEX_AGENT_TOOL = "CodexAgent";
|
|
4202
|
+
/** Tool name for the model's collab-agent calls (`wait`, `sendInput`, …), the
|
|
4203
|
+
* `tool` field carried in the input. One name for the whole open axis rather
|
|
4204
|
+
* than a name per verb, so a future verb renders instead of vanishing. */
|
|
4205
|
+
const CODEX_COLLAB_TOOL = "CodexCollab";
|
|
4206
|
+
/** An agent's name is its path's basename: '/root/date_one' → 'date_one'. */
|
|
4207
|
+
function agentName(agentPath) {
|
|
4208
|
+
if (typeof agentPath !== "string") return void 0;
|
|
4209
|
+
return agentPath.split("/").filter(Boolean).at(-1) || void 0;
|
|
4210
|
+
}
|
|
4211
|
+
/** The collab card's input: the verb always, the rich fields only when codex
|
|
4212
|
+
* actually filled them (measured against 0.146.0 they arrive empty — the card
|
|
4213
|
+
* must not render five null columns to say 'wait'). */
|
|
4214
|
+
function collabInput(item) {
|
|
4215
|
+
return {
|
|
4216
|
+
tool: item.tool,
|
|
4217
|
+
...item.receiverThreadIds?.length ? { receiverThreadIds: item.receiverThreadIds } : {},
|
|
4218
|
+
...item.prompt ? { prompt: item.prompt } : {},
|
|
4219
|
+
...item.model ? { model: item.model } : {}
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
/** A completed turn's answer, from its summary `items` page — the last
|
|
4223
|
+
* `agentMessage` text. For a sub-agent's thread this is the agent's report,
|
|
4224
|
+
* which is exactly what belongs in the anchor's `tool_result`. */
|
|
4225
|
+
function turnReport(turn) {
|
|
4226
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
4227
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
4228
|
+
const item = items[index];
|
|
4229
|
+
if (item?.type === "agentMessage" && typeof item.text === "string" && item.text) return item.text;
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
3457
4232
|
/** Longest `result` worth putting in a tool card. The field is free-form and
|
|
3458
4233
|
* undocumented; anything past this is assumed to be an encoded image rather
|
|
3459
4234
|
* than a sentence, and encoded images do not go in the event log. */
|
|
@@ -3795,8 +4570,15 @@ var CodexRunner = class {
|
|
|
3795
4570
|
/** {@link CodexRunnerConfig.cwd}, checked once in the constructor. */
|
|
3796
4571
|
#cwd;
|
|
3797
4572
|
#events = [];
|
|
3798
|
-
#
|
|
4573
|
+
#subscribers = new SubscriberSet();
|
|
3799
4574
|
#seq = 0;
|
|
4575
|
+
/**
|
|
4576
|
+
* Latest context-window reading, retained from the last `context_usage` this
|
|
4577
|
+
* runner emitted so `GET /sessions` can answer it without an attach — see
|
|
4578
|
+
* `SessionInfo.contextUsage`. Folded in the emit path, so it is by
|
|
4579
|
+
* construction the same number the transcript last drew.
|
|
4580
|
+
*/
|
|
4581
|
+
#contextUsage;
|
|
3800
4582
|
#activityCount = 0;
|
|
3801
4583
|
#status = "starting";
|
|
3802
4584
|
#sdkSessionId;
|
|
@@ -3852,6 +4634,12 @@ var CodexRunner = class {
|
|
|
3852
4634
|
* `mcpServerStatus/list` does not carry a status field at all, so without
|
|
3853
4635
|
* this every server would read as "configured" and never as up or down. */
|
|
3854
4636
|
#mcpStatus = /* @__PURE__ */ new Map();
|
|
4637
|
+
/** The spawned agents, keyed by their thread ids — the attribution table
|
|
4638
|
+
* behind `parentToolUseId` and the rollup behind `info().subagents`. Runner-
|
|
4639
|
+
* level, not per-turn: an agent's thread outlives the root turn that spawned
|
|
4640
|
+
* it, and only the child process dying (or the session closing) ends them
|
|
4641
|
+
* all — see the module doc in `subagents.ts`. */
|
|
4642
|
+
#agents = new CodexAgentTracker();
|
|
3855
4643
|
constructor(config, id = randomUUID()) {
|
|
3856
4644
|
const mode = config.permissionMode ?? "default";
|
|
3857
4645
|
if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
|
|
@@ -3903,13 +4691,15 @@ var CodexRunner = class {
|
|
|
3903
4691
|
createdAt: this.createdAt,
|
|
3904
4692
|
lastSeq: this.#seq,
|
|
3905
4693
|
activityCount: this.#activityCount,
|
|
4694
|
+
contextUsage: this.#contextUsage,
|
|
3906
4695
|
pendingPermissionCount: this.#approvals.size,
|
|
3907
4696
|
meta: this.#config.meta,
|
|
3908
4697
|
scope: this.#config.scope,
|
|
3909
4698
|
title: this.#title(),
|
|
3910
4699
|
totalCostUsd: this.#totalCostUsd,
|
|
3911
4700
|
numTurns: this.#numTurns || void 0,
|
|
3912
|
-
lastActivityAt: this.#lastActivityAt
|
|
4701
|
+
lastActivityAt: this.#lastActivityAt,
|
|
4702
|
+
subagents: this.#agents.list()
|
|
3913
4703
|
};
|
|
3914
4704
|
}
|
|
3915
4705
|
#title() {
|
|
@@ -4123,6 +4913,7 @@ var CodexRunner = class {
|
|
|
4123
4913
|
}, "policy");
|
|
4124
4914
|
this.#connection?.close();
|
|
4125
4915
|
this.#connection = void 0;
|
|
4916
|
+
this.#agents.sweep();
|
|
4126
4917
|
this.#activeTurn?.reject(/* @__PURE__ */ new Error("session closed"));
|
|
4127
4918
|
if (this.#imageDir) try {
|
|
4128
4919
|
rmSync(this.#imageDir, {
|
|
@@ -4136,15 +4927,14 @@ var CodexRunner = class {
|
|
|
4136
4927
|
});
|
|
4137
4928
|
this.#setStatus("closed");
|
|
4138
4929
|
}
|
|
4930
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
4931
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
4932
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
4933
|
+
eventAt(seq) {
|
|
4934
|
+
return this.#events.find((event) => event.seq === seq);
|
|
4935
|
+
}
|
|
4139
4936
|
subscribe(listener, afterSeq = 0, options) {
|
|
4140
|
-
|
|
4141
|
-
for (const event of this.#events) {
|
|
4142
|
-
if (event.seq <= afterSeq) continue;
|
|
4143
|
-
if (stale?.has(event.seq)) continue;
|
|
4144
|
-
listener(event);
|
|
4145
|
-
}
|
|
4146
|
-
this.#listeners.add(listener);
|
|
4147
|
-
return () => this.#listeners.delete(listener);
|
|
4937
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
|
|
4148
4938
|
}
|
|
4149
4939
|
#scheduleTurn() {
|
|
4150
4940
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
@@ -4174,6 +4964,7 @@ var CodexRunner = class {
|
|
|
4174
4964
|
behavior: "deny",
|
|
4175
4965
|
message
|
|
4176
4966
|
}, "policy");
|
|
4967
|
+
this.#agents.sweep();
|
|
4177
4968
|
this.#activeTurn?.reject(new Error(message));
|
|
4178
4969
|
});
|
|
4179
4970
|
try {
|
|
@@ -4469,11 +5260,79 @@ var CodexRunner = class {
|
|
|
4469
5260
|
}
|
|
4470
5261
|
#handleNotification(method, params) {
|
|
4471
5262
|
if (this.#closed) return;
|
|
5263
|
+
if (THREAD_SCOPED_NOTIFICATIONS.has(method) && !this.#isRootThread(params)) {
|
|
5264
|
+
if (method === "turn/completed") this.#settleAgentTurn(params);
|
|
5265
|
+
else if (method === "turn/started") {
|
|
5266
|
+
const threadId = this.#threadIdOf(params);
|
|
5267
|
+
const record = threadId ? this.#agents.get(threadId) : void 0;
|
|
5268
|
+
if (record && record.status !== "running") this.#agents.revive(record);
|
|
5269
|
+
}
|
|
5270
|
+
return;
|
|
5271
|
+
}
|
|
4472
5272
|
this.#notifications[method]?.(params);
|
|
4473
5273
|
}
|
|
5274
|
+
/** Whether a notification is about the session's own thread. A notification
|
|
5275
|
+
* with no `threadId` counts as the root's: every thread-scoped method the
|
|
5276
|
+
* schema defines carries one, so an absent id means an older or narrower
|
|
5277
|
+
* shape, not a sub-agent. */
|
|
5278
|
+
#isRootThread(params) {
|
|
5279
|
+
const threadId = this.#threadIdOf(params);
|
|
5280
|
+
if (threadId === void 0) return true;
|
|
5281
|
+
return threadId === this.#sdkSessionId;
|
|
5282
|
+
}
|
|
5283
|
+
#threadIdOf(params) {
|
|
5284
|
+
const threadId = params?.threadId;
|
|
5285
|
+
return typeof threadId === "string" ? threadId : void 0;
|
|
5286
|
+
}
|
|
5287
|
+
/**
|
|
5288
|
+
* The agent behind a notification's `threadId` — the attribution every item
|
|
5289
|
+
* and delta handler asks before emitting, so two agents streaming
|
|
5290
|
+
* concurrently into this one connection come apart again by the id each
|
|
5291
|
+
* frame carries, never by any mutable "current agent".
|
|
5292
|
+
*
|
|
5293
|
+
* A non-root thread with no record still gets one: a thread emitting items on
|
|
5294
|
+
* this connection *is* an agent, whatever announced it (codex runs threads of
|
|
5295
|
+
* its own for review/compact, and a `subAgentActivity` could in principle be
|
|
5296
|
+
* missed) — the claude tracker's nested-event fallback, on a stronger signal.
|
|
5297
|
+
* The minted record is label-less and its anchor is authored here, because an
|
|
5298
|
+
* attributed event whose parent id matches no top-level `tool_use` would
|
|
5299
|
+
* render inline rather than as a frame; a late `started` edge fills the name
|
|
5300
|
+
* in. Root-thread traffic — and, defensively, the pre-thread shapes with no
|
|
5301
|
+
* id at all — stays unattributed (`undefined`).
|
|
5302
|
+
*/
|
|
5303
|
+
#agentFor(params) {
|
|
5304
|
+
const threadId = this.#threadIdOf(params);
|
|
5305
|
+
if (threadId === void 0 || threadId === this.#sdkSessionId) return void 0;
|
|
5306
|
+
const known = this.#agents.get(threadId);
|
|
5307
|
+
if (known) return known;
|
|
5308
|
+
const nonce = this.#activeTurn?.nonce ?? "codex";
|
|
5309
|
+
const record = this.#agents.open(threadId, `${nonce}:agent:${threadId}`, void 0, Date.now());
|
|
5310
|
+
record.anchored = true;
|
|
5311
|
+
this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, { agentThreadId: threadId });
|
|
5312
|
+
return record;
|
|
5313
|
+
}
|
|
5314
|
+
/**
|
|
5315
|
+
* A child thread's `turn/completed` is that AGENT's completion — the one
|
|
5316
|
+
* codex sends (`subAgentActivity` has no 'completed' kind, verified live).
|
|
5317
|
+
* The verdict is the turn's own status, and the report is the completed
|
|
5318
|
+
* turn's final message, delivered as the anchor's `tool_result` so the row
|
|
5319
|
+
* settles exactly the way a claude `Task`'s does. Deliberately not gated on
|
|
5320
|
+
* `#activeTurn`: an agent finishing between root turns still finished.
|
|
5321
|
+
*/
|
|
5322
|
+
#settleAgentTurn(params) {
|
|
5323
|
+
const threadId = this.#threadIdOf(params);
|
|
5324
|
+
const record = threadId ? this.#agents.get(threadId) : void 0;
|
|
5325
|
+
if (!record || record.status !== "running") return;
|
|
5326
|
+
const turn = params?.turn;
|
|
5327
|
+
const status = turn?.status === "completed" ? "done" : "failed";
|
|
5328
|
+
this.#agents.settle(record, status);
|
|
5329
|
+
const report = (turn ? turnReport(turn) : void 0) ?? turn?.error?.message ?? (status === "done" ? "" : turn?.status ?? "failed");
|
|
5330
|
+
this.#emitToolResult(record.toolUseId, report, status === "failed");
|
|
5331
|
+
}
|
|
4474
5332
|
/** Reasoning deltas arrive on two methods that differ only in which section
|
|
4475
5333
|
* counter they advance; the section key carries the method so the two streams
|
|
4476
|
-
* never share a boundary
|
|
5334
|
+
* never share a boundary (and item ids are per-thread, so two agents' streams
|
|
5335
|
+
* never share one either). Section boundaries (a new summary/content entry)
|
|
4477
5336
|
* render as paragraph breaks — the completed item joins sections with '\n\n'. */
|
|
4478
5337
|
#reasoningDelta(method) {
|
|
4479
5338
|
return (params) => {
|
|
@@ -4489,7 +5348,7 @@ var CodexRunner = class {
|
|
|
4489
5348
|
this.#emitDelta({
|
|
4490
5349
|
type: "thinking_delta",
|
|
4491
5350
|
thinking: separator + payload.delta
|
|
4492
|
-
});
|
|
5351
|
+
}, this.#agentFor(params)?.toolUseId ?? null);
|
|
4493
5352
|
};
|
|
4494
5353
|
}
|
|
4495
5354
|
/** One item-progress handler serves `item/started` and `item/updated`. */
|
|
@@ -4497,7 +5356,7 @@ var CodexRunner = class {
|
|
|
4497
5356
|
const active = this.#activeTurn;
|
|
4498
5357
|
if (!active) return;
|
|
4499
5358
|
const item = params?.item;
|
|
4500
|
-
if (item) this.#handleItemProgress(item, active);
|
|
5359
|
+
if (item) this.#handleItemProgress(item, active, this.#agentFor(params));
|
|
4501
5360
|
};
|
|
4502
5361
|
/** The notification dispatch table — every method the child emits that this
|
|
4503
5362
|
* runner maps, in one place. Handlers read `this.#activeTurn` themselves:
|
|
@@ -4515,7 +5374,9 @@ var CodexRunner = class {
|
|
|
4515
5374
|
"turn/completed": (params) => {
|
|
4516
5375
|
const active = this.#activeTurn;
|
|
4517
5376
|
const turn = params?.turn;
|
|
4518
|
-
if (active
|
|
5377
|
+
if (!active || !turn) return;
|
|
5378
|
+
if (active.turnId && turn.id && turn.id !== active.turnId) return;
|
|
5379
|
+
active.resolve(turn);
|
|
4519
5380
|
},
|
|
4520
5381
|
"item/started": this.#itemProgress,
|
|
4521
5382
|
"item/updated": this.#itemProgress,
|
|
@@ -4523,7 +5384,7 @@ var CodexRunner = class {
|
|
|
4523
5384
|
const active = this.#activeTurn;
|
|
4524
5385
|
if (!active) return;
|
|
4525
5386
|
const item = params?.item;
|
|
4526
|
-
if (item) this.#handleItemCompleted(item, active);
|
|
5387
|
+
if (item) this.#handleItemCompleted(item, active, this.#agentFor(params));
|
|
4527
5388
|
},
|
|
4528
5389
|
"item/agentMessage/delta": (params) => {
|
|
4529
5390
|
if (!this.#activeTurn) return;
|
|
@@ -4531,7 +5392,7 @@ var CodexRunner = class {
|
|
|
4531
5392
|
if (typeof delta === "string" && delta) this.#emitDelta({
|
|
4532
5393
|
type: "text_delta",
|
|
4533
5394
|
text: delta
|
|
4534
|
-
});
|
|
5395
|
+
}, this.#agentFor(params)?.toolUseId ?? null);
|
|
4535
5396
|
},
|
|
4536
5397
|
"item/reasoning/textDelta": this.#reasoningDelta("item/reasoning/textDelta"),
|
|
4537
5398
|
"item/reasoning/summaryTextDelta": this.#reasoningDelta("item/reasoning/summaryTextDelta"),
|
|
@@ -4724,30 +5585,40 @@ var CodexRunner = class {
|
|
|
4724
5585
|
if (!this.#closed && this.#approvals.size === 0 && this.#status === "awaiting_approval") this.#setStatus("running");
|
|
4725
5586
|
}
|
|
4726
5587
|
/** Tool calls surface as tool_use when they start; text and reasoning stream
|
|
4727
|
-
* natively via the delta notifications.
|
|
4728
|
-
|
|
5588
|
+
* natively via the delta notifications. `agent` is the sub-agent whose thread
|
|
5589
|
+
* the item arrived on — undefined for the session's own. */
|
|
5590
|
+
#handleItemProgress(item, active, agent) {
|
|
4729
5591
|
const id = `${active.nonce}:${item.id}`;
|
|
5592
|
+
if (item.type === "subAgentActivity") {
|
|
5593
|
+
this.#itemCompleted.subAgentActivity(item, active, id, agent);
|
|
5594
|
+
return;
|
|
5595
|
+
}
|
|
4730
5596
|
if (item.type === "commandExecution" && !active.toolUseEmitted.has(id)) {
|
|
4731
5597
|
active.toolUseEmitted.add(id);
|
|
4732
|
-
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
5598
|
+
this.#emitToolUse(id, "CodexCommand", { command: item.command }, agent);
|
|
4733
5599
|
return;
|
|
4734
5600
|
}
|
|
4735
5601
|
if (item.type === "mcpToolCall" && !active.toolUseEmitted.has(id)) {
|
|
4736
5602
|
active.toolUseEmitted.add(id);
|
|
4737
|
-
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
5603
|
+
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments, agent);
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5606
|
+
if (item.type === "collabAgentToolCall" && !active.toolUseEmitted.has(id)) {
|
|
5607
|
+
active.toolUseEmitted.add(id);
|
|
5608
|
+
this.#emitToolUse(id, CODEX_COLLAB_TOOL, collabInput(item), agent);
|
|
4738
5609
|
return;
|
|
4739
5610
|
}
|
|
4740
5611
|
if (item.type === "imageGeneration" && !active.toolUseEmitted.has(id)) {
|
|
4741
5612
|
active.toolUseEmitted.add(id);
|
|
4742
|
-
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
5613
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item), agent);
|
|
4743
5614
|
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
4744
5615
|
}
|
|
4745
5616
|
}
|
|
4746
|
-
#handleItemCompleted(item, active) {
|
|
5617
|
+
#handleItemCompleted(item, active, agent) {
|
|
4747
5618
|
const id = `${active.nonce}:${item.id}`;
|
|
4748
5619
|
const handler = this.#itemCompleted[item.type];
|
|
4749
5620
|
if (handler) {
|
|
4750
|
-
handler(item, active, id);
|
|
5621
|
+
handler(item, active, id, agent);
|
|
4751
5622
|
return;
|
|
4752
5623
|
}
|
|
4753
5624
|
const unknown = item;
|
|
@@ -4768,67 +5639,121 @@ var CodexRunner = class {
|
|
|
4768
5639
|
* union has never heard of; those take the passthrough above.)
|
|
4769
5640
|
*/
|
|
4770
5641
|
#itemCompleted = {
|
|
4771
|
-
userMessage: () => {
|
|
4772
|
-
|
|
5642
|
+
userMessage: (item, active, _id, agent) => {
|
|
5643
|
+
if (!agent) return;
|
|
5644
|
+
const text = historyUserText(item);
|
|
5645
|
+
if (!text) return;
|
|
5646
|
+
this.#emit({
|
|
5647
|
+
type: "user_message",
|
|
5648
|
+
message: {
|
|
5649
|
+
role: "user",
|
|
5650
|
+
content: text
|
|
5651
|
+
},
|
|
5652
|
+
parentToolUseId: agent.toolUseId,
|
|
5653
|
+
uuid: `${active.nonce}:${item.id}`
|
|
5654
|
+
});
|
|
5655
|
+
},
|
|
5656
|
+
agentMessage: (item, active, id, agent) => {
|
|
4773
5657
|
const text = typeof item.text === "string" ? item.text : "";
|
|
4774
5658
|
this.#emitAssistant(id, [{
|
|
4775
5659
|
type: "text",
|
|
4776
5660
|
text
|
|
4777
|
-
}]);
|
|
4778
|
-
active.finalText = text;
|
|
5661
|
+
}], agent?.toolUseId ?? null);
|
|
5662
|
+
if (!agent) active.finalText = text;
|
|
4779
5663
|
},
|
|
4780
|
-
reasoning: (item, _active, id) => {
|
|
5664
|
+
reasoning: (item, _active, id, agent) => {
|
|
4781
5665
|
const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
|
|
4782
5666
|
const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
|
|
4783
5667
|
const thinking = (summary.length > 0 ? summary : content).join("\n\n");
|
|
4784
5668
|
if (thinking) this.#emitAssistant(id, [{
|
|
4785
5669
|
type: "thinking",
|
|
4786
5670
|
thinking
|
|
4787
|
-
}]);
|
|
5671
|
+
}], agent?.toolUseId ?? null);
|
|
4788
5672
|
},
|
|
4789
|
-
commandExecution: (item, active, id) => {
|
|
5673
|
+
commandExecution: (item, active, id, agent) => {
|
|
4790
5674
|
if (!active.toolUseEmitted.has(id)) {
|
|
4791
5675
|
active.toolUseEmitted.add(id);
|
|
4792
|
-
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
5676
|
+
this.#emitToolUse(id, "CodexCommand", { command: item.command }, agent);
|
|
4793
5677
|
}
|
|
4794
5678
|
const exitCode = item.exitCode ?? void 0;
|
|
4795
5679
|
const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
|
|
4796
5680
|
const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
|
|
4797
|
-
this.#emitToolResult(id, output, failed);
|
|
5681
|
+
this.#emitToolResult(id, output, failed, void 0, agent?.toolUseId ?? null);
|
|
4798
5682
|
},
|
|
4799
|
-
fileChange: (item, _active, id) => {
|
|
4800
|
-
this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
|
|
5683
|
+
fileChange: (item, _active, id, agent) => {
|
|
5684
|
+
this.#emitToolUse(id, "CodexFileChange", { changes: item.changes }, agent);
|
|
4801
5685
|
const lines = item.changes.map((change) => {
|
|
4802
5686
|
return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
|
|
4803
5687
|
});
|
|
4804
5688
|
const only = item.changes.length === 1 ? item.changes[0] : void 0;
|
|
4805
|
-
this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0);
|
|
5689
|
+
this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0, agent?.toolUseId ?? null);
|
|
4806
5690
|
},
|
|
4807
|
-
mcpToolCall: (item, active, id) => {
|
|
5691
|
+
mcpToolCall: (item, active, id, agent) => {
|
|
4808
5692
|
if (!active.toolUseEmitted.has(id)) {
|
|
4809
5693
|
active.toolUseEmitted.add(id);
|
|
4810
|
-
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
5694
|
+
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments, agent);
|
|
4811
5695
|
}
|
|
4812
5696
|
const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
|
|
4813
|
-
this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
|
|
5697
|
+
this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError, void 0, agent?.toolUseId ?? null);
|
|
4814
5698
|
},
|
|
4815
|
-
webSearch: (item, _active, id) => {
|
|
4816
|
-
this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
|
|
4817
|
-
this.#emitToolResult(id, "", false);
|
|
5699
|
+
webSearch: (item, _active, id, agent) => {
|
|
5700
|
+
this.#emitToolUse(id, "CodexWebSearch", { query: item.query }, agent);
|
|
5701
|
+
this.#emitToolResult(id, "", false, void 0, agent?.toolUseId ?? null);
|
|
4818
5702
|
},
|
|
4819
|
-
imageGeneration: (item, active, id) => {
|
|
5703
|
+
imageGeneration: (item, active, id, agent) => {
|
|
4820
5704
|
active.toolUseEmitted.add(id);
|
|
4821
|
-
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
5705
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item), agent);
|
|
4822
5706
|
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
4823
5707
|
const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
|
|
4824
|
-
this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
|
|
5708
|
+
this.#emitToolResult(id, lines.join("\n"), item.status === "failed", void 0, agent?.toolUseId ?? null);
|
|
4825
5709
|
},
|
|
4826
|
-
imageView: (item, _active, id) => {
|
|
4827
|
-
this.#emitToolUse(id, "CodexImageView", { path: item.path });
|
|
4828
|
-
this.#emitToolResult(id, item.path, false);
|
|
5710
|
+
imageView: (item, _active, id, agent) => {
|
|
5711
|
+
this.#emitToolUse(id, "CodexImageView", { path: item.path }, agent);
|
|
5712
|
+
this.#emitToolResult(id, item.path, false, void 0, agent?.toolUseId ?? null);
|
|
5713
|
+
},
|
|
5714
|
+
subAgentActivity: (item, _active, id, agent) => {
|
|
5715
|
+
if (this.#replayingHistory) {
|
|
5716
|
+
if (item.kind !== "started") return;
|
|
5717
|
+
this.#emitToolUse(id, CODEX_AGENT_TOOL, {
|
|
5718
|
+
...agentName(item.agentPath) ? { subagent_type: agentName(item.agentPath) } : {},
|
|
5719
|
+
agentThreadId: item.agentThreadId,
|
|
5720
|
+
...item.agentPath ? { agentPath: item.agentPath } : {}
|
|
5721
|
+
}, agent);
|
|
5722
|
+
this.#emitToolResult(id, "(ran in its own thread — its work is not part of this thread's stored history)", false, void 0, agent?.toolUseId ?? null);
|
|
5723
|
+
return;
|
|
5724
|
+
}
|
|
5725
|
+
const record = this.#agents.get(item.agentThreadId) ?? this.#agents.open(item.agentThreadId, id, void 0, Date.now());
|
|
5726
|
+
const name = agentName(item.agentPath);
|
|
5727
|
+
const relabel = record.agentType === void 0 && name !== void 0;
|
|
5728
|
+
if (relabel) record.agentType = name;
|
|
5729
|
+
if (!record.anchored || relabel) {
|
|
5730
|
+
record.anchored = true;
|
|
5731
|
+
this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, {
|
|
5732
|
+
...record.agentType ? { subagent_type: record.agentType } : {},
|
|
5733
|
+
agentThreadId: item.agentThreadId,
|
|
5734
|
+
...item.agentPath ? { agentPath: item.agentPath } : {}
|
|
5735
|
+
}, agent);
|
|
5736
|
+
}
|
|
5737
|
+
if (item.kind === "interrupted") {
|
|
5738
|
+
if (record.status === "running") {
|
|
5739
|
+
this.#agents.settle(record, "failed");
|
|
5740
|
+
this.#emitToolResult(record.toolUseId, "interrupted", true);
|
|
5741
|
+
}
|
|
5742
|
+
return;
|
|
5743
|
+
}
|
|
5744
|
+
if (item.kind !== "started" && record.status !== "running") this.#agents.revive(record);
|
|
5745
|
+
},
|
|
5746
|
+
collabAgentToolCall: (item, active, id, agent) => {
|
|
5747
|
+
if (!active.toolUseEmitted.has(id)) {
|
|
5748
|
+
active.toolUseEmitted.add(id);
|
|
5749
|
+
this.#emitToolUse(id, CODEX_COLLAB_TOOL, collabInput(item), agent);
|
|
5750
|
+
}
|
|
5751
|
+
if (item.status === "inProgress") return;
|
|
5752
|
+
const failed = item.status === "failed" || item.status === "declined";
|
|
5753
|
+
this.#emitToolResult(id, failed ? item.status : "", failed, void 0, agent?.toolUseId ?? null);
|
|
4829
5754
|
}
|
|
4830
5755
|
};
|
|
4831
|
-
#emitDelta(delta) {
|
|
5756
|
+
#emitDelta(delta, parent) {
|
|
4832
5757
|
if (this.#config.includePartialMessages === false) return;
|
|
4833
5758
|
this.#emit({
|
|
4834
5759
|
type: "stream_delta",
|
|
@@ -4836,11 +5761,11 @@ var CodexRunner = class {
|
|
|
4836
5761
|
type: "content_block_delta",
|
|
4837
5762
|
delta
|
|
4838
5763
|
},
|
|
4839
|
-
parentToolUseId:
|
|
5764
|
+
parentToolUseId: parent,
|
|
4840
5765
|
uuid: randomUUID()
|
|
4841
5766
|
});
|
|
4842
5767
|
}
|
|
4843
|
-
#emitAssistant(uuid, content) {
|
|
5768
|
+
#emitAssistant(uuid, content, parent) {
|
|
4844
5769
|
this.#emit({
|
|
4845
5770
|
type: "assistant_message",
|
|
4846
5771
|
message: {
|
|
@@ -4848,11 +5773,19 @@ var CodexRunner = class {
|
|
|
4848
5773
|
content,
|
|
4849
5774
|
model: this.#model ?? this.#resolvedModel
|
|
4850
5775
|
},
|
|
4851
|
-
parentToolUseId:
|
|
5776
|
+
parentToolUseId: parent,
|
|
4852
5777
|
uuid
|
|
4853
5778
|
});
|
|
4854
5779
|
}
|
|
4855
|
-
|
|
5780
|
+
/** `agent` (rather than a bare parent id) because a nested call is also the
|
|
5781
|
+
* agent's progress reading: `SubagentInfo.toolCount` ticks here, once per
|
|
5782
|
+
* card — the `counted` set is what keeps an upserted re-emission (the
|
|
5783
|
+
* finished imageGeneration input) from counting one picture twice. */
|
|
5784
|
+
#emitToolUse(id, name, input, agent) {
|
|
5785
|
+
if (agent && !agent.counted.has(id)) {
|
|
5786
|
+
agent.counted.add(id);
|
|
5787
|
+
agent.toolCount += 1;
|
|
5788
|
+
}
|
|
4856
5789
|
this.#emit({
|
|
4857
5790
|
type: "assistant_message",
|
|
4858
5791
|
message: {
|
|
@@ -4865,11 +5798,11 @@ var CodexRunner = class {
|
|
|
4865
5798
|
}],
|
|
4866
5799
|
model: this.#model ?? this.#resolvedModel
|
|
4867
5800
|
},
|
|
4868
|
-
parentToolUseId: null,
|
|
5801
|
+
parentToolUseId: agent?.toolUseId ?? null,
|
|
4869
5802
|
uuid: `${id}-use`
|
|
4870
5803
|
});
|
|
4871
5804
|
}
|
|
4872
|
-
#emitToolResult(toolUseId, content, isError, patch) {
|
|
5805
|
+
#emitToolResult(toolUseId, content, isError, patch, parent = null) {
|
|
4873
5806
|
this.#emit({
|
|
4874
5807
|
type: "user_message",
|
|
4875
5808
|
message: {
|
|
@@ -4881,7 +5814,7 @@ var CodexRunner = class {
|
|
|
4881
5814
|
is_error: isError || void 0
|
|
4882
5815
|
}]
|
|
4883
5816
|
},
|
|
4884
|
-
parentToolUseId:
|
|
5817
|
+
parentToolUseId: parent,
|
|
4885
5818
|
synthetic: true,
|
|
4886
5819
|
patch,
|
|
4887
5820
|
uuid: `${toolUseId}-result`
|
|
@@ -5009,10 +5942,10 @@ var CodexRunner = class {
|
|
|
5009
5942
|
};
|
|
5010
5943
|
this.#lastActivityAt = event.ts;
|
|
5011
5944
|
this.#activityCount += transcriptActivity(body);
|
|
5945
|
+
this.#contextUsage = contextReading(body) ?? this.#contextUsage;
|
|
5946
|
+
if (body.type === "conversation_reset") this.#contextUsage = void 0;
|
|
5012
5947
|
this.#events.push(event);
|
|
5013
|
-
|
|
5014
|
-
listener(event);
|
|
5015
|
-
} catch {}
|
|
5948
|
+
this.#subscribers.emit(event);
|
|
5016
5949
|
}
|
|
5017
5950
|
};
|
|
5018
5951
|
//#endregion
|
|
@@ -5446,6 +6379,6 @@ function getEngineAdapter(engine) {
|
|
|
5446
6379
|
return ADAPTERS[engine ?? "claude"];
|
|
5447
6380
|
}
|
|
5448
6381
|
//#endregion
|
|
5449
|
-
export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withHostTools, withMcpTools };
|
|
6382
|
+
export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, replaySlice, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, truncateResultBlocks, withHostTools, withMcpTools };
|
|
5450
6383
|
|
|
5451
6384
|
//# sourceMappingURL=index.mjs.map
|