@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.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/normalize.ts
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": return {
355
- type: "user_message",
356
- message: toApiMessage(msg.message),
357
- parentToolUseId: msg.parent_tool_use_id,
358
- replay: "isReplay" in msg && msg.isReplay === true ? true : void 0,
359
- synthetic: msg.isSynthetic === true ? true : void 0,
360
- uuid: msg.uuid
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/runner.ts
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
- for (const event of this.#events) if (event.seq > afterSeq) listener(event);
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") this.#emit({
680
- type: "user_message",
681
- message: toApiMessage(m.message),
682
- parentToolUseId: m.parent_tool_use_id,
683
- replay: true,
684
- uuid: m.uuid
685
- });
686
- else if (m.type === "assistant") this.#emit({
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/ai-sdk-runner.ts
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
- for (const event of this.#events) if (event.seq > afterSeq) listener(event);
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-auth.ts
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(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;|&apos;/g, "'").replace(/&amp;/g, "&");
2607
2902
  }
2608
2903
  //#endregion
2609
- //#region src/engine.ts
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
- /** The text of a history `userMessage` item: its content entries' text parts
3319
- * joined. Image parts have no replayable representation (the bytes went to the
3320
- * model, not into the rollout we can render from) and are skipped. */
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
- return item.content.map((part) => {
3627
+ let images = 0;
3628
+ const text = item.content.map((part) => {
3324
3629
  const candidate = part;
3325
- return candidate?.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
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
- for (const event of this.#events) if (event.seq > afterSeq) listener(event);
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
- switch (method) {
4160
- case "thread/started": {
4161
- const thread = params?.thread;
4162
- if (typeof thread?.id === "string") this.#sdkSessionId = thread.id;
4163
- return;
4164
- }
4165
- case "turn/started": {
4166
- const turn = params?.turn;
4167
- if (active && turn && !active.turnId) active.turnId = turn.id;
4168
- return;
4169
- }
4170
- case "turn/completed": {
4171
- const turn = params?.turn;
4172
- if (active && turn) active.resolve(turn);
4173
- return;
4174
- }
4175
- case "item/started":
4176
- case "item/updated": {
4177
- if (!active) return;
4178
- const item = params?.item;
4179
- if (item) this.#handleItemProgress(item, active);
4180
- return;
4181
- }
4182
- case "item/completed": {
4183
- if (!active) return;
4184
- const item = params?.item;
4185
- if (item) this.#handleItemCompleted(item, active);
4186
- return;
4187
- }
4188
- case "item/agentMessage/delta": {
4189
- if (!active) return;
4190
- const delta = params?.delta;
4191
- if (typeof delta === "string" && delta) this.#emitDelta({
4192
- type: "text_delta",
4193
- text: delta
4194
- });
4195
- return;
4196
- }
4197
- case "item/reasoning/textDelta":
4198
- case "item/reasoning/summaryTextDelta": {
4199
- if (!active) return;
4200
- const payload = params;
4201
- if (typeof payload?.delta !== "string" || !payload.delta) return;
4202
- const index = payload.contentIndex ?? payload.summaryIndex ?? 0;
4203
- const key = `${payload.itemId ?? ""}:${method}`;
4204
- const previous = active.sectionIndex.get(key);
4205
- active.sectionIndex.set(key, index);
4206
- const separator = previous !== void 0 && index > previous ? "\n\n" : "";
4207
- this.#emitDelta({
4208
- type: "thinking_delta",
4209
- thinking: separator + payload.delta
4210
- });
4211
- return;
4212
- }
4213
- case "thread/tokenUsage/updated": {
4214
- if (!active) return;
4215
- const last = params?.tokenUsage?.last;
4216
- if (!last) return;
4217
- active.sawUsage = true;
4218
- active.usage.inputTokens += last.inputTokens ?? 0;
4219
- active.usage.cachedInputTokens += last.cachedInputTokens ?? 0;
4220
- active.usage.cacheWriteInputTokens = (active.usage.cacheWriteInputTokens ?? 0) + (last.cacheWriteInputTokens ?? 0);
4221
- active.usage.outputTokens += last.outputTokens ?? 0;
4222
- active.usage.reasoningOutputTokens += last.reasoningOutputTokens ?? 0;
4223
- const update = params;
4224
- active.contextTokens = last.totalTokens ?? void 0;
4225
- active.contextWindow = update.tokenUsage?.modelContextWindow ?? void 0;
4226
- return;
4227
- }
4228
- case "mcpServer/startupStatus/updated": {
4229
- const update = params;
4230
- if (typeof update?.name !== "string") return;
4231
- this.#mcpStatus.set(update.name, {
4232
- status: typeof update.status === "string" ? update.status : "starting",
4233
- ...update.error ? { error: update.error } : {},
4234
- ...update.failureReason ? { failureReason: update.failureReason } : {}
4235
- });
4236
- return;
4237
- }
4238
- case "skills/changed": {
4239
- const connection = this.#connection;
4240
- if (connection) this.#refreshSkills(connection);
4241
- return;
4242
- }
4243
- case "account/rateLimits/updated":
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
- case "error": {
4276
- const error = params?.error;
4277
- if (active && typeof error?.message === "string") active.lastError = error.message;
4278
- return;
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
- switch (item.type) {
4429
- case "userMessage": return;
4430
- case "agentMessage": {
4431
- const text = typeof item.text === "string" ? item.text : "";
4432
- this.#emitAssistant(id, [{
4433
- type: "text",
4434
- text
4435
- }]);
4436
- active.finalText = text;
4437
- return;
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
- case "webSearch":
4478
- this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
4479
- this.#emitToolResult(id, "", false);
4480
- return;
4481
- case "imageGeneration": {
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, CODEX_IMAGE_TOOL, imageGenerationInput(item));
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
- case "imageView":
4490
- this.#emitToolUse(id, "CodexImageView", { path: item.path });
4491
- this.#emitToolResult(id, item.path, false);
4492
- return;
4493
- default: {
4494
- const unknown = item;
4495
- this.#emit({
4496
- type: "sdk_event",
4497
- payload: {
4498
- type: `codex.${unknown.type}`,
4499
- item: unknown
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
  }