@workerdeck/core 0.15.0 → 0.16.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/build/index.d.mts +53 -20
- package/build/index.mjs +579 -252
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
package/build/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
-
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, transcriptActivity } from "@workerdeck/protocol";
|
|
3
|
+
import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, replayCoalesceKey, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
|
|
5
5
|
import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
7
|
import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -10,7 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import { lookup } from "node:dns/promises";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
//#region src/attachments.ts
|
|
13
|
+
//#region src/lib/attachments.ts
|
|
14
14
|
/** The four the Anthropic API accepts. Notably absent: image/heic — an iPhone's
|
|
15
15
|
* native photo format, which clients must transcode before upload. */
|
|
16
16
|
const IMAGE_TYPES = new Set([
|
|
@@ -103,7 +103,7 @@ function decodeText(base64) {
|
|
|
103
103
|
return Buffer.from(base64, "base64").toString("utf8");
|
|
104
104
|
}
|
|
105
105
|
//#endregion
|
|
106
|
-
//#region src/input-queue.ts
|
|
106
|
+
//#region src/lib/input-queue.ts
|
|
107
107
|
/**
|
|
108
108
|
* Push-based single-consumer AsyncIterable bridging imperative sendMessage() calls
|
|
109
109
|
* into the streaming `prompt` the Agent SDK consumes.
|
|
@@ -162,7 +162,160 @@ var InputQueue = class {
|
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
164
|
//#endregion
|
|
165
|
-
//#region src/
|
|
165
|
+
//#region src/lib/patch.ts
|
|
166
|
+
/**
|
|
167
|
+
* Turning an engine's edit output into the wire's {@link FilePatch}.
|
|
168
|
+
*
|
|
169
|
+
* Both engines know exactly which lines of which file changed, and both say so
|
|
170
|
+
* in their own vocabulary: the Claude SDK hands over a `structuredPatch` array
|
|
171
|
+
* on `SDKUserMessage.tool_use_result`, codex puts a unified diff string on each
|
|
172
|
+
* `fileChange` item. A client can reconstruct neither — it has never seen the
|
|
173
|
+
* file — so anything not normalized here is a diff that renders without line
|
|
174
|
+
* numbers.
|
|
175
|
+
*
|
|
176
|
+
* Normalizing in the runner rather than in each client is the point: one shape
|
|
177
|
+
* reaches the wire, and the dashboard, the extension and the phone all render
|
|
178
|
+
* from it without a per-engine branch or a diff parser of their own.
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* The most lines a patch may put on the wire.
|
|
182
|
+
*
|
|
183
|
+
* A patch is replayed on every attach and captured into parking snapshots, so
|
|
184
|
+
* "the diff is big" must not become "this session is expensive to open forever".
|
|
185
|
+
* Whole hunks are kept or dropped — half a hunk has misleading line numbers —
|
|
186
|
+
* and the drop is flagged so a renderer can say the diff is partial instead of
|
|
187
|
+
* presenting it as the whole change.
|
|
188
|
+
*/
|
|
189
|
+
const MAX_PATCH_LINES = 400;
|
|
190
|
+
function capHunks(hunks) {
|
|
191
|
+
const kept = [];
|
|
192
|
+
let lines = 0;
|
|
193
|
+
for (const hunk of hunks) {
|
|
194
|
+
if (lines + hunk.lines.length > MAX_PATCH_LINES && kept.length > 0) return {
|
|
195
|
+
hunks: kept,
|
|
196
|
+
truncated: true
|
|
197
|
+
};
|
|
198
|
+
kept.push(hunk);
|
|
199
|
+
lines += hunk.lines.length;
|
|
200
|
+
}
|
|
201
|
+
return { hunks: kept };
|
|
202
|
+
}
|
|
203
|
+
/** Structural, not `instanceof`: this reads a field the SDK types as `unknown`,
|
|
204
|
+
* and a shape check is the only honest way to know what arrived. */
|
|
205
|
+
function isHunk(value) {
|
|
206
|
+
const hunk = value;
|
|
207
|
+
return !!hunk && typeof hunk.oldStart === "number" && typeof hunk.oldLines === "number" && typeof hunk.newStart === "number" && typeof hunk.newLines === "number" && Array.isArray(hunk.lines) && hunk.lines.every((line) => typeof line === "string");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A {@link FilePatch} from the Claude SDK's structured tool output
|
|
211
|
+
* (`SDKUserMessage.tool_use_result` for Edit/Write/NotebookEdit).
|
|
212
|
+
*
|
|
213
|
+
* Everything else on that object is deliberately left behind — `originalFile`
|
|
214
|
+
* alone is the entire pre-edit file, which is precisely what must not be logged
|
|
215
|
+
* (see `FilePatch`'s own note).
|
|
216
|
+
*/
|
|
217
|
+
function filePatchFromToolResult(result) {
|
|
218
|
+
const output = result;
|
|
219
|
+
if (!output || !Array.isArray(output.structuredPatch)) return void 0;
|
|
220
|
+
const hunks = output.structuredPatch.filter(isHunk);
|
|
221
|
+
if (hunks.length === 0) return void 0;
|
|
222
|
+
const { hunks: kept, truncated } = capHunks(hunks);
|
|
223
|
+
return {
|
|
224
|
+
...typeof output.filePath === "string" && { path: output.filePath },
|
|
225
|
+
...output.type === "create" || output.originalFile === null ? { kind: "create" } : output.type === "update" || typeof output.originalFile === "string" ? { kind: "update" } : {},
|
|
226
|
+
hunks: kept,
|
|
227
|
+
...truncated && { truncated }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** `@@ -oldStart,oldLines +newStart,newLines @@` — the counts are optional and
|
|
231
|
+
* mean 1 when absent, which is what a single-line hunk looks like. */
|
|
232
|
+
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
233
|
+
/**
|
|
234
|
+
* A {@link FilePatch} from a unified diff — codex's `fileChange.diff`.
|
|
235
|
+
*
|
|
236
|
+
* Only the hunks are read. A diff's `---`/`+++` header names the file, but codex
|
|
237
|
+
* already reports the path on the change itself, and a header path is often
|
|
238
|
+
* relative or `/dev/null`, so the caller's path is the one worth trusting.
|
|
239
|
+
*
|
|
240
|
+
* Returns undefined when there is no hunk header at all: that is not a unified
|
|
241
|
+
* diff, and inventing hunk numbers for it would put wrong line numbers on screen
|
|
242
|
+
* — worse than none.
|
|
243
|
+
*/
|
|
244
|
+
function parseUnifiedDiff(diff, path) {
|
|
245
|
+
const hunks = [];
|
|
246
|
+
let current;
|
|
247
|
+
for (const line of diff.split("\n")) {
|
|
248
|
+
const header = HUNK_HEADER.exec(line);
|
|
249
|
+
if (header) {
|
|
250
|
+
current = {
|
|
251
|
+
oldStart: Number(header[1]),
|
|
252
|
+
oldLines: header[2] === void 0 ? 1 : Number(header[2]),
|
|
253
|
+
newStart: Number(header[3]),
|
|
254
|
+
newLines: header[4] === void 0 ? 1 : Number(header[4]),
|
|
255
|
+
lines: []
|
|
256
|
+
};
|
|
257
|
+
hunks.push(current);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!current) continue;
|
|
261
|
+
if (line.startsWith(" ") || line.startsWith("-") || line.startsWith("+")) current.lines.push(line);
|
|
262
|
+
else if (line === "") current.lines.push(" ");
|
|
263
|
+
else current = void 0;
|
|
264
|
+
}
|
|
265
|
+
if (hunks.length === 0) return void 0;
|
|
266
|
+
const { hunks: kept, truncated } = capHunks(hunks);
|
|
267
|
+
return {
|
|
268
|
+
...path && { path },
|
|
269
|
+
hunks: kept,
|
|
270
|
+
...truncated && { truncated }
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/lib/normalize.ts
|
|
275
|
+
/** Does this message answer exactly one tool call? A patch is per-file-edit and
|
|
276
|
+
* the message says nothing about which of two results it describes, so anything
|
|
277
|
+
* else gets no patch rather than a diff pinned to the wrong call. */
|
|
278
|
+
function singleToolResult(message) {
|
|
279
|
+
const content = message.content;
|
|
280
|
+
if (!Array.isArray(content)) return false;
|
|
281
|
+
return content.filter((block) => block.type === "tool_result").length === 1;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The wrappers the CLI writes into the transcript when the *harness* is talking
|
|
285
|
+
* to the model rather than a person talking to the session.
|
|
286
|
+
*
|
|
287
|
+
* Deliberately a text test, and only these two. The live path has structure to
|
|
288
|
+
* go on (`isSynthetic`, `origin.kind`), but **the resumed path has none**: the
|
|
289
|
+
* SDK's `SessionMessage` carries exactly `message`, `uuid`, `session_id`,
|
|
290
|
+
* `parent_tool_use_id`, `parent_agent_id` and `timestamp` — every one of
|
|
291
|
+
* `isMeta`, `isSidechain`, `promptSource` and `origin` is dropped between the
|
|
292
|
+
* stored JSONL and what `getSessionMessages` hands back (verified against real
|
|
293
|
+
* transcripts). So on resume this is the only signal there is, and without it a
|
|
294
|
+
* `<task-notification>` blob comes back as a blue user row and a scrubber mark,
|
|
295
|
+
* as if someone had typed it.
|
|
296
|
+
*
|
|
297
|
+
* `<local-command-caveat>` is here for symmetry and cheap insurance: the SDK
|
|
298
|
+
* filters `isMeta` entries out of a resumed transcript itself today, which is
|
|
299
|
+
* not a contract anyone wrote down.
|
|
300
|
+
*
|
|
301
|
+
* What is *not* here matters as much:
|
|
302
|
+
* - `<local-command-stdout>` — the reducer turns it into a notice row on
|
|
303
|
+
* purpose; marking it synthetic would delete a row both paths show.
|
|
304
|
+
* - `<command-name>` — that is a person running a slash command. The reducer
|
|
305
|
+
* renders it as the command line they typed; hiding it would erase the turn's
|
|
306
|
+
* cause.
|
|
307
|
+
*/
|
|
308
|
+
const SYNTHETIC_USER_PREFIXES = ["<task-notification>", "<local-command-caveat>"];
|
|
309
|
+
/** First text block's leading tag, for the test above. Tool results and images
|
|
310
|
+
* carry no text and are never synthetic by this rule (a tool result is already
|
|
311
|
+
* a tool result to every renderer). */
|
|
312
|
+
function isSyntheticUserText(message) {
|
|
313
|
+
const content = message.content;
|
|
314
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((block) => block.type === "text")?.text : void 0;
|
|
315
|
+
if (typeof text !== "string") return false;
|
|
316
|
+
const head = text.trimStart();
|
|
317
|
+
return SYNTHETIC_USER_PREFIXES.some((prefix) => head.startsWith(prefix));
|
|
318
|
+
}
|
|
166
319
|
function toApiMessage(message) {
|
|
167
320
|
const m = message;
|
|
168
321
|
return {
|
|
@@ -351,14 +504,18 @@ function normalizeSdkMessage(msg) {
|
|
|
351
504
|
parentToolUseId: msg.parent_tool_use_id,
|
|
352
505
|
uuid: msg.uuid
|
|
353
506
|
};
|
|
354
|
-
case "user":
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
507
|
+
case "user": {
|
|
508
|
+
const message = toApiMessage(msg.message);
|
|
509
|
+
return {
|
|
510
|
+
type: "user_message",
|
|
511
|
+
message,
|
|
512
|
+
parentToolUseId: msg.parent_tool_use_id,
|
|
513
|
+
replay: "isReplay" in msg && msg.isReplay === true ? true : void 0,
|
|
514
|
+
synthetic: msg.isSynthetic === true || msg.origin?.kind === "task-notification" || isSyntheticUserText(message) ? true : void 0,
|
|
515
|
+
patch: singleToolResult(message) ? filePatchFromToolResult(msg.tool_use_result) : void 0,
|
|
516
|
+
uuid: msg.uuid
|
|
517
|
+
};
|
|
518
|
+
}
|
|
362
519
|
case "stream_event": return {
|
|
363
520
|
type: "stream_delta",
|
|
364
521
|
event: msg.event,
|
|
@@ -376,6 +533,10 @@ function normalizeSdkMessage(msg) {
|
|
|
376
533
|
errors: msg.subtype === "success" ? void 0 : msg.errors,
|
|
377
534
|
usage: msg.usage
|
|
378
535
|
};
|
|
536
|
+
case "conversation_reset": return {
|
|
537
|
+
type: "conversation_reset",
|
|
538
|
+
sdkSessionId: msg.new_conversation_id
|
|
539
|
+
};
|
|
379
540
|
case "rate_limit_event": return {
|
|
380
541
|
type: "rate_limit",
|
|
381
542
|
info: {
|
|
@@ -399,7 +560,41 @@ function normalizeSdkMessage(msg) {
|
|
|
399
560
|
}
|
|
400
561
|
}
|
|
401
562
|
//#endregion
|
|
402
|
-
//#region src/
|
|
563
|
+
//#region src/lib/replay.ts
|
|
564
|
+
/**
|
|
565
|
+
* Which buffered events a coalesced replay should skip: everything superseded
|
|
566
|
+
* by a later event with the same {@link replayCoalesceKey}.
|
|
567
|
+
*
|
|
568
|
+
* A **backwards** scan, keeping the first occurrence of each key — which is the
|
|
569
|
+
* whole trick. Walking forwards would need a second pass to know which of the
|
|
570
|
+
* fifty context readings was the last one; walking backwards, the first one you
|
|
571
|
+
* meet *is* the last one, and everything after it (in scan order) is history.
|
|
572
|
+
*
|
|
573
|
+
* Note what this does **not** do: it never reorders and never touches an event
|
|
574
|
+
* with no key. Transcript content is an ordered fold — a stream delta
|
|
575
|
+
* accumulates onto a message, a tool result attaches to a call that came
|
|
576
|
+
* earlier, a turn result finalizes — so it must arrive exactly as it was
|
|
577
|
+
* emitted. Only last-write-wins *state* is eligible, and `replayCoalesceKey`
|
|
578
|
+
* is where that judgement lives.
|
|
579
|
+
*
|
|
580
|
+
* `afterSeq` is honoured so the scan agrees with the caller's replay window: an
|
|
581
|
+
* event the caller was never going to send must not suppress one it was.
|
|
582
|
+
*/
|
|
583
|
+
function staleReplaySeqs(events, afterSeq) {
|
|
584
|
+
const stale = /* @__PURE__ */ new Set();
|
|
585
|
+
const seen = /* @__PURE__ */ new Set();
|
|
586
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
587
|
+
const event = events[index];
|
|
588
|
+
if (event.seq <= afterSeq) break;
|
|
589
|
+
const key = replayCoalesceKey(event);
|
|
590
|
+
if (key === void 0) continue;
|
|
591
|
+
if (seen.has(key)) stale.add(event.seq);
|
|
592
|
+
else seen.add(key);
|
|
593
|
+
}
|
|
594
|
+
return stale;
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region src/engines/claude/runner.ts
|
|
403
598
|
const DEFAULT_APPROVAL_TIMEOUT_MS$1 = 3e5;
|
|
404
599
|
/**
|
|
405
600
|
* One live Agent SDK session: owns the query() call, the streaming input queue, the
|
|
@@ -416,6 +611,15 @@ var SessionRunner = class {
|
|
|
416
611
|
#listeners = /* @__PURE__ */ new Set();
|
|
417
612
|
#seq = 0;
|
|
418
613
|
#activityCount = 0;
|
|
614
|
+
/**
|
|
615
|
+
* Seq of the latest `conversation_reset` event, 0 when none. The log itself is
|
|
616
|
+
* never truncated — it still carries the state-bearing events (`capabilities`,
|
|
617
|
+
* `system_init`, …) a fresh attacher depends on and which are not re-emitted —
|
|
618
|
+
* but `subscribe()` skips transcript *content* strictly below this mark, so a
|
|
619
|
+
* replay does not resurrect a cleared conversation. A later reset supersedes
|
|
620
|
+
* an earlier one by overwriting it.
|
|
621
|
+
*/
|
|
622
|
+
#resetSeq = 0;
|
|
419
623
|
#status = "starting";
|
|
420
624
|
#statusDetail;
|
|
421
625
|
#sdkSessionId;
|
|
@@ -432,6 +636,9 @@ var SessionRunner = class {
|
|
|
432
636
|
/** Last plan reported by the usage poll, so `plan_info` is emitted on change
|
|
433
637
|
* rather than once per turn. */
|
|
434
638
|
#subscriptionType;
|
|
639
|
+
/** The title the CLI gave this thread (see `#fetchEngineTitle`). Undefined
|
|
640
|
+
* until it has one — a session gets its summary a turn or two in. */
|
|
641
|
+
#engineTitle;
|
|
435
642
|
#started = false;
|
|
436
643
|
#closed = false;
|
|
437
644
|
#runPromise;
|
|
@@ -484,9 +691,20 @@ var SessionRunner = class {
|
|
|
484
691
|
lastActivityAt: this.#lastActivityAt
|
|
485
692
|
};
|
|
486
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Three sources, most-deliberate first: the host's own rename (`meta.title`),
|
|
696
|
+
* the title the CLI gave this thread (`#engineTitle`), then the first prompt
|
|
697
|
+
* truncated.
|
|
698
|
+
*
|
|
699
|
+
* The rename outranks everything by design — a person naming a session must
|
|
700
|
+
* not have it renamed under them by a model — which is also why the engine
|
|
701
|
+
* title is *only ever read* while `meta.title` is unset (see
|
|
702
|
+
* `#fetchEngineTitle`), rather than read and then discarded here.
|
|
703
|
+
*/
|
|
487
704
|
#title() {
|
|
488
705
|
const metaTitle = this.#config.meta?.title;
|
|
489
706
|
if (typeof metaTitle === "string" && metaTitle.length > 0) return metaTitle;
|
|
707
|
+
if (this.#engineTitle) return this.#engineTitle;
|
|
490
708
|
const prompt = this.#config.prompt;
|
|
491
709
|
if (!prompt) return void 0;
|
|
492
710
|
return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
|
|
@@ -616,9 +834,24 @@ var SessionRunner = class {
|
|
|
616
834
|
/**
|
|
617
835
|
* Replay buffered events with seq > afterSeq, then deliver live events.
|
|
618
836
|
* Returns an unsubscribe function.
|
|
837
|
+
*
|
|
838
|
+
* Replay honours the reset watermark: transcript content below the latest
|
|
839
|
+
* `conversation_reset` is skipped (the reducer would clear it again anyway,
|
|
840
|
+
* and a pre-reset client that never learned the reducer's case would render
|
|
841
|
+
* a conversation the engine has discarded), while state-bearing events —
|
|
842
|
+
* which are emitted once and never again — always replay. The reset event
|
|
843
|
+
* itself replays (the skip is strictly-below), which is what clears a
|
|
844
|
+
* reconnecting client still holding pre-reset rows; superseded resets are
|
|
845
|
+
* content below the newer one and are skipped with what they cleared.
|
|
619
846
|
*/
|
|
620
|
-
subscribe(listener, afterSeq = 0) {
|
|
621
|
-
|
|
847
|
+
subscribe(listener, afterSeq = 0, options) {
|
|
848
|
+
const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
|
|
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
|
+
}
|
|
622
855
|
this.#listeners.add(listener);
|
|
623
856
|
return () => this.#listeners.delete(listener);
|
|
624
857
|
}
|
|
@@ -676,14 +909,17 @@ var SessionRunner = class {
|
|
|
676
909
|
}
|
|
677
910
|
for (const m of messages) {
|
|
678
911
|
if (this.#closed) return;
|
|
679
|
-
if (m.type === "user")
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
912
|
+
if (m.type === "user") {
|
|
913
|
+
const message = toApiMessage(m.message);
|
|
914
|
+
this.#emit({
|
|
915
|
+
type: "user_message",
|
|
916
|
+
message,
|
|
917
|
+
parentToolUseId: m.parent_tool_use_id,
|
|
918
|
+
replay: true,
|
|
919
|
+
synthetic: isSyntheticUserText(message) ? true : void 0,
|
|
920
|
+
uuid: m.uuid
|
|
921
|
+
});
|
|
922
|
+
} else if (m.type === "assistant") this.#emit({
|
|
687
923
|
type: "assistant_message",
|
|
688
924
|
message: toApiMessage(m.message),
|
|
689
925
|
parentToolUseId: m.parent_tool_use_id,
|
|
@@ -738,6 +974,7 @@ var SessionRunner = class {
|
|
|
738
974
|
this.#fetchCapabilities();
|
|
739
975
|
this.#fetchContextUsage();
|
|
740
976
|
this.#fetchRateLimits();
|
|
977
|
+
this.#fetchEngineTitle();
|
|
741
978
|
return;
|
|
742
979
|
}
|
|
743
980
|
if (msg.type === "system" && msg.subtype === "session_state_changed") {
|
|
@@ -749,12 +986,17 @@ var SessionRunner = class {
|
|
|
749
986
|
const body = normalizeSdkMessage(msg);
|
|
750
987
|
if (body) {
|
|
751
988
|
this.#emit(body);
|
|
989
|
+
if (body.type === "conversation_reset") {
|
|
990
|
+
if (body.sdkSessionId) this.#sdkSessionId = body.sdkSessionId;
|
|
991
|
+
this.#fetchContextUsage();
|
|
992
|
+
}
|
|
752
993
|
if (body.type === "turn_result") {
|
|
753
994
|
this.#totalCostUsd = body.totalCostUsd;
|
|
754
995
|
this.#numTurns = body.numTurns;
|
|
755
996
|
if (this.#pending.size === 0) this.#setStatus("idle");
|
|
756
997
|
this.#fetchContextUsage();
|
|
757
998
|
this.#fetchRateLimits();
|
|
999
|
+
this.#fetchEngineTitle();
|
|
758
1000
|
}
|
|
759
1001
|
}
|
|
760
1002
|
}
|
|
@@ -784,6 +1026,44 @@ var SessionRunner = class {
|
|
|
784
1026
|
});
|
|
785
1027
|
} catch {}
|
|
786
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Adopt the title the CLI gave this thread — the "friendly title" it writes a
|
|
1031
|
+
* turn or two into a session, and the name a resumed thread already carries.
|
|
1032
|
+
*
|
|
1033
|
+
* A **poll, not an observation**, and unavoidably so: no member of the SDK's
|
|
1034
|
+
* `SDKMessage` union carries it (the whole union was checked). It lives on
|
|
1035
|
+
* `SDKSessionInfo`, which only `getSessionInfo` / `listSessions` return — the
|
|
1036
|
+
* same record `GET /sdk-sessions` already serves as `SdkSessionSummary`. So it
|
|
1037
|
+
* is read at init and after each turn, which is also roughly the rate at which
|
|
1038
|
+
* it changes.
|
|
1039
|
+
*
|
|
1040
|
+
* Two rules:
|
|
1041
|
+
* - **Never while `meta.title` is set.** A rename is a person's decision and a
|
|
1042
|
+
* generated summary must not overwrite it. Not read at all in that case, so
|
|
1043
|
+
* there is no stored value waiting to resurface if the rename is cleared —
|
|
1044
|
+
* the next turn simply fetches it again.
|
|
1045
|
+
* - `summary` falls back to the first prompt when the session has no real
|
|
1046
|
+
* title yet, so it is taken only when it *differs* from `firstPrompt`.
|
|
1047
|
+
* Otherwise `#title()`'s own prompt fallback covers it, and the two would
|
|
1048
|
+
* disagree only in how they truncate.
|
|
1049
|
+
*
|
|
1050
|
+
* Best-effort throughout: an unreadable transcript, a session file that is not
|
|
1051
|
+
* there yet, an SDK without the function — all leave the title as it was.
|
|
1052
|
+
*/
|
|
1053
|
+
async #fetchEngineTitle() {
|
|
1054
|
+
const metaTitle = this.#config.meta?.title;
|
|
1055
|
+
if (typeof metaTitle === "string" && metaTitle.length > 0) return;
|
|
1056
|
+
const sdkSessionId = this.#sdkSessionId;
|
|
1057
|
+
if (!sdkSessionId) return;
|
|
1058
|
+
const read = this.#config.sessionInfoFn ?? getSessionInfo;
|
|
1059
|
+
try {
|
|
1060
|
+
const info = await read(sdkSessionId, { dir: this.#cwd });
|
|
1061
|
+
if (this.#closed || !info) return;
|
|
1062
|
+
const summary = info.summary && info.summary !== info.firstPrompt ? info.summary : void 0;
|
|
1063
|
+
const title = info.customTitle || summary;
|
|
1064
|
+
if (title) this.#engineTitle = title;
|
|
1065
|
+
} catch {}
|
|
1066
|
+
}
|
|
787
1067
|
/** Snapshot the context window after a turn and surface it as an event. Optional-chained
|
|
788
1068
|
* and best-effort for the same reasons as #fetchCapabilities. */
|
|
789
1069
|
async #fetchContextUsage() {
|
|
@@ -965,6 +1245,7 @@ var SessionRunner = class {
|
|
|
965
1245
|
};
|
|
966
1246
|
this.#lastActivityAt = event.ts;
|
|
967
1247
|
this.#activityCount += transcriptActivity(body);
|
|
1248
|
+
if (body.type === "conversation_reset") this.#resetSeq = event.seq;
|
|
968
1249
|
this.#events.push(event);
|
|
969
1250
|
for (const listener of this.#listeners) try {
|
|
970
1251
|
listener(event);
|
|
@@ -986,7 +1267,7 @@ function recommendedAnswers(input) {
|
|
|
986
1267
|
return answers;
|
|
987
1268
|
}
|
|
988
1269
|
//#endregion
|
|
989
|
-
//#region src/
|
|
1270
|
+
//#region src/engines/provider/runner.ts
|
|
990
1271
|
/** Permission modes this engine can honor. The rest of the protocol vocabulary
|
|
991
1272
|
* (acceptEdits/plan/auto) is Claude Code CLI semantics with no meaning here —
|
|
992
1273
|
* setPermissionMode rejects them, which the server surfaces as protocol_error. */
|
|
@@ -1357,8 +1638,13 @@ var AiSdkRunner = class {
|
|
|
1357
1638
|
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1358
1639
|
} catch {}
|
|
1359
1640
|
}
|
|
1360
|
-
subscribe(listener, afterSeq = 0) {
|
|
1361
|
-
|
|
1641
|
+
subscribe(listener, afterSeq = 0, options) {
|
|
1642
|
+
const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
|
|
1643
|
+
for (const event of this.#events) {
|
|
1644
|
+
if (event.seq <= afterSeq) continue;
|
|
1645
|
+
if (stale?.has(event.seq)) continue;
|
|
1646
|
+
listener(event);
|
|
1647
|
+
}
|
|
1362
1648
|
this.#listeners.add(listener);
|
|
1363
1649
|
return () => this.#listeners.delete(listener);
|
|
1364
1650
|
}
|
|
@@ -1489,29 +1775,29 @@ var AiSdkRunner = class {
|
|
|
1489
1775
|
cacheWrite: 0,
|
|
1490
1776
|
cacheRead: 0
|
|
1491
1777
|
};
|
|
1778
|
+
let blocks = [];
|
|
1779
|
+
const textBuf = /* @__PURE__ */ new Map();
|
|
1780
|
+
const reasoningBuf = /* @__PURE__ */ new Map();
|
|
1781
|
+
const flush = () => {
|
|
1782
|
+
if (blocks.length === 0) return;
|
|
1783
|
+
this.#emit({
|
|
1784
|
+
type: "assistant_message",
|
|
1785
|
+
message: {
|
|
1786
|
+
role: "assistant",
|
|
1787
|
+
content: blocks,
|
|
1788
|
+
model: this.#modelId()
|
|
1789
|
+
},
|
|
1790
|
+
parentToolUseId: null,
|
|
1791
|
+
uuid: randomUUID()
|
|
1792
|
+
});
|
|
1793
|
+
blocks = [];
|
|
1794
|
+
};
|
|
1492
1795
|
try {
|
|
1493
1796
|
const result = await agent.stream({
|
|
1494
1797
|
messages: [...this.#messages],
|
|
1495
1798
|
abortSignal: abort.signal
|
|
1496
1799
|
});
|
|
1497
1800
|
const partials = this.#config.includePartialMessages !== false;
|
|
1498
|
-
let blocks = [];
|
|
1499
|
-
const textBuf = /* @__PURE__ */ new Map();
|
|
1500
|
-
const reasoningBuf = /* @__PURE__ */ new Map();
|
|
1501
|
-
const flush = () => {
|
|
1502
|
-
if (blocks.length === 0) return;
|
|
1503
|
-
this.#emit({
|
|
1504
|
-
type: "assistant_message",
|
|
1505
|
-
message: {
|
|
1506
|
-
role: "assistant",
|
|
1507
|
-
content: blocks,
|
|
1508
|
-
model: this.#modelId()
|
|
1509
|
-
},
|
|
1510
|
-
parentToolUseId: null,
|
|
1511
|
-
uuid: randomUUID()
|
|
1512
|
-
});
|
|
1513
|
-
blocks = [];
|
|
1514
|
-
};
|
|
1515
1801
|
const emitToolResult = (toolCallId, content, isError) => {
|
|
1516
1802
|
flush();
|
|
1517
1803
|
this.#emit({
|
|
@@ -1641,6 +1927,15 @@ var AiSdkRunner = class {
|
|
|
1641
1927
|
this.#finishTurn(text);
|
|
1642
1928
|
} catch (error) {
|
|
1643
1929
|
if (this.#closed) return;
|
|
1930
|
+
for (const [, thinking] of reasoningBuf) if (thinking) blocks.push({
|
|
1931
|
+
type: "thinking",
|
|
1932
|
+
thinking
|
|
1933
|
+
});
|
|
1934
|
+
for (const [, text] of textBuf) if (text) blocks.push({
|
|
1935
|
+
type: "text",
|
|
1936
|
+
text
|
|
1937
|
+
});
|
|
1938
|
+
flush();
|
|
1644
1939
|
const message = error instanceof Error ? error.message : String(error);
|
|
1645
1940
|
this.#numTurns += 1;
|
|
1646
1941
|
this.#emit({
|
|
@@ -1761,7 +2056,7 @@ function errorText(error) {
|
|
|
1761
2056
|
return error instanceof Error ? error.message : String(error);
|
|
1762
2057
|
}
|
|
1763
2058
|
//#endregion
|
|
1764
|
-
//#region src/claude
|
|
2059
|
+
//#region src/engines/claude/auth.ts
|
|
1765
2060
|
/**
|
|
1766
2061
|
* The native Claude Code binary the Agent SDK itself spawns, resolved the way
|
|
1767
2062
|
* the SDK resolves it: the platform-specific optional dependency installed next
|
|
@@ -1815,7 +2110,7 @@ function checkClaudeAuth(env, options = {}) {
|
|
|
1815
2110
|
});
|
|
1816
2111
|
}
|
|
1817
2112
|
//#endregion
|
|
1818
|
-
//#region src/quickjs-executor.ts
|
|
2113
|
+
//#region src/executors/quickjs-executor.ts
|
|
1819
2114
|
/**
|
|
1820
2115
|
* In-process execution backend: runs a tool's untrusted script in the QuickJS
|
|
1821
2116
|
* WASM guest. Always settles inline — nothing downstream assumes that, which is
|
|
@@ -1913,7 +2208,7 @@ function isHostAllowed(url, allowedHosts) {
|
|
|
1913
2208
|
});
|
|
1914
2209
|
}
|
|
1915
2210
|
//#endregion
|
|
1916
|
-
//#region src/pending-registry.ts
|
|
2211
|
+
//#region src/lib/pending-registry.ts
|
|
1917
2212
|
var PendingRequestRegistry = class {
|
|
1918
2213
|
#slots = /* @__PURE__ */ new Map();
|
|
1919
2214
|
get size() {
|
|
@@ -2020,7 +2315,7 @@ function toEntry(slot) {
|
|
|
2020
2315
|
};
|
|
2021
2316
|
}
|
|
2022
2317
|
//#endregion
|
|
2023
|
-
//#region src/browser-bridge-executor.ts
|
|
2318
|
+
//#region src/executors/browser-bridge-executor.ts
|
|
2024
2319
|
/**
|
|
2025
2320
|
* Executes tool calls in the attached client's own sandbox. The first backend
|
|
2026
2321
|
* that genuinely returns `pending`: dispatch puts a request on the wire and
|
|
@@ -2131,7 +2426,7 @@ function toExecutionResult(outcome) {
|
|
|
2131
2426
|
};
|
|
2132
2427
|
}
|
|
2133
2428
|
//#endregion
|
|
2134
|
-
//#region src/deferred-executor.ts
|
|
2429
|
+
//#region src/executors/deferred-executor.ts
|
|
2135
2430
|
/**
|
|
2136
2431
|
* The executor for work that outlives the session's process residency: dispatch
|
|
2137
2432
|
* hands the call off and returns `pending` **without holding a promise**, because
|
|
@@ -2178,7 +2473,7 @@ var DeferredExecutor = class {
|
|
|
2178
2473
|
}
|
|
2179
2474
|
};
|
|
2180
2475
|
//#endregion
|
|
2181
|
-
//#region src/tools.ts
|
|
2476
|
+
//#region src/engines/provider/tools.ts
|
|
2182
2477
|
const MAX_FILE_BYTES = 1024 * 1024;
|
|
2183
2478
|
/**
|
|
2184
2479
|
* Build the capability-scoped tool set for a session.
|
|
@@ -2394,7 +2689,7 @@ function truncate(text) {
|
|
|
2394
2689
|
return text.length > MAX_FILE_BYTES ? text.slice(0, MAX_FILE_BYTES) : text;
|
|
2395
2690
|
}
|
|
2396
2691
|
//#endregion
|
|
2397
|
-
//#region src/web-fetch.ts
|
|
2692
|
+
//#region src/engines/provider/web-fetch.ts
|
|
2398
2693
|
const MAX_CACHE_ENTRIES = 64;
|
|
2399
2694
|
const MAX_REDIRECTS = 5;
|
|
2400
2695
|
function createWebFetch(options = {}) {
|
|
@@ -2606,7 +2901,7 @@ function decodeEntities(text) {
|
|
|
2606
2901
|
return text.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).replace(/&#x([\da-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16))).replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'|'/g, "'").replace(/&/g, "&");
|
|
2607
2902
|
}
|
|
2608
2903
|
//#endregion
|
|
2609
|
-
//#region src/
|
|
2904
|
+
//#region src/engines/provider/session.ts
|
|
2610
2905
|
/** Which capability a wired backend yields, for grant filtering. */
|
|
2611
2906
|
const CAPABILITY_TOOLS = {
|
|
2612
2907
|
search: "web_search",
|
|
@@ -3315,15 +3610,29 @@ function userQuestionsFromCodex(questions) {
|
|
|
3315
3610
|
}))
|
|
3316
3611
|
}));
|
|
3317
3612
|
}
|
|
3318
|
-
/**
|
|
3319
|
-
*
|
|
3320
|
-
*
|
|
3613
|
+
/**
|
|
3614
|
+
* The text of a history `userMessage` item: its content entries' text parts
|
|
3615
|
+
* joined.
|
|
3616
|
+
*
|
|
3617
|
+
* Image parts have no replayable representation — the bytes went to the model,
|
|
3618
|
+
* not into the rollout we can render from — so they are named rather than
|
|
3619
|
+
* dropped. A prompt that was *only* an image used to produce an empty string,
|
|
3620
|
+
* which the caller read as "nothing to replay" and skipped: the turn lost its
|
|
3621
|
+
* user row and, with it, the prompt mark the scrubber navigates by, so a resumed
|
|
3622
|
+
* thread had answers with no visible question. A word in place of the picture is
|
|
3623
|
+
* a smaller lie than a turn that never happened.
|
|
3624
|
+
*/
|
|
3321
3625
|
function historyUserText(item) {
|
|
3322
3626
|
if (!Array.isArray(item.content)) return "";
|
|
3323
|
-
|
|
3627
|
+
let images = 0;
|
|
3628
|
+
const text = item.content.map((part) => {
|
|
3324
3629
|
const candidate = part;
|
|
3325
|
-
|
|
3630
|
+
if (candidate?.type === "text" && typeof candidate.text === "string") return candidate.text;
|
|
3631
|
+
if (typeof candidate?.type === "string" && candidate.type.toLowerCase().includes("image")) images += 1;
|
|
3632
|
+
return "";
|
|
3326
3633
|
}).filter(Boolean).join("\n");
|
|
3634
|
+
if (text) return text;
|
|
3635
|
+
return images > 0 ? `[${images === 1 ? "image" : `${images} images`}]` : "";
|
|
3327
3636
|
}
|
|
3328
3637
|
/** The AskUserQuestion answer convention (question text → chosen label(s),
|
|
3329
3638
|
* comma-joined) mapped back to codex's id-keyed shape. Questions the client
|
|
@@ -3827,8 +4136,13 @@ var CodexRunner = class {
|
|
|
3827
4136
|
});
|
|
3828
4137
|
this.#setStatus("closed");
|
|
3829
4138
|
}
|
|
3830
|
-
subscribe(listener, afterSeq = 0) {
|
|
3831
|
-
|
|
4139
|
+
subscribe(listener, afterSeq = 0, options) {
|
|
4140
|
+
const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
|
|
4141
|
+
for (const event of this.#events) {
|
|
4142
|
+
if (event.seq <= afterSeq) continue;
|
|
4143
|
+
if (stale?.has(event.seq)) continue;
|
|
4144
|
+
listener(event);
|
|
4145
|
+
}
|
|
3832
4146
|
this.#listeners.add(listener);
|
|
3833
4147
|
return () => this.#listeners.delete(listener);
|
|
3834
4148
|
}
|
|
@@ -4155,131 +4469,137 @@ var CodexRunner = class {
|
|
|
4155
4469
|
}
|
|
4156
4470
|
#handleNotification(method, params) {
|
|
4157
4471
|
if (this.#closed) return;
|
|
4472
|
+
this.#notifications[method]?.(params);
|
|
4473
|
+
}
|
|
4474
|
+
/** Reasoning deltas arrive on two methods that differ only in which section
|
|
4475
|
+
* counter they advance; the section key carries the method so the two streams
|
|
4476
|
+
* never share a boundary. Section boundaries (a new summary/content entry)
|
|
4477
|
+
* render as paragraph breaks — the completed item joins sections with '\n\n'. */
|
|
4478
|
+
#reasoningDelta(method) {
|
|
4479
|
+
return (params) => {
|
|
4480
|
+
const active = this.#activeTurn;
|
|
4481
|
+
if (!active) return;
|
|
4482
|
+
const payload = params;
|
|
4483
|
+
if (typeof payload?.delta !== "string" || !payload.delta) return;
|
|
4484
|
+
const index = payload.contentIndex ?? payload.summaryIndex ?? 0;
|
|
4485
|
+
const key = `${payload.itemId ?? ""}:${method}`;
|
|
4486
|
+
const previous = active.sectionIndex.get(key);
|
|
4487
|
+
active.sectionIndex.set(key, index);
|
|
4488
|
+
const separator = previous !== void 0 && index > previous ? "\n\n" : "";
|
|
4489
|
+
this.#emitDelta({
|
|
4490
|
+
type: "thinking_delta",
|
|
4491
|
+
thinking: separator + payload.delta
|
|
4492
|
+
});
|
|
4493
|
+
};
|
|
4494
|
+
}
|
|
4495
|
+
/** One item-progress handler serves `item/started` and `item/updated`. */
|
|
4496
|
+
#itemProgress = (params) => {
|
|
4158
4497
|
const active = this.#activeTurn;
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
this.#emitRateLimits(params?.rateLimits);
|
|
4245
|
-
return;
|
|
4246
|
-
case "turn/plan/updated": {
|
|
4247
|
-
if (!active) return;
|
|
4248
|
-
const plan = params?.plan;
|
|
4249
|
-
if (!Array.isArray(plan)) return;
|
|
4250
|
-
this.#emit({
|
|
4251
|
-
type: "sdk_event",
|
|
4252
|
-
payload: {
|
|
4253
|
-
type: "codex.todo_list",
|
|
4254
|
-
id: `${active.nonce}:plan`,
|
|
4255
|
-
items: plan.map((step) => ({
|
|
4256
|
-
text: step.step,
|
|
4257
|
-
completed: step.status === "completed"
|
|
4258
|
-
}))
|
|
4259
|
-
}
|
|
4260
|
-
});
|
|
4261
|
-
return;
|
|
4262
|
-
}
|
|
4263
|
-
case "serverRequest/resolved": {
|
|
4264
|
-
const requestId = params?.requestId;
|
|
4265
|
-
if (requestId === void 0) return;
|
|
4266
|
-
for (const [id, pending] of this.#approvals) if (pending.wireId === requestId) {
|
|
4267
|
-
this.#settleApproval(id, pending, {
|
|
4268
|
-
behavior: "deny",
|
|
4269
|
-
message: "resolved by codex"
|
|
4270
|
-
}, "policy");
|
|
4271
|
-
return;
|
|
4498
|
+
if (!active) return;
|
|
4499
|
+
const item = params?.item;
|
|
4500
|
+
if (item) this.#handleItemProgress(item, active);
|
|
4501
|
+
};
|
|
4502
|
+
/** The notification dispatch table — every method the child emits that this
|
|
4503
|
+
* runner maps, in one place. Handlers read `this.#activeTurn` themselves:
|
|
4504
|
+
* dispatch is synchronous, so the read is the same one the old switch made. */
|
|
4505
|
+
#notifications = {
|
|
4506
|
+
"thread/started": (params) => {
|
|
4507
|
+
const thread = params?.thread;
|
|
4508
|
+
if (typeof thread?.id === "string") this.#sdkSessionId = thread.id;
|
|
4509
|
+
},
|
|
4510
|
+
"turn/started": (params) => {
|
|
4511
|
+
const active = this.#activeTurn;
|
|
4512
|
+
const turn = params?.turn;
|
|
4513
|
+
if (active && turn && !active.turnId) active.turnId = turn.id;
|
|
4514
|
+
},
|
|
4515
|
+
"turn/completed": (params) => {
|
|
4516
|
+
const active = this.#activeTurn;
|
|
4517
|
+
const turn = params?.turn;
|
|
4518
|
+
if (active && turn) active.resolve(turn);
|
|
4519
|
+
},
|
|
4520
|
+
"item/started": this.#itemProgress,
|
|
4521
|
+
"item/updated": this.#itemProgress,
|
|
4522
|
+
"item/completed": (params) => {
|
|
4523
|
+
const active = this.#activeTurn;
|
|
4524
|
+
if (!active) return;
|
|
4525
|
+
const item = params?.item;
|
|
4526
|
+
if (item) this.#handleItemCompleted(item, active);
|
|
4527
|
+
},
|
|
4528
|
+
"item/agentMessage/delta": (params) => {
|
|
4529
|
+
if (!this.#activeTurn) return;
|
|
4530
|
+
const delta = params?.delta;
|
|
4531
|
+
if (typeof delta === "string" && delta) this.#emitDelta({
|
|
4532
|
+
type: "text_delta",
|
|
4533
|
+
text: delta
|
|
4534
|
+
});
|
|
4535
|
+
},
|
|
4536
|
+
"item/reasoning/textDelta": this.#reasoningDelta("item/reasoning/textDelta"),
|
|
4537
|
+
"item/reasoning/summaryTextDelta": this.#reasoningDelta("item/reasoning/summaryTextDelta"),
|
|
4538
|
+
"thread/tokenUsage/updated": (params) => {
|
|
4539
|
+
const active = this.#activeTurn;
|
|
4540
|
+
if (!active) return;
|
|
4541
|
+
const last = params?.tokenUsage?.last;
|
|
4542
|
+
if (!last) return;
|
|
4543
|
+
active.sawUsage = true;
|
|
4544
|
+
active.usage.inputTokens += last.inputTokens ?? 0;
|
|
4545
|
+
active.usage.cachedInputTokens += last.cachedInputTokens ?? 0;
|
|
4546
|
+
active.usage.cacheWriteInputTokens = (active.usage.cacheWriteInputTokens ?? 0) + (last.cacheWriteInputTokens ?? 0);
|
|
4547
|
+
active.usage.outputTokens += last.outputTokens ?? 0;
|
|
4548
|
+
active.usage.reasoningOutputTokens += last.reasoningOutputTokens ?? 0;
|
|
4549
|
+
const update = params;
|
|
4550
|
+
active.contextTokens = last.totalTokens ?? void 0;
|
|
4551
|
+
active.contextWindow = update.tokenUsage?.modelContextWindow ?? void 0;
|
|
4552
|
+
},
|
|
4553
|
+
"mcpServer/startupStatus/updated": (params) => {
|
|
4554
|
+
const update = params;
|
|
4555
|
+
if (typeof update?.name !== "string") return;
|
|
4556
|
+
this.#mcpStatus.set(update.name, {
|
|
4557
|
+
status: typeof update.status === "string" ? update.status : "starting",
|
|
4558
|
+
...update.error ? { error: update.error } : {},
|
|
4559
|
+
...update.failureReason ? { failureReason: update.failureReason } : {}
|
|
4560
|
+
});
|
|
4561
|
+
},
|
|
4562
|
+
"skills/changed": () => {
|
|
4563
|
+
const connection = this.#connection;
|
|
4564
|
+
if (connection) this.#refreshSkills(connection);
|
|
4565
|
+
},
|
|
4566
|
+
"account/rateLimits/updated": (params) => {
|
|
4567
|
+
this.#emitRateLimits(params?.rateLimits);
|
|
4568
|
+
},
|
|
4569
|
+
"turn/plan/updated": (params) => {
|
|
4570
|
+
const active = this.#activeTurn;
|
|
4571
|
+
if (!active) return;
|
|
4572
|
+
const plan = params?.plan;
|
|
4573
|
+
if (!Array.isArray(plan)) return;
|
|
4574
|
+
this.#emit({
|
|
4575
|
+
type: "sdk_event",
|
|
4576
|
+
payload: {
|
|
4577
|
+
type: "codex.todo_list",
|
|
4578
|
+
id: `${active.nonce}:plan`,
|
|
4579
|
+
items: plan.map((step) => ({
|
|
4580
|
+
text: step.step,
|
|
4581
|
+
completed: step.status === "completed"
|
|
4582
|
+
}))
|
|
4272
4583
|
}
|
|
4584
|
+
});
|
|
4585
|
+
},
|
|
4586
|
+
"serverRequest/resolved": (params) => {
|
|
4587
|
+
const requestId = params?.requestId;
|
|
4588
|
+
if (requestId === void 0) return;
|
|
4589
|
+
for (const [id, pending] of this.#approvals) if (pending.wireId === requestId) {
|
|
4590
|
+
this.#settleApproval(id, pending, {
|
|
4591
|
+
behavior: "deny",
|
|
4592
|
+
message: "resolved by codex"
|
|
4593
|
+
}, "policy");
|
|
4273
4594
|
return;
|
|
4274
4595
|
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
default: return;
|
|
4596
|
+
},
|
|
4597
|
+
"error": (params) => {
|
|
4598
|
+
const active = this.#activeTurn;
|
|
4599
|
+
const error = params?.error;
|
|
4600
|
+
if (active && typeof error?.message === "string") active.lastError = error.message;
|
|
4281
4601
|
}
|
|
4282
|
-
}
|
|
4602
|
+
};
|
|
4283
4603
|
/** Answer a server→client request: the ask channels become pending
|
|
4284
4604
|
* permission requests; anything else gets a JSON-RPC -32601 rather than a
|
|
4285
4605
|
* hang (an unanswered server request wedges the turn). */
|
|
@@ -4425,83 +4745,89 @@ var CodexRunner = class {
|
|
|
4425
4745
|
}
|
|
4426
4746
|
#handleItemCompleted(item, active) {
|
|
4427
4747
|
const id = `${active.nonce}:${item.id}`;
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
case "reasoning": {
|
|
4440
|
-
const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
|
|
4441
|
-
const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
|
|
4442
|
-
const thinking = (summary.length > 0 ? summary : content).join("\n\n");
|
|
4443
|
-
if (thinking) this.#emitAssistant(id, [{
|
|
4444
|
-
type: "thinking",
|
|
4445
|
-
thinking
|
|
4446
|
-
}]);
|
|
4447
|
-
return;
|
|
4448
|
-
}
|
|
4449
|
-
case "commandExecution": {
|
|
4450
|
-
if (!active.toolUseEmitted.has(id)) {
|
|
4451
|
-
active.toolUseEmitted.add(id);
|
|
4452
|
-
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
4453
|
-
}
|
|
4454
|
-
const exitCode = item.exitCode ?? void 0;
|
|
4455
|
-
const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
|
|
4456
|
-
const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
|
|
4457
|
-
this.#emitToolResult(id, output, failed);
|
|
4458
|
-
return;
|
|
4459
|
-
}
|
|
4460
|
-
case "fileChange": {
|
|
4461
|
-
this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
|
|
4462
|
-
const lines = item.changes.map((change) => {
|
|
4463
|
-
return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
|
|
4464
|
-
});
|
|
4465
|
-
this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined");
|
|
4466
|
-
return;
|
|
4467
|
-
}
|
|
4468
|
-
case "mcpToolCall": {
|
|
4469
|
-
if (!active.toolUseEmitted.has(id)) {
|
|
4470
|
-
active.toolUseEmitted.add(id);
|
|
4471
|
-
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
4472
|
-
}
|
|
4473
|
-
const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
|
|
4474
|
-
this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
|
|
4475
|
-
return;
|
|
4748
|
+
const handler = this.#itemCompleted[item.type];
|
|
4749
|
+
if (handler) {
|
|
4750
|
+
handler(item, active, id);
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
const unknown = item;
|
|
4754
|
+
this.#emit({
|
|
4755
|
+
type: "sdk_event",
|
|
4756
|
+
payload: {
|
|
4757
|
+
type: `codex.${unknown.type}`,
|
|
4758
|
+
item: unknown
|
|
4476
4759
|
}
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4760
|
+
});
|
|
4761
|
+
}
|
|
4762
|
+
/**
|
|
4763
|
+
* The completed-item mapping, one handler per member of the {@link AppServerItem}
|
|
4764
|
+
* union. The mapped type is the invariant made checkable: model a new item
|
|
4765
|
+
* type in `types.ts` and this table fails to compile until it says what the
|
|
4766
|
+
* item becomes on the wire — the old switch silently fell through to the
|
|
4767
|
+
* unknown-item passthrough instead. (The runtime still receives types the
|
|
4768
|
+
* union has never heard of; those take the passthrough above.)
|
|
4769
|
+
*/
|
|
4770
|
+
#itemCompleted = {
|
|
4771
|
+
userMessage: () => {},
|
|
4772
|
+
agentMessage: (item, active, id) => {
|
|
4773
|
+
const text = typeof item.text === "string" ? item.text : "";
|
|
4774
|
+
this.#emitAssistant(id, [{
|
|
4775
|
+
type: "text",
|
|
4776
|
+
text
|
|
4777
|
+
}]);
|
|
4778
|
+
active.finalText = text;
|
|
4779
|
+
},
|
|
4780
|
+
reasoning: (item, _active, id) => {
|
|
4781
|
+
const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
|
|
4782
|
+
const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
|
|
4783
|
+
const thinking = (summary.length > 0 ? summary : content).join("\n\n");
|
|
4784
|
+
if (thinking) this.#emitAssistant(id, [{
|
|
4785
|
+
type: "thinking",
|
|
4786
|
+
thinking
|
|
4787
|
+
}]);
|
|
4788
|
+
},
|
|
4789
|
+
commandExecution: (item, active, id) => {
|
|
4790
|
+
if (!active.toolUseEmitted.has(id)) {
|
|
4482
4791
|
active.toolUseEmitted.add(id);
|
|
4483
|
-
this.#emitToolUse(id,
|
|
4484
|
-
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
4485
|
-
const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
|
|
4486
|
-
this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
|
|
4487
|
-
return;
|
|
4792
|
+
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
4488
4793
|
}
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4794
|
+
const exitCode = item.exitCode ?? void 0;
|
|
4795
|
+
const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
|
|
4796
|
+
const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
|
|
4797
|
+
this.#emitToolResult(id, output, failed);
|
|
4798
|
+
},
|
|
4799
|
+
fileChange: (item, _active, id) => {
|
|
4800
|
+
this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
|
|
4801
|
+
const lines = item.changes.map((change) => {
|
|
4802
|
+
return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
|
|
4803
|
+
});
|
|
4804
|
+
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);
|
|
4806
|
+
},
|
|
4807
|
+
mcpToolCall: (item, active, id) => {
|
|
4808
|
+
if (!active.toolUseEmitted.has(id)) {
|
|
4809
|
+
active.toolUseEmitted.add(id);
|
|
4810
|
+
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
4502
4811
|
}
|
|
4812
|
+
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);
|
|
4814
|
+
},
|
|
4815
|
+
webSearch: (item, _active, id) => {
|
|
4816
|
+
this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
|
|
4817
|
+
this.#emitToolResult(id, "", false);
|
|
4818
|
+
},
|
|
4819
|
+
imageGeneration: (item, active, id) => {
|
|
4820
|
+
active.toolUseEmitted.add(id);
|
|
4821
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
4822
|
+
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
4823
|
+
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");
|
|
4825
|
+
},
|
|
4826
|
+
imageView: (item, _active, id) => {
|
|
4827
|
+
this.#emitToolUse(id, "CodexImageView", { path: item.path });
|
|
4828
|
+
this.#emitToolResult(id, item.path, false);
|
|
4503
4829
|
}
|
|
4504
|
-
}
|
|
4830
|
+
};
|
|
4505
4831
|
#emitDelta(delta) {
|
|
4506
4832
|
if (this.#config.includePartialMessages === false) return;
|
|
4507
4833
|
this.#emit({
|
|
@@ -4543,7 +4869,7 @@ var CodexRunner = class {
|
|
|
4543
4869
|
uuid: `${id}-use`
|
|
4544
4870
|
});
|
|
4545
4871
|
}
|
|
4546
|
-
#emitToolResult(toolUseId, content, isError) {
|
|
4872
|
+
#emitToolResult(toolUseId, content, isError, patch) {
|
|
4547
4873
|
this.#emit({
|
|
4548
4874
|
type: "user_message",
|
|
4549
4875
|
message: {
|
|
@@ -4557,6 +4883,7 @@ var CodexRunner = class {
|
|
|
4557
4883
|
},
|
|
4558
4884
|
parentToolUseId: null,
|
|
4559
4885
|
synthetic: true,
|
|
4886
|
+
patch,
|
|
4560
4887
|
uuid: `${toolUseId}-result`
|
|
4561
4888
|
});
|
|
4562
4889
|
}
|