@khorsheed/dsh-local-agent-codex 0.2.0-rc.3 → 0.2.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/CHANGELOG.md +15 -1
- package/README.en.md +23 -136
- package/README.i18n.yaml +2 -2
- package/README.md +23 -136
- package/lib/client.js +64 -64
- package/lib/index.js +317 -148
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/codex-cli-provider.d.ts +33 -4
- package/lib/types/codex-cli-provider.js +144 -43
- package/lib/types/live-driver.d.ts +0 -5
- package/lib/types/live-driver.js +231 -106
- package/lib/types/model-broker.js +42 -1
- package/lib/types/records.d.ts +6 -4
- package/lib/types/records.js +13 -6
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -135,10 +135,12 @@ async function listCodexSessions(homeDir) {
|
|
|
135
135
|
* `input_tokens` is the TOTAL input including cache hits (OpenAI-style
|
|
136
136
|
* accounting, confirmed against `total_tokens` in the rollout token_count),
|
|
137
137
|
* and `cached_input_tokens` is the cache-read subset — so the uncached
|
|
138
|
-
* bucket subtracts the cached portion to avoid double counting.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
138
|
+
* bucket subtracts the cached portion to avoid double counting.
|
|
139
|
+
* `cached_input_tokens` rides every payload (zero included), so
|
|
140
|
+
* `cacheReadTokens` is written whenever the field is present — a reader can
|
|
141
|
+
* tell "no hits" from "not reported". Codex also reports cache creation
|
|
142
|
+
* (`cache_write_input_tokens`), mapped to `cacheWriteTokens` (the shared
|
|
143
|
+
* four-bucket contract: uncached input, output, cache read, cache write).
|
|
142
144
|
* @param usage - the raw codex usage object (`turn.completed`'s `usage` or a
|
|
143
145
|
* rollout `token_count` `info.last_token_usage`/`total_token_usage`).
|
|
144
146
|
* @returns the shared usage record.
|
|
@@ -147,12 +149,14 @@ function usageFromCodex(usage) {
|
|
|
147
149
|
const raw = usage;
|
|
148
150
|
const input = Number(raw.input_tokens);
|
|
149
151
|
const cached = Number(raw.cached_input_tokens);
|
|
152
|
+
const cacheWrite = Number(raw.cache_write_input_tokens);
|
|
150
153
|
const output = Number(raw.output_tokens);
|
|
151
154
|
const usageRecord = {
|
|
152
|
-
inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached) : Number.isFinite(input) ? input : 0,
|
|
155
|
+
inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0)) : Number.isFinite(input) ? input : 0,
|
|
153
156
|
outputTokens: Number.isFinite(output) ? output : 0
|
|
154
157
|
};
|
|
155
|
-
if (Number.isFinite(cached)
|
|
158
|
+
if (Number.isFinite(cached)) usageRecord.cacheReadTokens = cached;
|
|
159
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0) usageRecord.cacheWriteTokens = cacheWrite;
|
|
156
160
|
return usageRecord;
|
|
157
161
|
}
|
|
158
162
|
/**
|
|
@@ -740,11 +744,6 @@ const DEFAULT_LIVE_CHANNEL_RETRY_MS = 3e5;
|
|
|
740
744
|
const RECLAIM_EOF_GRACE_MS = 1e3;
|
|
741
745
|
/** Default minimum interval between one streaming item's snapshot messages. */
|
|
742
746
|
const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
|
|
743
|
-
/**
|
|
744
|
-
* The app-server channel could not come up (spawn failure or handshake
|
|
745
|
-
* timeout/mismatch). The provider catches exactly this and falls back to the
|
|
746
|
-
* exec one-shot; any other error is a real round failure.
|
|
747
|
-
*/
|
|
748
747
|
var LiveChannelUnavailableError = class extends Error {
|
|
749
748
|
constructor(message) {
|
|
750
749
|
super(message);
|
|
@@ -826,11 +825,14 @@ function codexAppServerItemToLine(item) {
|
|
|
826
825
|
case "dynamicToolCall": {
|
|
827
826
|
const tool = typeof item["tool"] === "string" ? item.tool : "dynamic";
|
|
828
827
|
const args = item["arguments"] === void 0 || item["arguments"] === null ? void 0 : typeof item["arguments"] === "string" ? item["arguments"] : JSON.stringify(item["arguments"]);
|
|
828
|
+
const content = item["contentItems"];
|
|
829
|
+
const result = Array.isArray(content) ? content.filter((block) => typeof block === "object" && block !== null).map((block) => typeof block["text"] === "string" ? block["text"] : void 0).filter((text) => text !== void 0 && text !== "").join("\n") : void 0;
|
|
829
830
|
return {
|
|
830
831
|
kind: "tool",
|
|
831
832
|
id: itemId ?? `codex-live-${randomUUID()}`,
|
|
832
833
|
name: tool,
|
|
833
|
-
...args === void 0 || args === "" ? {} : { args }
|
|
834
|
+
...args === void 0 || args === "" ? {} : { args },
|
|
835
|
+
...result === void 0 || result === "" ? {} : { result }
|
|
834
836
|
};
|
|
835
837
|
}
|
|
836
838
|
case "collabAgentToolCall": {
|
|
@@ -861,12 +863,14 @@ function codexAppServerItemToLine(item) {
|
|
|
861
863
|
function usageFromAppServer(breakdown) {
|
|
862
864
|
const input = Number(breakdown["inputTokens"]);
|
|
863
865
|
const cached = Number(breakdown["cachedInputTokens"]);
|
|
866
|
+
const cacheWrite = Number(breakdown["cacheWriteInputTokens"]);
|
|
864
867
|
const output = Number(breakdown["outputTokens"]);
|
|
865
868
|
const usage = {
|
|
866
|
-
inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached) : Number.isFinite(input) ? input : 0,
|
|
869
|
+
inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0)) : Number.isFinite(input) ? input : 0,
|
|
867
870
|
outputTokens: Number.isFinite(output) ? output : 0
|
|
868
871
|
};
|
|
869
|
-
if (Number.isFinite(cached)
|
|
872
|
+
if (Number.isFinite(cached)) usage.cacheReadTokens = cached;
|
|
873
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
870
874
|
return usage;
|
|
871
875
|
}
|
|
872
876
|
/**
|
|
@@ -1428,27 +1432,31 @@ var CodexLiveDriver = class {
|
|
|
1428
1432
|
const lines = [];
|
|
1429
1433
|
let mirrored = 0;
|
|
1430
1434
|
let usage;
|
|
1431
|
-
/**
|
|
1432
|
-
let
|
|
1435
|
+
/** The cumulative turn total the wire's `tokenUsage.total` names, for the settled report. */
|
|
1436
|
+
let totalUsage;
|
|
1433
1437
|
/**
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
* mid-stream therefore folds ABOVE the stream's step and the projection
|
|
1439
|
-
* renders the round's chronological order.
|
|
1438
|
+
* Per-call usage by transcript line: each `thread/tokenUsage/updated`
|
|
1439
|
+
* notification closes one API call (its items have all completed), and
|
|
1440
|
+
* the call's LAST line carries its `tokenUsage.last`. The map is
|
|
1441
|
+
* authoritative per line, independent of the flush's withUsage flag.
|
|
1440
1442
|
*/
|
|
1441
|
-
const
|
|
1443
|
+
const usageByLine = /* @__PURE__ */ new Map();
|
|
1444
|
+
/** Whether a folded line already carries usage (per-call, or the round's fallback — no backfill event exists). */
|
|
1445
|
+
let usageCarried = false;
|
|
1442
1446
|
/**
|
|
1443
|
-
* The streaming items seen this round by item id:
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1447
|
+
* The streaming items seen this round by item id: deltas accumulate into
|
|
1448
|
+
* throttled snapshots at the item's step once it may open in the log
|
|
1449
|
+
* (previewed live before that) — the host folds repeated settles at one
|
|
1450
|
+
* coordinate into one live-updating chat node. The item's completion
|
|
1451
|
+
* folds at the same step and finalizes it; an entry whose completion
|
|
1452
|
+
* never arrives is force-finalized at settle. A tool that completes
|
|
1453
|
+
* mid-stream folds after the stream, keeping the round's chronology.
|
|
1450
1454
|
*/
|
|
1451
1455
|
const streams = /* @__PURE__ */ new Map();
|
|
1456
|
+
/** Provisional stream keys (live flush and preview identity), never log steps. */
|
|
1457
|
+
let streamKeys = 0;
|
|
1458
|
+
/** Line indexes already written (a line may fold ahead of an earlier one it chronologically precedes). */
|
|
1459
|
+
const folded = /* @__PURE__ */ new Set();
|
|
1452
1460
|
/** The item deltas currently accumulate into (items stream sequentially). */
|
|
1453
1461
|
let activeStream;
|
|
1454
1462
|
/** Items/completions that arrived before the turn id was known. */
|
|
@@ -1492,6 +1500,27 @@ var CodexLiveDriver = class {
|
|
|
1492
1500
|
*/
|
|
1493
1501
|
const streamPublisher = localAgent?.liveStreams === void 0 ? void 0 : new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, (error) => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
|
|
1494
1502
|
const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
1503
|
+
let lastStep = 0;
|
|
1504
|
+
let openStep;
|
|
1505
|
+
const openStreamStep = (stream) => {
|
|
1506
|
+
lastStep += 1;
|
|
1507
|
+
childSession.append("step/start", {
|
|
1508
|
+
turn,
|
|
1509
|
+
step: lastStep
|
|
1510
|
+
});
|
|
1511
|
+
stream.durable = lastStep;
|
|
1512
|
+
stream.opened = true;
|
|
1513
|
+
openStep = lastStep;
|
|
1514
|
+
};
|
|
1515
|
+
const closeOpenStep = () => {
|
|
1516
|
+
if (openStep === void 0) return;
|
|
1517
|
+
childSession.append("step/end", {
|
|
1518
|
+
turn,
|
|
1519
|
+
step: openStep
|
|
1520
|
+
});
|
|
1521
|
+
openStep = void 0;
|
|
1522
|
+
};
|
|
1523
|
+
const canOpenStream = (stream) => openStep === void 0 && mirrored >= stream.after && streams.keys().next().value === stream.itemId;
|
|
1495
1524
|
const pendingStreamArrival = /* @__PURE__ */ new Map();
|
|
1496
1525
|
const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
|
|
1497
1526
|
if (stream.text.trim() === "") return;
|
|
@@ -1504,30 +1533,29 @@ var CodexLiveDriver = class {
|
|
|
1504
1533
|
liveFlush.cancel(stream.step);
|
|
1505
1534
|
const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
|
|
1506
1535
|
pendingStreamArrival.delete(stream.step);
|
|
1507
|
-
if (
|
|
1508
|
-
if (
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1536
|
+
if (!stream.opened) {
|
|
1537
|
+
if (canOpenStream(stream)) openStreamStep(stream);
|
|
1538
|
+
else if (!final) {
|
|
1539
|
+
streamPublisher?.preview(stream.step, stream.kind, stream.text, {
|
|
1540
|
+
itemId: stream.itemId,
|
|
1541
|
+
receivedAt
|
|
1512
1542
|
});
|
|
1513
|
-
|
|
1543
|
+
return;
|
|
1544
|
+
} else {
|
|
1545
|
+
closeOpenStep();
|
|
1546
|
+
openStreamStep(stream);
|
|
1514
1547
|
}
|
|
1515
|
-
|
|
1548
|
+
}
|
|
1549
|
+
if (streamPublisher !== void 0 && !final) {
|
|
1550
|
+
streamPublisher.update(stream.durable, stream.kind, stream.text, { receivedAt });
|
|
1516
1551
|
stream.lastSnapshotAt = now;
|
|
1517
1552
|
stream.lastSnapshotLen = stream.text.length;
|
|
1518
1553
|
return;
|
|
1519
1554
|
}
|
|
1520
|
-
|
|
1521
|
-
childSession.append("step/start", {
|
|
1522
|
-
turn,
|
|
1523
|
-
step: stream.step
|
|
1524
|
-
});
|
|
1525
|
-
stream.opened = true;
|
|
1526
|
-
}
|
|
1527
|
-
const attachUsage = withUsage && !usageCarried && usage !== void 0;
|
|
1555
|
+
const attachUsage = withUsage && !usageCarried && usageByLine.size === 0 && usage !== void 0;
|
|
1528
1556
|
childSession.append("assistant/message", {
|
|
1529
1557
|
turn,
|
|
1530
|
-
step: stream.
|
|
1558
|
+
step: stream.durable,
|
|
1531
1559
|
message: codexAssistantEvent([stream.kind === "think" ? {
|
|
1532
1560
|
type: "reasoning",
|
|
1533
1561
|
text: stream.text
|
|
@@ -1536,7 +1564,7 @@ var CodexLiveDriver = class {
|
|
|
1536
1564
|
text: stream.text
|
|
1537
1565
|
}]),
|
|
1538
1566
|
stream: [],
|
|
1539
|
-
...withUsage && !usageCarried && usage !== void 0 ? { usage } : {},
|
|
1567
|
+
...withUsage && !usageCarried && usageByLine.size === 0 && usage !== void 0 ? { usage } : {},
|
|
1540
1568
|
...interrupted ? { interrupted: true } : {}
|
|
1541
1569
|
}, { surfaceOp: "append" });
|
|
1542
1570
|
if (attachUsage) usageCarried = true;
|
|
@@ -1544,11 +1572,19 @@ var CodexLiveDriver = class {
|
|
|
1544
1572
|
stream.lastSnapshotLen = stream.text.length;
|
|
1545
1573
|
persist();
|
|
1546
1574
|
};
|
|
1575
|
+
/** Close a stream's live presentation once its final text is in the log. */
|
|
1576
|
+
const finishStream = (stream) => {
|
|
1577
|
+
liveFlush.cancel(stream.step);
|
|
1578
|
+
streams.delete(stream.itemId);
|
|
1579
|
+
if (activeStream === stream.itemId) activeStream = void 0;
|
|
1580
|
+
streamPublisher?.finish(stream.step);
|
|
1581
|
+
if (stream.durable !== void 0) streamPublisher?.finish(stream.durable);
|
|
1582
|
+
};
|
|
1547
1583
|
/**
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
* different item's deltas force one
|
|
1551
|
-
*
|
|
1584
|
+
* Track one new streaming item: its provisional key (live flush and
|
|
1585
|
+
* preview identity) and how many lines had completed when it began. A
|
|
1586
|
+
* different item's deltas force one freshness snapshot of the current
|
|
1587
|
+
* stream.
|
|
1552
1588
|
*/
|
|
1553
1589
|
const reserveStream = (itemId, kind) => {
|
|
1554
1590
|
if (activeStream !== void 0 && activeStream !== itemId) {
|
|
@@ -1558,12 +1594,12 @@ var CodexLiveDriver = class {
|
|
|
1558
1594
|
}
|
|
1559
1595
|
let stream = streams.get(itemId);
|
|
1560
1596
|
if (stream === void 0) {
|
|
1561
|
-
|
|
1562
|
-
reservedSteps.push(step);
|
|
1597
|
+
streamKeys += 1;
|
|
1563
1598
|
stream = {
|
|
1564
1599
|
itemId,
|
|
1565
1600
|
kind,
|
|
1566
|
-
step,
|
|
1601
|
+
step: streamKeys,
|
|
1602
|
+
after: lines.length,
|
|
1567
1603
|
text: "",
|
|
1568
1604
|
lastSnapshotAt: 0,
|
|
1569
1605
|
lastSnapshotLen: 0,
|
|
@@ -1573,60 +1609,78 @@ var CodexLiveDriver = class {
|
|
|
1573
1609
|
}
|
|
1574
1610
|
activeStream = itemId;
|
|
1575
1611
|
};
|
|
1576
|
-
/** The
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1612
|
+
/** The stream a completed line finalizes, if it was streamed. */
|
|
1613
|
+
const streamOfLine = (line) => line === void 0 || line.kind === "tool" ? void 0 : streams.get(line.itemId ?? "");
|
|
1614
|
+
/** Write lines[index] at the next step(s). */
|
|
1615
|
+
const foldLine = (index, lineUsage) => {
|
|
1616
|
+
const line = lines[index];
|
|
1617
|
+
const stream = streamOfLine(line);
|
|
1618
|
+
if (stream !== void 0) {
|
|
1619
|
+
if (!stream.opened) {
|
|
1620
|
+
closeOpenStep();
|
|
1621
|
+
openStreamStep(stream);
|
|
1622
|
+
}
|
|
1623
|
+
const blocks = stream.kind === "think" ? [{
|
|
1624
|
+
type: "reasoning",
|
|
1625
|
+
text: line.kind === "tool" ? "" : line.text
|
|
1626
|
+
}] : [{
|
|
1627
|
+
type: "text",
|
|
1628
|
+
text: line.kind === "tool" ? "" : line.text
|
|
1629
|
+
}];
|
|
1630
|
+
childSession.append("assistant/message", {
|
|
1631
|
+
turn,
|
|
1632
|
+
step: stream.durable,
|
|
1633
|
+
message: codexAssistantEvent(blocks),
|
|
1634
|
+
stream: [],
|
|
1635
|
+
...lineUsage === void 0 ? {} : { usage: lineUsage }
|
|
1636
|
+
}, { surfaceOp: "append" });
|
|
1637
|
+
finishStream(stream);
|
|
1638
|
+
closeOpenStep();
|
|
1639
|
+
} else {
|
|
1640
|
+
closeOpenStep();
|
|
1641
|
+
lastStep += 1;
|
|
1642
|
+
appendCodexTranscriptLine(childSession, turn, lastStep, line, lineUsage);
|
|
1643
|
+
}
|
|
1644
|
+
folded.add(index);
|
|
1645
|
+
localAgent?.reportRunProgress(childSession.id, {
|
|
1646
|
+
kind: "delta",
|
|
1647
|
+
text: codexLineText(line)
|
|
1648
|
+
});
|
|
1582
1649
|
};
|
|
1583
|
-
/**
|
|
1650
|
+
/**
|
|
1651
|
+
* Mirror the completed lines before `upto` in chronological order; the
|
|
1652
|
+
* last line is held back until completion. A line waits behind every
|
|
1653
|
+
* stream that began before it completed: that stream's line folds first,
|
|
1654
|
+
* or — while it is still streaming — the mirror stops until it lands.
|
|
1655
|
+
*/
|
|
1584
1656
|
const mirrorUpTo = (upto, withUsage) => {
|
|
1585
1657
|
let usageIndex = -1;
|
|
1586
|
-
if (withUsage) {
|
|
1658
|
+
if (withUsage && usageByLine.size === 0) {
|
|
1587
1659
|
for (let index = 0; index < lines.length; index += 1) if (lines[index]?.kind !== "tool") usageIndex = index;
|
|
1588
1660
|
}
|
|
1589
|
-
|
|
1590
|
-
const
|
|
1591
|
-
if (
|
|
1661
|
+
const usageFor = (index) => {
|
|
1662
|
+
const own = usageByLine.get(index);
|
|
1663
|
+
if (own !== void 0) {
|
|
1664
|
+
usageCarried = true;
|
|
1665
|
+
return own;
|
|
1666
|
+
}
|
|
1592
1667
|
const lineUsage = withUsage && index === usageIndex ? usage : void 0;
|
|
1593
1668
|
if (lineUsage !== void 0) usageCarried = true;
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
}] : [{
|
|
1608
|
-
type: "text",
|
|
1609
|
-
text: line.kind === "tool" ? "" : line.text
|
|
1610
|
-
}];
|
|
1611
|
-
childSession.append("assistant/message", {
|
|
1612
|
-
turn,
|
|
1613
|
-
step: stream.step,
|
|
1614
|
-
message: codexAssistantEvent(blocks),
|
|
1615
|
-
stream: [],
|
|
1616
|
-
...lineUsage === void 0 ? {} : { usage: lineUsage }
|
|
1617
|
-
}, { surfaceOp: "append" });
|
|
1618
|
-
streamPublisher?.finish(stream.step);
|
|
1619
|
-
childSession.append("step/end", {
|
|
1620
|
-
turn,
|
|
1621
|
-
step: stream.step
|
|
1622
|
-
});
|
|
1623
|
-
} else appendCodexTranscriptLine(childSession, turn, foldStep(index), line, lineUsage);
|
|
1624
|
-
mirrored = index + 1;
|
|
1625
|
-
localAgent?.reportRunProgress(childSession.id, {
|
|
1626
|
-
kind: "delta",
|
|
1627
|
-
text: codexLineText(line)
|
|
1628
|
-
});
|
|
1669
|
+
return lineUsage;
|
|
1670
|
+
};
|
|
1671
|
+
outer: for (let index = mirrored; index < upto; index += 1) {
|
|
1672
|
+
if (folded.has(index)) continue;
|
|
1673
|
+
const own = streamOfLine(lines[index]);
|
|
1674
|
+
const all = [...streams.values()];
|
|
1675
|
+
const before = own === void 0 ? all.filter((stream) => stream.after <= index) : all.slice(0, all.indexOf(own));
|
|
1676
|
+
for (const earlier of before) {
|
|
1677
|
+
const at = lines.findIndex((line, position) => position > index && position < upto && !folded.has(position) && streamOfLine(line) === earlier);
|
|
1678
|
+
if (at < 0) break outer;
|
|
1679
|
+
foldLine(at, usageFor(at));
|
|
1680
|
+
}
|
|
1681
|
+
foldLine(index, usageFor(index));
|
|
1629
1682
|
}
|
|
1683
|
+
while (folded.has(mirrored)) mirrored += 1;
|
|
1630
1684
|
if (upto > 0) persist();
|
|
1631
1685
|
};
|
|
1632
1686
|
let resolveCompletion;
|
|
@@ -1635,6 +1689,11 @@ var CodexLiveDriver = class {
|
|
|
1635
1689
|
});
|
|
1636
1690
|
const openTurn = () => {
|
|
1637
1691
|
childSession.append("turn/start", { turn });
|
|
1692
|
+
if (spec.configuration?.model !== void 0) appendCodexRequestHeader(childSession, {
|
|
1693
|
+
provider: "codex-local",
|
|
1694
|
+
model: spec.configuration.model,
|
|
1695
|
+
...spec.configuration.effort === void 0 ? {} : { reasoningEffort: spec.configuration.effort }
|
|
1696
|
+
});
|
|
1638
1697
|
childSession.append("user/message", createUserMessage({
|
|
1639
1698
|
content: [{
|
|
1640
1699
|
type: "text",
|
|
@@ -1685,17 +1744,29 @@ var CodexLiveDriver = class {
|
|
|
1685
1744
|
return;
|
|
1686
1745
|
}
|
|
1687
1746
|
if (method === "thread/tokenUsage/updated") {
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1747
|
+
const tokenUsage = params["tokenUsage"];
|
|
1748
|
+
const last = tokenUsage?.["last"];
|
|
1749
|
+
const total = tokenUsage?.["total"];
|
|
1750
|
+
if (total !== void 0 && (Number.isFinite(Number(total["inputTokens"])) || Number.isFinite(Number(total["outputTokens"])))) totalUsage = usageFromAppServer(total);
|
|
1751
|
+
if (last !== void 0) {
|
|
1752
|
+
usage = usageFromAppServer(last);
|
|
1753
|
+
if (lines.length > 0) {
|
|
1754
|
+
usageByLine.set(lines.length - 1, usage);
|
|
1755
|
+
mirrorUpTo(lines.length - 1, true);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1690
1758
|
return;
|
|
1691
1759
|
}
|
|
1692
1760
|
if (method === "turn/completed") {
|
|
1693
1761
|
const turnData = params["turn"];
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1762
|
+
try {
|
|
1763
|
+
mirrorUpTo(lines.length, true);
|
|
1764
|
+
} finally {
|
|
1765
|
+
resolveCompletion({
|
|
1766
|
+
status: String(turnData["status"]),
|
|
1767
|
+
...turnData["error"] === null || turnData["error"] === void 0 ? {} : { error: turnData["error"] }
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1699
1770
|
}
|
|
1700
1771
|
};
|
|
1701
1772
|
/** Route one wire notification: round-scoped, turn-id tagged, early ones buffered. */
|
|
@@ -1795,15 +1866,19 @@ var CodexLiveDriver = class {
|
|
|
1795
1866
|
liveFlush.dispose();
|
|
1796
1867
|
if (turnOpened) {
|
|
1797
1868
|
if (mirrored < lines.length) mirrorUpTo(lines.length, true);
|
|
1869
|
+
while (mirrored < lines.length && streams.size > 0) {
|
|
1870
|
+
const oldest = streams.values().next().value;
|
|
1871
|
+
appendStreamSnapshot(oldest, true, settled.stopReason !== "completed", false, true);
|
|
1872
|
+
finishStream(oldest);
|
|
1873
|
+
closeOpenStep();
|
|
1874
|
+
mirrorUpTo(lines.length, true);
|
|
1875
|
+
}
|
|
1798
1876
|
if (streams.size > 0) {
|
|
1799
1877
|
const remaining = [...streams.values()];
|
|
1800
1878
|
for (const [position, stream] of remaining.entries()) {
|
|
1801
1879
|
appendStreamSnapshot(stream, true, settled.stopReason !== "completed", position === remaining.length - 1, true);
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
turn,
|
|
1805
|
-
step: stream.step
|
|
1806
|
-
});
|
|
1880
|
+
finishStream(stream);
|
|
1881
|
+
closeOpenStep();
|
|
1807
1882
|
}
|
|
1808
1883
|
streams.clear();
|
|
1809
1884
|
activeStream = void 0;
|
|
@@ -1854,7 +1929,7 @@ var CodexLiveDriver = class {
|
|
|
1854
1929
|
windowStart: startedAtMs,
|
|
1855
1930
|
cwd: spec.cwd
|
|
1856
1931
|
});
|
|
1857
|
-
const settledUsage = usage ?? facts.usage;
|
|
1932
|
+
const settledUsage = totalUsage ?? usage ?? facts.usage;
|
|
1858
1933
|
const round = {
|
|
1859
1934
|
...facts.model === void 0 ? {} : { observedModel: facts.model },
|
|
1860
1935
|
...facts.cliVersion === void 0 ? {} : { cliVersion: facts.cliVersion },
|
|
@@ -2046,23 +2121,6 @@ async function listCodexConfigModels(homeDir) {
|
|
|
2046
2121
|
}
|
|
2047
2122
|
return found;
|
|
2048
2123
|
}
|
|
2049
|
-
//#endregion
|
|
2050
|
-
//#region lib/types/codex-cli-provider.js
|
|
2051
|
-
/**
|
|
2052
|
-
* One-shot Codex CLI subagent lifecycle: spawn `codex exec "<task>"` through
|
|
2053
|
-
* the subprocess seam under the harness's scoped home, capture its printed
|
|
2054
|
-
* response as the run output, and dispose to whole-tree quiescence. Mirrors
|
|
2055
|
-
* the kimi-cli provider: every accepted run is a fresh process and a fresh
|
|
2056
|
-
* codex session; there is no continuation across runs.
|
|
2057
|
-
*
|
|
2058
|
-
* The provider name `codex-local` avoids colliding with the official
|
|
2059
|
-
* `subagent-codex` package's provider name `codex` — a composition that
|
|
2060
|
-
* mounts both would fail loud with DUPLICATE_PROVIDER. The tool row in this
|
|
2061
|
-
* bundle's patch takes the official model-facing name `subagent_codex`
|
|
2062
|
-
* instead: the official preset row ships disabled, and the patch disables
|
|
2063
|
-
* it too (a deliberate user re-enable conflicts loud, by design).
|
|
2064
|
-
* @module @khorsheed/dsh-local-agent-codex/codex-cli-provider
|
|
2065
|
-
*/
|
|
2066
2124
|
/** Quote one TOML basic string for the `-c` config override. */
|
|
2067
2125
|
function tomlString(value) {
|
|
2068
2126
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
@@ -2086,7 +2144,7 @@ function registerCodexMemberRun(ctx, childSessionId, parentSessionId) {
|
|
|
2086
2144
|
});
|
|
2087
2145
|
const serverName = `dsh-member-${token.slice(0, 8)}`;
|
|
2088
2146
|
const bridge = registry.memberBridgeCommand();
|
|
2089
|
-
const configOverride = `mcp_servers.${serverName}={command=${tomlString(bridge.command)},args=[${bridge.args.map(tomlString).join(",")}],env={${MEMBER_BRIDGE_SOCKET_ENV}=${tomlString(registry.memberBridgeSocketPath())},${MEMBER_BRIDGE_TOKEN_ENV}=${tomlString(token)}},default_tools_approval_mode=${tomlString("approve")}}`;
|
|
2147
|
+
const configOverride = `mcp_servers.${serverName}={command=${tomlString(bridge.command)},args=[${bridge.args.map(tomlString).join(",")}],env={${MEMBER_BRIDGE_SOCKET_ENV}=${tomlString(registry.memberBridgeSocketPath())},${MEMBER_BRIDGE_TOKEN_ENV}=${tomlString(token)}},default_tools_approval_mode=${tomlString("approve")},tool_timeout_sec=1800}`;
|
|
2090
2148
|
let released = false;
|
|
2091
2149
|
return {
|
|
2092
2150
|
token,
|
|
@@ -2557,6 +2615,23 @@ function foldCodexStreamLine(state, raw) {
|
|
|
2557
2615
|
result: last.result === void 0 ? output : `${last.result}\n${output}`
|
|
2558
2616
|
};
|
|
2559
2617
|
}
|
|
2618
|
+
} else if (item.type === "custom_tool_call") {
|
|
2619
|
+
countToolCall(state, "custom_tool_call");
|
|
2620
|
+
state.lines.push({
|
|
2621
|
+
kind: "tool",
|
|
2622
|
+
id: typeof item.id === "string" ? item.id : `codex-tool-${state.lines.length}`,
|
|
2623
|
+
name: item.name ?? "custom",
|
|
2624
|
+
...typeof item.input === "string" && item.input !== "" ? { args: item.input } : {}
|
|
2625
|
+
});
|
|
2626
|
+
} else if (item.type === "custom_tool_call_output") {
|
|
2627
|
+
const output = typeof item.output === "string" ? item.output : Array.isArray(item.output) ? item.output.filter((block) => typeof block === "object" && block !== null && typeof block.text === "string").map((block) => block.text).join("\n") : void 0;
|
|
2628
|
+
if (output !== void 0 && output.trim() !== "") {
|
|
2629
|
+
const last = state.lines[state.lines.length - 1];
|
|
2630
|
+
if (last !== void 0 && last.kind === "tool") state.lines[state.lines.length - 1] = {
|
|
2631
|
+
...last,
|
|
2632
|
+
result: last.result === void 0 ? output : `${last.result}\n${output}`
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2560
2635
|
}
|
|
2561
2636
|
}
|
|
2562
2637
|
/**
|
|
@@ -2591,7 +2666,8 @@ var CodexStreamParser = class {
|
|
|
2591
2666
|
* not put one on the wire; the settle path then reads the rollout's
|
|
2592
2667
|
* turn_context). Item types covered: `reasoning` (thinking), `agent_message`
|
|
2593
2668
|
* (reply text), `command_execution` (shell command with aggregated output),
|
|
2594
|
-
* `web_search_call`,
|
|
2669
|
+
* `web_search_call`, `custom_tool_call`/`custom_tool_call_output` (deferred
|
|
2670
|
+
* and dynamic tools), and `function_call_output`. The last `agent_message`
|
|
2595
2671
|
* wins as the run output (the final answer), and `turn.completed` carries the
|
|
2596
2672
|
* turn's usage. Malformed lines are skipped.
|
|
2597
2673
|
* @param stream - the collected stdout NDJSON text.
|
|
@@ -2683,6 +2759,11 @@ function startCodexCliRun(request, spec) {
|
|
|
2683
2759
|
});
|
|
2684
2760
|
const startedAtMs = Date.now();
|
|
2685
2761
|
spec.childSession?.append("turn/start", { turn });
|
|
2762
|
+
if (spec.model !== void 0 && spec.childSession !== void 0) appendCodexRequestHeader(spec.childSession, {
|
|
2763
|
+
provider: "codex-local",
|
|
2764
|
+
model: spec.model,
|
|
2765
|
+
...spec.effort === void 0 ? {} : { reasoningEffort: spec.effort }
|
|
2766
|
+
});
|
|
2686
2767
|
const liveMirror = spec.childSession !== void 0 && spec.ctx !== void 0 ? createCodexLiveMirror(spec, task, turn) : void 0;
|
|
2687
2768
|
let output = "";
|
|
2688
2769
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -2770,7 +2851,16 @@ function startCodexCliRun(request, spec) {
|
|
|
2770
2851
|
onAbort
|
|
2771
2852
|
}).then((settled) => {
|
|
2772
2853
|
if (settled.stopReason === "completed") spec.onAuthSuccess?.();
|
|
2854
|
+
return settled;
|
|
2855
|
+
});
|
|
2856
|
+
result.then(() => child.done).then(async () => {
|
|
2857
|
+
if (spec.onAuthFailure !== void 0) {
|
|
2858
|
+
const authDetail = `${child.collected.stderr?.readFrom(0).text || stderr}\n${child.collected.stdout?.readFrom(0).text || output}`;
|
|
2859
|
+
if (CODEX_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
|
|
2860
|
+
}
|
|
2861
|
+
await mirrorCodexAfterExit(spec, task, turn, output, liveMirror, startedAtMs);
|
|
2773
2862
|
if (spec.childSession !== void 0) {
|
|
2863
|
+
const settled = await result;
|
|
2774
2864
|
if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
|
|
2775
2865
|
turn,
|
|
2776
2866
|
reason: { kind: "completed" }
|
|
@@ -2792,15 +2882,10 @@ function startCodexCliRun(request, spec) {
|
|
|
2792
2882
|
}
|
|
2793
2883
|
}
|
|
2794
2884
|
});
|
|
2885
|
+
if (spec.ctx !== void 0) await persistChildSession(spec.ctx, spec.childSession).catch((error) => {
|
|
2886
|
+
spec.ctx?.logger.warn(`subagent-codex: turn/end persistence failed: ${thrown(error).message}`);
|
|
2887
|
+
});
|
|
2795
2888
|
}
|
|
2796
|
-
return settled;
|
|
2797
|
-
});
|
|
2798
|
-
result.then(() => child.done).then(() => {
|
|
2799
|
-
if (spec.onAuthFailure !== void 0) {
|
|
2800
|
-
const authDetail = `${child.collected.stderr?.readFrom(0).text || stderr}\n${child.collected.stdout?.readFrom(0).text || output}`;
|
|
2801
|
-
if (CODEX_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
|
|
2802
|
-
}
|
|
2803
|
-
return mirrorCodexAfterExit(spec, task, turn, output, liveMirror, startedAtMs);
|
|
2804
2889
|
}, () => {});
|
|
2805
2890
|
return Promise.resolve(subprocessRunHandle({
|
|
2806
2891
|
id: SessionId(randomUUID()),
|
|
@@ -2811,6 +2896,22 @@ function startCodexCliRun(request, spec) {
|
|
|
2811
2896
|
teardown: disposeProcess
|
|
2812
2897
|
}));
|
|
2813
2898
|
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Write the round's request/header when the config differs from the child
|
|
2901
|
+
* log's last one (the host logs changed snapshots, not per-call duplicates):
|
|
2902
|
+
* the first write is `initial`, a differing one `change`, an identical one
|
|
2903
|
+
* skipped. Context Lens reads the child log's config from these headers.
|
|
2904
|
+
*/
|
|
2905
|
+
function appendCodexRequestHeader(childSession, config) {
|
|
2906
|
+
const serialized = JSON.stringify(config);
|
|
2907
|
+
let last;
|
|
2908
|
+
for (const event of childSession.snapshotEvents()) if (event.type === "request/header") last = JSON.stringify(event.data.header?.config);
|
|
2909
|
+
if (last === serialized) return;
|
|
2910
|
+
childSession.append("request/header", {
|
|
2911
|
+
header: { config },
|
|
2912
|
+
reason: last === void 0 ? "initial" : "change"
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2814
2915
|
/** One assistant-role message event, attributed to the codex route. */
|
|
2815
2916
|
function codexAssistantEvent(blocks) {
|
|
2816
2917
|
return createAssistantMessage({
|
|
@@ -2827,8 +2928,17 @@ function codexAssistantEvent(blocks) {
|
|
|
2827
2928
|
* assembler requires: without a boundary the step never enters the location
|
|
2828
2929
|
* index, the line's assistant message resolves to a turn-level location, and
|
|
2829
2930
|
* the real-time view renders nothing (only a full rebuild, which mints step
|
|
2830
|
-
* drafts from the explicit coordinates, recovers it).
|
|
2831
|
-
*
|
|
2931
|
+
* drafts from the explicit coordinates, recovers it).
|
|
2932
|
+
*
|
|
2933
|
+
* A tool line folds the NATIVE lifecycle inside its one step: an assistant
|
|
2934
|
+
* message advertising the tool-call block, the `tool/call`, then its
|
|
2935
|
+
* `tool/result` — the released-log reader pairs call and result against the
|
|
2936
|
+
* advertisement and resolves every advertised call before the step closes.
|
|
2937
|
+
* Every fold sees a COMPLETED item (the exec live mirror holds the volatile
|
|
2938
|
+
* last line; the live driver folds at item/completed), so a result the
|
|
2939
|
+
* stream has not recorded by fold time will never arrive — the call folds
|
|
2940
|
+
* with the synthesized interrupted result instead of leaving the step
|
|
2941
|
+
* unresolvable.
|
|
2832
2942
|
* @returns whether the line folded (false when the options skipped it).
|
|
2833
2943
|
*/
|
|
2834
2944
|
function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
|
|
@@ -2837,23 +2947,36 @@ function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
|
|
|
2837
2947
|
step
|
|
2838
2948
|
});
|
|
2839
2949
|
if (line.kind === "tool") {
|
|
2950
|
+
const args = line.args ?? "";
|
|
2951
|
+
childSession.append("assistant/message", {
|
|
2952
|
+
turn,
|
|
2953
|
+
step,
|
|
2954
|
+
message: codexAssistantEvent([{
|
|
2955
|
+
type: "tool-call",
|
|
2956
|
+
id: line.id,
|
|
2957
|
+
name: line.name,
|
|
2958
|
+
arguments: args
|
|
2959
|
+
}]),
|
|
2960
|
+
stream: [],
|
|
2961
|
+
...usage === void 0 ? {} : { usage }
|
|
2962
|
+
}, { surfaceOp: "append" });
|
|
2840
2963
|
const call = childSession.append("tool/call", {
|
|
2841
2964
|
turn,
|
|
2842
2965
|
step,
|
|
2843
2966
|
callId: line.id,
|
|
2844
2967
|
name: line.name,
|
|
2845
|
-
arguments:
|
|
2968
|
+
arguments: args
|
|
2846
2969
|
});
|
|
2847
|
-
|
|
2970
|
+
childSession.append("tool/result", {
|
|
2848
2971
|
turn,
|
|
2849
2972
|
step,
|
|
2850
2973
|
message: createToolResultMessage({
|
|
2851
2974
|
callId: line.id,
|
|
2852
2975
|
content: [{
|
|
2853
2976
|
type: "text",
|
|
2854
|
-
text: line.result
|
|
2977
|
+
text: line.result ?? "(工具调用被中断,未能记录返回结果)"
|
|
2855
2978
|
}],
|
|
2856
|
-
isError:
|
|
2979
|
+
isError: line.result === void 0
|
|
2857
2980
|
})
|
|
2858
2981
|
}, {
|
|
2859
2982
|
surfaceOp: "append",
|
|
@@ -3224,6 +3347,47 @@ function dedupeChoices(layers) {
|
|
|
3224
3347
|
}
|
|
3225
3348
|
return choices;
|
|
3226
3349
|
}
|
|
3350
|
+
/**
|
|
3351
|
+
* Scope the served directory to the surface asking for it (the same helper
|
|
3352
|
+
* lives in each provider's broker — fold into the family core when the
|
|
3353
|
+
* directory faces next evolve):
|
|
3354
|
+
*
|
|
3355
|
+
* - memberless (the card's add-model dialog, the invite picker): the
|
|
3356
|
+
* endpoint's served catalog only — the configuration/history extras the
|
|
3357
|
+
* broker's info extension adds for STATUS display must never become add
|
|
3358
|
+
* candidates;
|
|
3359
|
+
* - a member whose harness has a curated list: the list IS the vocabulary,
|
|
3360
|
+
* in the list's own order (the same rule `choices` already follows) — a
|
|
3361
|
+
* curated value the endpoint no longer serves stays as a bare row (the
|
|
3362
|
+
* card shows it the same way), and the member's current effective model is
|
|
3363
|
+
* kept visible as a bare entry when it sits outside the list, so a pre-list
|
|
3364
|
+
* binding never renders blank;
|
|
3365
|
+
* - a member without a curated list: the legacy extended vocabulary.
|
|
3366
|
+
*/
|
|
3367
|
+
function scopeServedDirectory(directory, opts) {
|
|
3368
|
+
if (opts.member && opts.curated.length === 0) return directory;
|
|
3369
|
+
const served = directory.entries.filter((entry) => entry.source === "native");
|
|
3370
|
+
if (!opts.member) return {
|
|
3371
|
+
...directory,
|
|
3372
|
+
entries: served
|
|
3373
|
+
};
|
|
3374
|
+
const byValue = new Map(served.flatMap((entry) => entry.resolvedModel === void 0 ? [[entry.value, entry]] : [[entry.value, entry], [entry.resolvedModel, entry]]));
|
|
3375
|
+
const entries = opts.curated.map((value) => byValue.get(value) ?? {
|
|
3376
|
+
value,
|
|
3377
|
+
label: value,
|
|
3378
|
+
source: "configuration"
|
|
3379
|
+
});
|
|
3380
|
+
const current = opts.current?.trim();
|
|
3381
|
+
if (current !== void 0 && current !== "" && !entries.some((entry) => entry.value === current || entry.resolvedModel === current)) entries.push({
|
|
3382
|
+
value: current,
|
|
3383
|
+
label: current,
|
|
3384
|
+
source: "configuration"
|
|
3385
|
+
});
|
|
3386
|
+
return {
|
|
3387
|
+
...directory,
|
|
3388
|
+
entries
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3227
3391
|
var CodexModelBroker = class {
|
|
3228
3392
|
deps;
|
|
3229
3393
|
constructor(deps) {
|
|
@@ -3275,14 +3439,19 @@ var CodexModelBroker = class {
|
|
|
3275
3439
|
async modelDirectory(childSessionId, refresh = false) {
|
|
3276
3440
|
const home = this.deps.homeDir(childSessionId);
|
|
3277
3441
|
if (refresh) await this.deps.refreshDirectory?.(home, this.deps.cwd?.(childSessionId));
|
|
3278
|
-
|
|
3442
|
+
const info = await this.modelInfo(childSessionId, childSessionId === void 0 ? void 0 : this.deps.ctx.localAgent.getDelegation(childSessionId)?.model);
|
|
3443
|
+
return scopeServedDirectory(info.directory ?? {
|
|
3279
3444
|
entries: [],
|
|
3280
3445
|
complete: false,
|
|
3281
3446
|
customInput: true,
|
|
3282
3447
|
status: "unsupported",
|
|
3283
3448
|
refreshing: false,
|
|
3284
3449
|
revision: 0
|
|
3285
|
-
}
|
|
3450
|
+
}, {
|
|
3451
|
+
member: childSessionId !== void 0,
|
|
3452
|
+
curated: this.deps.modelList?.(home) ?? [],
|
|
3453
|
+
current: info.effective
|
|
3454
|
+
});
|
|
3286
3455
|
}
|
|
3287
3456
|
async *followModelDirectory(childSessionId, signal) {
|
|
3288
3457
|
if (this.deps.followDirectory === void 0) {
|