@workerdeck/core 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/build/index.d.mts +205 -21
- package/build/index.mjs +1174 -285
- 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, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, imagePartRef, replayCoalesceKey, replayRetains, snapshotRetains, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
|
|
5
5
|
import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
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,531 @@ 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
|
+
/**
|
|
597
|
+
* The one replay body, and what a socket receives from it.
|
|
598
|
+
*
|
|
599
|
+
* Every runner had a byte-identical copy of this loop — three spellings of four
|
|
600
|
+
* rules, one of which ("never drop the highest-seq event, whatever the rule
|
|
601
|
+
* says") is load-bearing and was three copies of a comment. Not a base class:
|
|
602
|
+
* the runners share nothing else, and a base class would have to own `#emit`,
|
|
603
|
+
* the most engine-specific method each of them has.
|
|
604
|
+
*
|
|
605
|
+
* The rules, in the order they are applied:
|
|
606
|
+
*
|
|
607
|
+
* 1. `afterSeq` — the caller already holds everything at or below it.
|
|
608
|
+
* 2. `resetSeq` — transcript *content* strictly below the latest
|
|
609
|
+
* `conversation_reset` is skipped, so a re-attach cannot resurrect a cleared
|
|
610
|
+
* conversation while state events still replay. Claude's alone; the other
|
|
611
|
+
* engines pass 0.
|
|
612
|
+
* 3. `coalesceReplay` — last-write-wins state readings superseded later in the
|
|
613
|
+
* same replay (`staleReplaySeqs`), plus everything `replayRetains` says the
|
|
614
|
+
* reducer reads and discards. Opt-in, and only sound for a consumer whose
|
|
615
|
+
* handling of those events is last-write-wins.
|
|
616
|
+
* 4. `imageRefs` — a base64 image part is delivered as an `image_ref` address
|
|
617
|
+
* (protocol's {@link imagePartRef}), its bytes one REST fetch away. Applied
|
|
618
|
+
* **before** rule 5, because it stamps indices from the stored part array
|
|
619
|
+
* which rule 5 then reshapes. Unlike rule 5 this also applies to the live
|
|
620
|
+
* path (see `SubscriberSet`), which is the one place these two rules differ.
|
|
621
|
+
* 5. `truncateResults` — a huge `tool_result` block is delivered as its head
|
|
622
|
+
* plus the markers that say so. **Never mutates the stored event**: the live
|
|
623
|
+
* path, the parking snapshot and the fetch route all need the whole thing,
|
|
624
|
+
* so this builds a copy and the log stays the log.
|
|
625
|
+
*
|
|
626
|
+
* The highest-seq event is delivered whatever rules 2 and 3 say — a client's
|
|
627
|
+
* replay hold waits for `state.lastSeq` to reach the attach's and would
|
|
628
|
+
* otherwise hang forever — but it is still *truncated* when rule 4 applies. A
|
|
629
|
+
* session that ends on a `find /` puts its 641 KB frame exactly there.
|
|
630
|
+
*/
|
|
631
|
+
function replaySlice(events, options) {
|
|
632
|
+
const { afterSeq, resetSeq = 0, coalesceReplay, truncateResults, imageRefs } = options;
|
|
633
|
+
const stale = coalesceReplay ? staleReplaySeqs(events, afterSeq) : void 0;
|
|
634
|
+
const lastSeq = events[events.length - 1]?.seq ?? 0;
|
|
635
|
+
const out = [];
|
|
636
|
+
for (const event of events) {
|
|
637
|
+
if (event.seq <= afterSeq) continue;
|
|
638
|
+
if (event.seq < resetSeq && transcriptContent(event)) continue;
|
|
639
|
+
if (stale?.has(event.seq)) continue;
|
|
640
|
+
if (coalesceReplay && event.seq !== lastSeq && !replayRetains(event)) continue;
|
|
641
|
+
let delivered = event;
|
|
642
|
+
if (imageRefs) delivered = refImageParts(delivered);
|
|
643
|
+
if (truncateResults) delivered = truncateResultBlocks(delivered);
|
|
644
|
+
out.push(delivered);
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* A copy of `event` whose oversized `tool_result` blocks carry their head and
|
|
650
|
+
* say so — or `event` itself, unchanged and un-copied, when nothing is over the
|
|
651
|
+
* budget. That identity matters: an attach is mostly small events, and a fresh
|
|
652
|
+
* object for every one of them would cost more than the feature saves.
|
|
653
|
+
*
|
|
654
|
+
* Blocks are measured and cut **individually**. A message answering three calls
|
|
655
|
+
* where one is a `find /` keeps the two small results whole, which is what makes
|
|
656
|
+
* the per-block marker (rather than a per-event one) honest.
|
|
657
|
+
*/
|
|
658
|
+
function truncateResultBlocks(event) {
|
|
659
|
+
if (event.type !== "user_message") return event;
|
|
660
|
+
const content = event.message.content;
|
|
661
|
+
if (!Array.isArray(content)) return event;
|
|
662
|
+
let cut = false;
|
|
663
|
+
const blocks = content.map((block) => {
|
|
664
|
+
if (block.type !== "tool_result") return block;
|
|
665
|
+
const result = block;
|
|
666
|
+
if (result.truncated) return block;
|
|
667
|
+
const total = resultChars(result.content);
|
|
668
|
+
if (total <= TOOL_RESULT_HEAD_CHARS) return block;
|
|
669
|
+
cut = true;
|
|
670
|
+
return {
|
|
671
|
+
...result,
|
|
672
|
+
content: headOf(result.content, TOOL_RESULT_HEAD_CHARS),
|
|
673
|
+
truncated: true,
|
|
674
|
+
total_chars: total
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
if (!cut) return event;
|
|
678
|
+
return {
|
|
679
|
+
...event,
|
|
680
|
+
message: {
|
|
681
|
+
...event.message,
|
|
682
|
+
content: blocks
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
/** Characters in a result's content, in the same terms a reader sees it: the
|
|
687
|
+
* string itself, or every text part of a block list joined by newlines — which
|
|
688
|
+
* is exactly what `blockText` in the reducer builds. Non-text parts (an image
|
|
689
|
+
* block) contribute nothing, because they are not what is large here and
|
|
690
|
+
* slicing them would corrupt them. */
|
|
691
|
+
function resultChars(content) {
|
|
692
|
+
if (typeof content === "string") return content.length;
|
|
693
|
+
if (!Array.isArray(content)) return 0;
|
|
694
|
+
return content.reduce((total, part, index) => total + (typeof part.text === "string" ? part.text.length + (index > 0 ? 1 : 0) : 0), 0);
|
|
695
|
+
}
|
|
696
|
+
/** The first `chars` characters, in the content's own shape — a string stays a
|
|
697
|
+
* string, a block list stays a block list (cut at the part that crosses the
|
|
698
|
+
* budget, with the remaining parts dropped). Shape-preserving on purpose: the
|
|
699
|
+
* reducer, both renderers and the copy button all read this the same way they
|
|
700
|
+
* read a whole one, so truncation is a shorter result and never a different
|
|
701
|
+
* kind of one. */
|
|
702
|
+
function headOf(content, chars) {
|
|
703
|
+
if (typeof content === "string") return content.slice(0, chars);
|
|
704
|
+
if (!Array.isArray(content)) return content;
|
|
705
|
+
const parts = [];
|
|
706
|
+
let used = 0;
|
|
707
|
+
for (const part of content) {
|
|
708
|
+
if (part.type === "image_ref") {
|
|
709
|
+
parts.push(part);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (typeof part.text !== "string") continue;
|
|
713
|
+
if (used >= chars) continue;
|
|
714
|
+
const text = part.text.slice(0, chars - used);
|
|
715
|
+
parts.push({
|
|
716
|
+
...part,
|
|
717
|
+
text
|
|
718
|
+
});
|
|
719
|
+
used += text.length + 1;
|
|
720
|
+
}
|
|
721
|
+
return parts;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* A copy of `event` whose `tool_result` blocks carry `image_ref` addresses in
|
|
725
|
+
* place of their base64 image parts — or `event` itself, unchanged and
|
|
726
|
+
* un-copied, when it holds none. Same identity rule as
|
|
727
|
+
* {@link truncateResultBlocks}, and it matters more here: an event carrying an
|
|
728
|
+
* image at all is the exception, so the common path must not allocate.
|
|
729
|
+
*
|
|
730
|
+
* **Never mutates the stored event.** The log is what the parking snapshot
|
|
731
|
+
* embeds, what `Runner.eventAt` reads, and therefore what the fetch route
|
|
732
|
+
* serves the bytes back from — a drop that reached the log would 404 the very
|
|
733
|
+
* lazy-load this rule promises.
|
|
734
|
+
*
|
|
735
|
+
* Indices are stamped from the **stored** array, which is why this runs *before*
|
|
736
|
+
* truncation rather than after: `headOf` reshapes a block's parts, so an address
|
|
737
|
+
* computed on its output would name the wrong part of the stored block. That
|
|
738
|
+
* ordering is asserted in `replay-image-ref.test.ts`, not merely intended.
|
|
739
|
+
*/
|
|
740
|
+
function refImageParts(event) {
|
|
741
|
+
if (event.type !== "user_message") return event;
|
|
742
|
+
const content = event.message.content;
|
|
743
|
+
if (!Array.isArray(content)) return event;
|
|
744
|
+
let changed = false;
|
|
745
|
+
const blocks = content.map((block) => {
|
|
746
|
+
if (block.type !== "tool_result") return block;
|
|
747
|
+
const result = block;
|
|
748
|
+
const parts = result.content;
|
|
749
|
+
if (!Array.isArray(parts)) return block;
|
|
750
|
+
let blockChanged = false;
|
|
751
|
+
const mapped = parts.map((part, index) => {
|
|
752
|
+
const ref = imagePartRef(part, index);
|
|
753
|
+
if (!ref) return part;
|
|
754
|
+
blockChanged = true;
|
|
755
|
+
return ref;
|
|
756
|
+
});
|
|
757
|
+
if (!blockChanged) return block;
|
|
758
|
+
changed = true;
|
|
759
|
+
return {
|
|
760
|
+
...result,
|
|
761
|
+
content: mapped
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
if (!changed) return event;
|
|
765
|
+
return {
|
|
766
|
+
...event,
|
|
767
|
+
message: {
|
|
768
|
+
...event.message,
|
|
769
|
+
content: blocks
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/lib/subscribers.ts
|
|
775
|
+
var SubscriberSet = class {
|
|
776
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
777
|
+
/**
|
|
778
|
+
* Replay `events` to `listener` under `options`, then hold it for live
|
|
779
|
+
* delivery. Returns the unsubscribe.
|
|
780
|
+
*
|
|
781
|
+
* The replay runs *before* the listener joins the set, which is the ordering
|
|
782
|
+
* every runner already had and is load-bearing: joining first would deliver a
|
|
783
|
+
* live event emitted mid-replay ahead of the buffered events preceding it.
|
|
784
|
+
*/
|
|
785
|
+
subscribe(events, listener, afterSeq = 0, options, resetSeq = 0) {
|
|
786
|
+
const asked = options ?? {};
|
|
787
|
+
for (const event of replaySlice(events, {
|
|
788
|
+
...asked,
|
|
789
|
+
afterSeq,
|
|
790
|
+
resetSeq
|
|
791
|
+
})) listener(event);
|
|
792
|
+
this.#listeners.set(listener, asked);
|
|
793
|
+
return () => {
|
|
794
|
+
this.#listeners.delete(listener);
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
/** Drop every subscriber — a park, which ends the session's live stream. */
|
|
798
|
+
clear() {
|
|
799
|
+
this.#listeners.clear();
|
|
800
|
+
}
|
|
801
|
+
/** Fan one event out, transformed per subscriber. */
|
|
802
|
+
emit(event) {
|
|
803
|
+
for (const [listener, asked] of this.#listeners) try {
|
|
804
|
+
listener(asked.imageRefs ? refImageParts(event) : event);
|
|
805
|
+
} catch {}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/engines/claude/subagents.ts
|
|
810
|
+
/**
|
|
811
|
+
* The rollup behind `SessionInfo.subagents` — what a sessions list (which never
|
|
812
|
+
* attaches) can know about the sub-agents running inside a session. Fed from
|
|
813
|
+
* `SessionRunner.#emit`, the one chokepoint every event passes through, so the
|
|
814
|
+
* resume backfill — which replays history through the same path — reconstructs
|
|
815
|
+
* it with no persistence of its own. Grouping is by Task id throughout, never
|
|
816
|
+
* adjacency: parallel sub-agents interleave in the stream, the same fact that
|
|
817
|
+
* broke the terminal theme's positional row model.
|
|
818
|
+
*
|
|
819
|
+
* Three decisions live here rather than in the protocol doc:
|
|
820
|
+
*
|
|
821
|
+
* **What counts as a spawn.** A record opens when a *top-level* assistant
|
|
822
|
+
* message carries a `tool_use` named `Task` or `Agent` — the moment the
|
|
823
|
+
* sub-agent exists, so a just-spawned agent is visible before its first nested
|
|
824
|
+
* event, with the block's input in hand for its labels. Both names are observed
|
|
825
|
+
* SDK spellings (`Task` synchronous, `Agent` async), and the name is a
|
|
826
|
+
* convention, not a law — a session that spawned three `Agent`s under a tracker
|
|
827
|
+
* that only knew `Task` reported all three as label-less failures. So three
|
|
828
|
+
* more openers back the allowlist up: the CLI's own `task_started` system event
|
|
829
|
+
* (which positively names the `tool_use_id` an agent runs under, with the brief
|
|
830
|
+
* as labels), the launch acknowledgement (below), and — as before — any nested
|
|
831
|
+
* event whose `parentToolUseId` has no record: an id that events demonstrably
|
|
832
|
+
* nest under *is* a sub-agent, whatever the spawning call was named. A fallback
|
|
833
|
+
* record never saw an input, so it stays label-less until a named signal fills
|
|
834
|
+
* it in rather than resetting an accumulated count.
|
|
835
|
+
*
|
|
836
|
+
* **A background agent's `tool_result` is a launch receipt, not a verdict.**
|
|
837
|
+
* An async agent's spawn call resolves seconds after the spawn with "Async
|
|
838
|
+
* agent launched successfully. (This tool result is internal metadata …)" —
|
|
839
|
+
* long before the agent has done anything — and its actual outcome travels on
|
|
840
|
+
* a `task_notification` system event instead (`status: 'completed'` is `done`,
|
|
841
|
+
* any other way of stopping is `failed`: the report the notification exists to
|
|
842
|
+
* deliver never came). Settling on the receipt would read "0 of 3 agents
|
|
843
|
+
* running" while three agents burn tokens, so a non-error result on a record
|
|
844
|
+
* known to be background never settles it. Known how: the `task_started` event
|
|
845
|
+
* live, or the receipt's own wrapper text on a resume — the stored transcript
|
|
846
|
+
* carries none of the CLI's system events, so, exactly as
|
|
847
|
+
* `isSyntheticUserText` documents for the `<task-notification>` blob, the text
|
|
848
|
+
* is the only signal the replayed path has.
|
|
849
|
+
*
|
|
850
|
+
* **What an interrupted turn leaves behind.** A Task whose `tool_result` never
|
|
851
|
+
* arrives — interrupt, session error, a turn or budget cap — would otherwise
|
|
852
|
+
* read `running` on an idle session forever, a lie a list re-renders at every
|
|
853
|
+
* poll. So the end of a turn settles every still-running record as `failed`:
|
|
854
|
+
* the report never came, which is the one thing `done` could have claimed. The
|
|
855
|
+
* sweep keys on `turn_result`, on the status coming to rest (`idle` — which is
|
|
856
|
+
* how a resumed history that ends mid-Task settles, since the backfill replays
|
|
857
|
+
* no `turn_result` — or a terminal state), and on the session closing. A real
|
|
858
|
+
* verdict arriving anyway outranks the sweep's inference. The sweep's premise
|
|
859
|
+
* — "the turn ended, so anything still running was cut off" — is false for a
|
|
860
|
+
* background agent, which is *designed* to outlive its turn: the real session
|
|
861
|
+
* behind this file ended three turns while its agents ran, and every
|
|
862
|
+
* `turn_result` re-branded live, working agents as failures. So the turn and
|
|
863
|
+
* idle sweeps spare a record marked background by a **live** signal. They do
|
|
864
|
+
* not spare one whose only evidence is replayed: the backfill describes a
|
|
865
|
+
* process that is gone, and a background agent the old process died inside can
|
|
866
|
+
* never notify — `running` would be the forever-lie again. `session_closed`
|
|
867
|
+
* and the terminal statuses settle everything, background included, for the
|
|
868
|
+
* same reason: the process hosting those agents is gone.
|
|
869
|
+
*/
|
|
870
|
+
var SubagentTracker = class {
|
|
871
|
+
#records = /* @__PURE__ */ new Map();
|
|
872
|
+
#settleCounter = 0;
|
|
873
|
+
/** Fold one emitted event body into the rollup, in log order. */
|
|
874
|
+
observe(body, ts) {
|
|
875
|
+
switch (body.type) {
|
|
876
|
+
case "assistant_message":
|
|
877
|
+
if (body.parentToolUseId != null) {
|
|
878
|
+
const record = this.#recordFor(body.parentToolUseId, ts);
|
|
879
|
+
record.toolCount += toolUseBlocks(body.message.content).length;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
for (const block of toolUseBlocks(body.message.content)) {
|
|
883
|
+
if (!SPAWNER_NAMES.has(block.name)) continue;
|
|
884
|
+
this.#open(block, ts);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
case "user_message": {
|
|
888
|
+
if (body.parentToolUseId != null) {
|
|
889
|
+
this.#recordFor(body.parentToolUseId, ts);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const note = parseTaskNotification(firstText(body.message.content));
|
|
893
|
+
if (note) {
|
|
894
|
+
const record = this.#recordFor(note.toolUseId, ts);
|
|
895
|
+
const status = note.status === "completed" ? "done" : "failed";
|
|
896
|
+
if (record.status !== status) this.#settle(record, status);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const content = body.message.content;
|
|
900
|
+
if (typeof content === "string") return;
|
|
901
|
+
for (const block of content) {
|
|
902
|
+
if (block.type !== "tool_result") continue;
|
|
903
|
+
const result = block;
|
|
904
|
+
if (typeof result.tool_use_id !== "string") continue;
|
|
905
|
+
if (result.is_error !== true && isLaunchAck(result.content)) {
|
|
906
|
+
const record = this.#recordFor(result.tool_use_id, ts);
|
|
907
|
+
if (record.background !== "live") record.background = body.replay === true ? "replay" : "live";
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const record = this.#records.get(result.tool_use_id);
|
|
911
|
+
if (!record) continue;
|
|
912
|
+
if (result.is_error !== true && record.background !== void 0) continue;
|
|
913
|
+
const status = result.is_error === true ? "failed" : "done";
|
|
914
|
+
if (record.status === status) continue;
|
|
915
|
+
this.#settle(record, status);
|
|
916
|
+
}
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
case "sdk_event": {
|
|
920
|
+
const p = body.payload;
|
|
921
|
+
if (p.type !== "system" || typeof p.tool_use_id !== "string") return;
|
|
922
|
+
if (p.subtype === "task_started") {
|
|
923
|
+
const record = this.#recordFor(p.tool_use_id, ts);
|
|
924
|
+
record.background = "live";
|
|
925
|
+
record.agentType ??= cleaned(p.subagent_type);
|
|
926
|
+
record.description ??= cleaned(p.description);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (p.subtype === "task_notification") {
|
|
930
|
+
const record = this.#recordFor(p.tool_use_id, ts);
|
|
931
|
+
const status = p.status === "completed" ? "done" : "failed";
|
|
932
|
+
if (record.status !== status) this.#settle(record, status);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
case "turn_result":
|
|
938
|
+
this.#sweep(false);
|
|
939
|
+
return;
|
|
940
|
+
case "session_closed":
|
|
941
|
+
this.#sweep(true);
|
|
942
|
+
return;
|
|
943
|
+
case "status_changed":
|
|
944
|
+
if (body.status === "idle") this.#sweep(false);
|
|
945
|
+
else if (body.status === "failed" || body.status === "closed") this.#sweep(true);
|
|
946
|
+
return;
|
|
947
|
+
case "conversation_reset":
|
|
948
|
+
this.#records.clear();
|
|
949
|
+
return;
|
|
950
|
+
default: return;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* The rollup as `SessionInfo.subagents` serves it: spawn order (the
|
|
955
|
+
* transcript's own), fresh objects, and `undefined` when there is nothing to
|
|
956
|
+
* say — absent and empty mean the same thing to a client, and an empty array
|
|
957
|
+
* on every row of a 1.2s-polled list is bytes spent saying nothing.
|
|
958
|
+
*/
|
|
959
|
+
list() {
|
|
960
|
+
if (this.#records.size === 0) return void 0;
|
|
961
|
+
const out = [];
|
|
962
|
+
for (const r of this.#records.values()) out.push({
|
|
963
|
+
toolUseId: r.toolUseId,
|
|
964
|
+
agentType: r.agentType,
|
|
965
|
+
description: r.description,
|
|
966
|
+
status: r.status,
|
|
967
|
+
startedAt: r.startedAt,
|
|
968
|
+
toolCount: r.toolCount
|
|
969
|
+
});
|
|
970
|
+
return out;
|
|
971
|
+
}
|
|
972
|
+
#recordFor(toolUseId, ts) {
|
|
973
|
+
let record = this.#records.get(toolUseId);
|
|
974
|
+
if (!record) {
|
|
975
|
+
record = {
|
|
976
|
+
toolUseId,
|
|
977
|
+
status: "running",
|
|
978
|
+
startedAt: ts,
|
|
979
|
+
toolCount: 0
|
|
980
|
+
};
|
|
981
|
+
this.#records.set(toolUseId, record);
|
|
982
|
+
}
|
|
983
|
+
return record;
|
|
984
|
+
}
|
|
985
|
+
#open(block, ts) {
|
|
986
|
+
const record = this.#recordFor(block.id, ts);
|
|
987
|
+
const input = block.input;
|
|
988
|
+
record.agentType ??= cleaned(input?.subagent_type);
|
|
989
|
+
record.description ??= cleaned(input?.description);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* End of turn (`final: false`): anything still running was cut off before
|
|
993
|
+
* its report — except a background agent the live process still hosts, which
|
|
994
|
+
* is designed to outlive the turn and settles by notification instead. End
|
|
995
|
+
* of session (`final: true`): everything, background included, because the
|
|
996
|
+
* process those agents lived in is gone.
|
|
997
|
+
*/
|
|
998
|
+
#sweep(final) {
|
|
999
|
+
for (const record of this.#records.values()) {
|
|
1000
|
+
if (record.status !== "running") continue;
|
|
1001
|
+
if (!final && record.background === "live") continue;
|
|
1002
|
+
this.#settle(record, "failed");
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
#settle(record, status) {
|
|
1006
|
+
record.status = status;
|
|
1007
|
+
record.settledOrder = ++this.#settleCounter;
|
|
1008
|
+
let settled = 0;
|
|
1009
|
+
for (const r of this.#records.values()) if (r.settledOrder !== void 0) settled++;
|
|
1010
|
+
while (settled > SUBAGENT_HISTORY) {
|
|
1011
|
+
let oldestId;
|
|
1012
|
+
let oldestOrder = Infinity;
|
|
1013
|
+
for (const r of this.#records.values()) {
|
|
1014
|
+
if (r.settledOrder === void 0 || r.settledOrder >= oldestOrder) continue;
|
|
1015
|
+
oldestId = r.toolUseId;
|
|
1016
|
+
oldestOrder = r.settledOrder;
|
|
1017
|
+
}
|
|
1018
|
+
if (oldestId === void 0) break;
|
|
1019
|
+
this.#records.delete(oldestId);
|
|
1020
|
+
settled--;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
/** The spawner names observed in the wild: `Task` runs the agent inside the
|
|
1025
|
+
* turn, `Agent` launches it in the background. Deliberately just these two —
|
|
1026
|
+
* a third spelling is caught by `task_started`, the launch receipt, or the
|
|
1027
|
+
* nested-event fallback, so widening this to every tool would only turn
|
|
1028
|
+
* ordinary calls into phantom agents. */
|
|
1029
|
+
const SPAWNER_NAMES = new Set(["Task", "Agent"]);
|
|
1030
|
+
/** The async spawn's immediate `tool_result` — "Async agent launched
|
|
1031
|
+
* successfully. (This tool result is internal metadata …)" — recognized by its
|
|
1032
|
+
* wrapper text because on a resume that text is the only signal there is (the
|
|
1033
|
+
* `SYNTHETIC_USER_PREFIXES` argument; the CLI's system events are not stored).
|
|
1034
|
+
* Live, `task_started` marks the record first and this is redundant armor. */
|
|
1035
|
+
const isLaunchAck = (content) => {
|
|
1036
|
+
const text = typeof content === "string" ? content : firstText(Array.isArray(content) ? content : []);
|
|
1037
|
+
return typeof text === "string" && text.trimStart().startsWith("Async agent launched");
|
|
1038
|
+
};
|
|
1039
|
+
/** A background agent stopping, parsed from the `<task-notification>` wrapper
|
|
1040
|
+
* the CLI writes into the transcript. Field-tolerant on purpose: only the
|
|
1041
|
+
* `tool-use-id` (this rollup's key) and the `status` verdict are read. */
|
|
1042
|
+
const parseTaskNotification = (text) => {
|
|
1043
|
+
if (text === void 0 || !text.trimStart().startsWith("<task-notification>")) return void 0;
|
|
1044
|
+
const toolUseId = /<tool-use-id>\s*([^<\s]+)\s*<\/tool-use-id>/.exec(text)?.[1];
|
|
1045
|
+
if (toolUseId === void 0) return void 0;
|
|
1046
|
+
return {
|
|
1047
|
+
toolUseId,
|
|
1048
|
+
status: /<status>\s*([^<]*?)\s*<\/status>/.exec(text)?.[1] ?? ""
|
|
1049
|
+
};
|
|
1050
|
+
};
|
|
1051
|
+
/** The first text of a message body, however the content is spelled — the
|
|
1052
|
+
* stored transcript uses bare strings, the live stream uses blocks. */
|
|
1053
|
+
const firstText = (content) => {
|
|
1054
|
+
if (typeof content === "string") return content;
|
|
1055
|
+
for (const block of content) {
|
|
1056
|
+
const b = block;
|
|
1057
|
+
if (b?.type === "text" && typeof b.text === "string") return b.text;
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
/** Trim, drop blank, clip at the same 80 the terminal theme's `taskLabel` uses.
|
|
1061
|
+
* Model-authored input rides every row of a polled sessions list, so it is
|
|
1062
|
+
* bounded here rather than trusted — a 10KB `description` would be paid for at
|
|
1063
|
+
* every poll. */
|
|
1064
|
+
const cleaned = (value) => {
|
|
1065
|
+
if (typeof value !== "string") return void 0;
|
|
1066
|
+
const text = value.trim();
|
|
1067
|
+
if (text === "") return void 0;
|
|
1068
|
+
return text.length > 80 ? text.slice(0, 79) + "…" : text;
|
|
1069
|
+
};
|
|
1070
|
+
/** The `tool_use` blocks of a message body, however the content is spelled. */
|
|
1071
|
+
function toolUseBlocks(content) {
|
|
1072
|
+
if (typeof content === "string") return [];
|
|
1073
|
+
const blocks = [];
|
|
1074
|
+
for (const block of content) {
|
|
1075
|
+
if (block.type !== "tool_use") continue;
|
|
1076
|
+
const b = block;
|
|
1077
|
+
if (typeof b.id !== "string" || typeof b.name !== "string") continue;
|
|
1078
|
+
blocks.push({
|
|
1079
|
+
id: b.id,
|
|
1080
|
+
name: b.name,
|
|
1081
|
+
input: b.input
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
return blocks;
|
|
1085
|
+
}
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/engines/claude/runner.ts
|
|
403
1088
|
const DEFAULT_APPROVAL_TIMEOUT_MS$1 = 3e5;
|
|
404
1089
|
/**
|
|
405
1090
|
* One live Agent SDK session: owns the query() call, the streaming input queue, the
|
|
@@ -413,9 +1098,18 @@ var SessionRunner = class {
|
|
|
413
1098
|
/** {@link SessionRunnerConfig.cwd}, checked once in the constructor. */
|
|
414
1099
|
#cwd;
|
|
415
1100
|
#events = [];
|
|
416
|
-
#
|
|
1101
|
+
#subscribers = new SubscriberSet();
|
|
417
1102
|
#seq = 0;
|
|
418
1103
|
#activityCount = 0;
|
|
1104
|
+
/**
|
|
1105
|
+
* Seq of the latest `conversation_reset` event, 0 when none. The log itself is
|
|
1106
|
+
* never truncated — it still carries the state-bearing events (`capabilities`,
|
|
1107
|
+
* `system_init`, …) a fresh attacher depends on and which are not re-emitted —
|
|
1108
|
+
* but `subscribe()` skips transcript *content* strictly below this mark, so a
|
|
1109
|
+
* replay does not resurrect a cleared conversation. A later reset supersedes
|
|
1110
|
+
* an earlier one by overwriting it.
|
|
1111
|
+
*/
|
|
1112
|
+
#resetSeq = 0;
|
|
419
1113
|
#status = "starting";
|
|
420
1114
|
#statusDetail;
|
|
421
1115
|
#sdkSessionId;
|
|
@@ -423,6 +1117,28 @@ var SessionRunner = class {
|
|
|
423
1117
|
#apiKeySource;
|
|
424
1118
|
#permissionMode;
|
|
425
1119
|
#pending = /* @__PURE__ */ new Map();
|
|
1120
|
+
/**
|
|
1121
|
+
* The turn ended while an approval was standing, and nothing has started a
|
|
1122
|
+
* new one since.
|
|
1123
|
+
*
|
|
1124
|
+
* `awaiting_approval` rightly outranks `idle` for display, so a turn-over
|
|
1125
|
+
* signal arriving under a standing approval cannot be applied when it lands.
|
|
1126
|
+
* It used to be **discarded** for that reason, which is a different thing
|
|
1127
|
+
* from outranked: the settle path then asserted `running` on the assumption
|
|
1128
|
+
* that an answered approval means work resumes, and when the turn was already
|
|
1129
|
+
* over — an interrupt, a timeout — the session claimed to be running one that
|
|
1130
|
+
* had produced its result. Status is purely edge-driven here, with no poll and
|
|
1131
|
+
* no reconciliation anywhere, so that single dropped edge never came back and
|
|
1132
|
+
* every client rendered it faithfully for the life of the session.
|
|
1133
|
+
*
|
|
1134
|
+
* So the fact is *deferred* rather than dropped, and it is deliberately
|
|
1135
|
+
* cleared the moment work genuinely resumes — a turn-over belongs to the turn
|
|
1136
|
+
* that produced it and must not settle the next one.
|
|
1137
|
+
*/
|
|
1138
|
+
#turnOverWhileBlocked = false;
|
|
1139
|
+
/** The read-time sub-agent rollup (`SessionInfo.subagents`), fed from #emit —
|
|
1140
|
+
* the one chokepoint — so the resume backfill reconstructs it for free. */
|
|
1141
|
+
#subagents = new SubagentTracker();
|
|
426
1142
|
#totalCostUsd;
|
|
427
1143
|
#numTurns;
|
|
428
1144
|
#lastActivityAt;
|
|
@@ -432,6 +1148,9 @@ var SessionRunner = class {
|
|
|
432
1148
|
/** Last plan reported by the usage poll, so `plan_info` is emitted on change
|
|
433
1149
|
* rather than once per turn. */
|
|
434
1150
|
#subscriptionType;
|
|
1151
|
+
/** The title the CLI gave this thread (see `#fetchEngineTitle`). Undefined
|
|
1152
|
+
* until it has one — a session gets its summary a turn or two in. */
|
|
1153
|
+
#engineTitle;
|
|
435
1154
|
#started = false;
|
|
436
1155
|
#closed = false;
|
|
437
1156
|
#runPromise;
|
|
@@ -476,6 +1195,7 @@ var SessionRunner = class {
|
|
|
476
1195
|
lastSeq: this.#seq,
|
|
477
1196
|
activityCount: this.#activityCount,
|
|
478
1197
|
pendingPermissionCount: this.#pending.size,
|
|
1198
|
+
subagents: this.#subagents.list(),
|
|
479
1199
|
meta: this.#config.meta,
|
|
480
1200
|
scope: this.#config.scope,
|
|
481
1201
|
title: this.#title(),
|
|
@@ -484,9 +1204,20 @@ var SessionRunner = class {
|
|
|
484
1204
|
lastActivityAt: this.#lastActivityAt
|
|
485
1205
|
};
|
|
486
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Three sources, most-deliberate first: the host's own rename (`meta.title`),
|
|
1209
|
+
* the title the CLI gave this thread (`#engineTitle`), then the first prompt
|
|
1210
|
+
* truncated.
|
|
1211
|
+
*
|
|
1212
|
+
* The rename outranks everything by design — a person naming a session must
|
|
1213
|
+
* not have it renamed under them by a model — which is also why the engine
|
|
1214
|
+
* title is *only ever read* while `meta.title` is unset (see
|
|
1215
|
+
* `#fetchEngineTitle`), rather than read and then discarded here.
|
|
1216
|
+
*/
|
|
487
1217
|
#title() {
|
|
488
1218
|
const metaTitle = this.#config.meta?.title;
|
|
489
1219
|
if (typeof metaTitle === "string" && metaTitle.length > 0) return metaTitle;
|
|
1220
|
+
if (this.#engineTitle) return this.#engineTitle;
|
|
490
1221
|
const prompt = this.#config.prompt;
|
|
491
1222
|
if (!prompt) return void 0;
|
|
492
1223
|
return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
|
|
@@ -613,14 +1344,27 @@ var SessionRunner = class {
|
|
|
613
1344
|
});
|
|
614
1345
|
this.#setStatus("closed");
|
|
615
1346
|
}
|
|
1347
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
1348
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
1349
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
1350
|
+
eventAt(seq) {
|
|
1351
|
+
return this.#events.find((event) => event.seq === seq);
|
|
1352
|
+
}
|
|
616
1353
|
/**
|
|
617
1354
|
* Replay buffered events with seq > afterSeq, then deliver live events.
|
|
618
1355
|
* Returns an unsubscribe function.
|
|
1356
|
+
*
|
|
1357
|
+
* Replay honours the reset watermark: transcript content below the latest
|
|
1358
|
+
* `conversation_reset` is skipped (the reducer would clear it again anyway,
|
|
1359
|
+
* and a pre-reset client that never learned the reducer's case would render
|
|
1360
|
+
* a conversation the engine has discarded), while state-bearing events —
|
|
1361
|
+
* which are emitted once and never again — always replay. The reset event
|
|
1362
|
+
* itself replays (the skip is strictly-below), which is what clears a
|
|
1363
|
+
* reconnecting client still holding pre-reset rows; superseded resets are
|
|
1364
|
+
* content below the newer one and are skipped with what they cleared.
|
|
619
1365
|
*/
|
|
620
|
-
subscribe(listener, afterSeq = 0) {
|
|
621
|
-
|
|
622
|
-
this.#listeners.add(listener);
|
|
623
|
-
return () => this.#listeners.delete(listener);
|
|
1366
|
+
subscribe(listener, afterSeq = 0, options) {
|
|
1367
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
|
|
624
1368
|
}
|
|
625
1369
|
async #run() {
|
|
626
1370
|
const queryFn = this.#config.queryFn ?? query;
|
|
@@ -676,14 +1420,17 @@ var SessionRunner = class {
|
|
|
676
1420
|
}
|
|
677
1421
|
for (const m of messages) {
|
|
678
1422
|
if (this.#closed) return;
|
|
679
|
-
if (m.type === "user")
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1423
|
+
if (m.type === "user") {
|
|
1424
|
+
const message = toApiMessage(m.message);
|
|
1425
|
+
this.#emit({
|
|
1426
|
+
type: "user_message",
|
|
1427
|
+
message,
|
|
1428
|
+
parentToolUseId: m.parent_tool_use_id,
|
|
1429
|
+
replay: true,
|
|
1430
|
+
synthetic: isSyntheticUserText(message) ? true : void 0,
|
|
1431
|
+
uuid: m.uuid
|
|
1432
|
+
});
|
|
1433
|
+
} else if (m.type === "assistant") this.#emit({
|
|
687
1434
|
type: "assistant_message",
|
|
688
1435
|
message: toApiMessage(m.message),
|
|
689
1436
|
parentToolUseId: m.parent_tool_use_id,
|
|
@@ -708,6 +1455,7 @@ var SessionRunner = class {
|
|
|
708
1455
|
forkSession: c.forkSession,
|
|
709
1456
|
effort: c.reasoningEffort,
|
|
710
1457
|
includePartialMessages: c.includePartialMessages ?? true,
|
|
1458
|
+
forwardSubagentText: true,
|
|
711
1459
|
canUseTool: this.#canUseTool,
|
|
712
1460
|
env: c.env,
|
|
713
1461
|
pathToClaudeCodeExecutable: c.pathToClaudeCodeExecutable,
|
|
@@ -734,14 +1482,20 @@ var SessionRunner = class {
|
|
|
734
1482
|
claudeCodeVersion: msg.claude_code_version,
|
|
735
1483
|
mcpServers: msg.mcp_servers
|
|
736
1484
|
});
|
|
1485
|
+
this.#turnOverWhileBlocked = false;
|
|
737
1486
|
this.#setStatus("running");
|
|
738
1487
|
this.#fetchCapabilities();
|
|
739
1488
|
this.#fetchContextUsage();
|
|
740
1489
|
this.#fetchRateLimits();
|
|
1490
|
+
this.#fetchEngineTitle();
|
|
741
1491
|
return;
|
|
742
1492
|
}
|
|
743
1493
|
if (msg.type === "system" && msg.subtype === "session_state_changed") {
|
|
744
|
-
if (this.#pending.size > 0)
|
|
1494
|
+
if (this.#pending.size > 0) {
|
|
1495
|
+
if (msg.state === "idle") this.#turnOverWhileBlocked = true;
|
|
1496
|
+
else if (msg.state === "running") this.#turnOverWhileBlocked = false;
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
745
1499
|
if (msg.state === "idle") this.#setStatus("idle");
|
|
746
1500
|
else if (msg.state === "running") this.#setStatus("running");
|
|
747
1501
|
return;
|
|
@@ -749,12 +1503,18 @@ var SessionRunner = class {
|
|
|
749
1503
|
const body = normalizeSdkMessage(msg);
|
|
750
1504
|
if (body) {
|
|
751
1505
|
this.#emit(body);
|
|
1506
|
+
if (body.type === "conversation_reset") {
|
|
1507
|
+
if (body.sdkSessionId) this.#sdkSessionId = body.sdkSessionId;
|
|
1508
|
+
this.#fetchContextUsage();
|
|
1509
|
+
}
|
|
752
1510
|
if (body.type === "turn_result") {
|
|
753
1511
|
this.#totalCostUsd = body.totalCostUsd;
|
|
754
1512
|
this.#numTurns = body.numTurns;
|
|
755
1513
|
if (this.#pending.size === 0) this.#setStatus("idle");
|
|
1514
|
+
else this.#turnOverWhileBlocked = true;
|
|
756
1515
|
this.#fetchContextUsage();
|
|
757
1516
|
this.#fetchRateLimits();
|
|
1517
|
+
this.#fetchEngineTitle();
|
|
758
1518
|
}
|
|
759
1519
|
}
|
|
760
1520
|
}
|
|
@@ -784,6 +1544,44 @@ var SessionRunner = class {
|
|
|
784
1544
|
});
|
|
785
1545
|
} catch {}
|
|
786
1546
|
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Adopt the title the CLI gave this thread — the "friendly title" it writes a
|
|
1549
|
+
* turn or two into a session, and the name a resumed thread already carries.
|
|
1550
|
+
*
|
|
1551
|
+
* A **poll, not an observation**, and unavoidably so: no member of the SDK's
|
|
1552
|
+
* `SDKMessage` union carries it (the whole union was checked). It lives on
|
|
1553
|
+
* `SDKSessionInfo`, which only `getSessionInfo` / `listSessions` return — the
|
|
1554
|
+
* same record `GET /sdk-sessions` already serves as `SdkSessionSummary`. So it
|
|
1555
|
+
* is read at init and after each turn, which is also roughly the rate at which
|
|
1556
|
+
* it changes.
|
|
1557
|
+
*
|
|
1558
|
+
* Two rules:
|
|
1559
|
+
* - **Never while `meta.title` is set.** A rename is a person's decision and a
|
|
1560
|
+
* generated summary must not overwrite it. Not read at all in that case, so
|
|
1561
|
+
* there is no stored value waiting to resurface if the rename is cleared —
|
|
1562
|
+
* the next turn simply fetches it again.
|
|
1563
|
+
* - `summary` falls back to the first prompt when the session has no real
|
|
1564
|
+
* title yet, so it is taken only when it *differs* from `firstPrompt`.
|
|
1565
|
+
* Otherwise `#title()`'s own prompt fallback covers it, and the two would
|
|
1566
|
+
* disagree only in how they truncate.
|
|
1567
|
+
*
|
|
1568
|
+
* Best-effort throughout: an unreadable transcript, a session file that is not
|
|
1569
|
+
* there yet, an SDK without the function — all leave the title as it was.
|
|
1570
|
+
*/
|
|
1571
|
+
async #fetchEngineTitle() {
|
|
1572
|
+
const metaTitle = this.#config.meta?.title;
|
|
1573
|
+
if (typeof metaTitle === "string" && metaTitle.length > 0) return;
|
|
1574
|
+
const sdkSessionId = this.#sdkSessionId;
|
|
1575
|
+
if (!sdkSessionId) return;
|
|
1576
|
+
const read = this.#config.sessionInfoFn ?? getSessionInfo;
|
|
1577
|
+
try {
|
|
1578
|
+
const info = await read(sdkSessionId, { dir: this.#cwd });
|
|
1579
|
+
if (this.#closed || !info) return;
|
|
1580
|
+
const summary = info.summary && info.summary !== info.firstPrompt ? info.summary : void 0;
|
|
1581
|
+
const title = info.customTitle || summary;
|
|
1582
|
+
if (title) this.#engineTitle = title;
|
|
1583
|
+
} catch {}
|
|
1584
|
+
}
|
|
787
1585
|
/** Snapshot the context window after a turn and surface it as an event. Optional-chained
|
|
788
1586
|
* and best-effort for the same reasons as #fetchCapabilities. */
|
|
789
1587
|
async #fetchContextUsage() {
|
|
@@ -944,7 +1742,12 @@ var SessionRunner = class {
|
|
|
944
1742
|
resolvedBy,
|
|
945
1743
|
message: decision.behavior === "deny" ? decision.message ?? "Denied" : void 0
|
|
946
1744
|
});
|
|
947
|
-
if (this.#pending.size === 0
|
|
1745
|
+
if (this.#pending.size === 0) {
|
|
1746
|
+
const endedWhileBlocked = this.#turnOverWhileBlocked;
|
|
1747
|
+
this.#turnOverWhileBlocked = false;
|
|
1748
|
+
if (endedWhileBlocked) this.#setStatus("idle");
|
|
1749
|
+
else if (this.#status === "awaiting_approval") this.#setStatus("running");
|
|
1750
|
+
}
|
|
948
1751
|
}
|
|
949
1752
|
#setStatus(status, detail) {
|
|
950
1753
|
if (this.#status === status && this.#statusDetail === detail) return;
|
|
@@ -965,10 +1768,10 @@ var SessionRunner = class {
|
|
|
965
1768
|
};
|
|
966
1769
|
this.#lastActivityAt = event.ts;
|
|
967
1770
|
this.#activityCount += transcriptActivity(body);
|
|
1771
|
+
if (body.type === "conversation_reset") this.#resetSeq = event.seq;
|
|
1772
|
+
this.#subagents.observe(body, event.ts);
|
|
968
1773
|
this.#events.push(event);
|
|
969
|
-
|
|
970
|
-
listener(event);
|
|
971
|
-
} catch {}
|
|
1774
|
+
this.#subscribers.emit(event);
|
|
972
1775
|
}
|
|
973
1776
|
};
|
|
974
1777
|
/** Answer each AskUserQuestion question with its first option's label — the tool's
|
|
@@ -986,7 +1789,7 @@ function recommendedAnswers(input) {
|
|
|
986
1789
|
return answers;
|
|
987
1790
|
}
|
|
988
1791
|
//#endregion
|
|
989
|
-
//#region src/
|
|
1792
|
+
//#region src/engines/provider/runner.ts
|
|
990
1793
|
/** Permission modes this engine can honor. The rest of the protocol vocabulary
|
|
991
1794
|
* (acceptEdits/plan/auto) is Claude Code CLI semantics with no meaning here —
|
|
992
1795
|
* setPermissionMode rejects them, which the server surfaces as protocol_error. */
|
|
@@ -1011,7 +1814,7 @@ var AiSdkRunner = class {
|
|
|
1011
1814
|
#config;
|
|
1012
1815
|
#model;
|
|
1013
1816
|
#events = [];
|
|
1014
|
-
#
|
|
1817
|
+
#subscribers = new SubscriberSet();
|
|
1015
1818
|
#seq = 0;
|
|
1016
1819
|
#activityCount = 0;
|
|
1017
1820
|
#status = "starting";
|
|
@@ -1124,10 +1927,7 @@ var AiSdkRunner = class {
|
|
|
1124
1927
|
start() {
|
|
1125
1928
|
if (this.#started) return this.#turnChain;
|
|
1126
1929
|
this.#started = true;
|
|
1127
|
-
if (this.#config.restore)
|
|
1128
|
-
if (this.#pendingToolCalls.size === 0) this.#scheduleTurn();
|
|
1129
|
-
return this.#turnChain;
|
|
1130
|
-
}
|
|
1930
|
+
if (this.#config.restore) return this.#turnChain;
|
|
1131
1931
|
this.#setStatus("idle");
|
|
1132
1932
|
if (this.#config.prompt) this.sendMessage(this.#config.prompt);
|
|
1133
1933
|
return this.#turnChain;
|
|
@@ -1142,6 +1942,61 @@ var AiSdkRunner = class {
|
|
|
1142
1942
|
if (this.#closed || this.#parked) return void 0;
|
|
1143
1943
|
if (this.#abort || !this.#restingOnDeferred()) return void 0;
|
|
1144
1944
|
this.#setStatus("parked");
|
|
1945
|
+
const snapshot = this.#buildSnapshot();
|
|
1946
|
+
this.#parked = true;
|
|
1947
|
+
this.#subscribers.clear();
|
|
1948
|
+
try {
|
|
1949
|
+
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1950
|
+
} catch {}
|
|
1951
|
+
return snapshot;
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* The same snapshot, taken without ending anything.
|
|
1955
|
+
*
|
|
1956
|
+
* `park()` and this are two operations that happen to produce the same value,
|
|
1957
|
+
* and the difference is the whole point: `park()` *ends* the live runner
|
|
1958
|
+
* (inert, listeners dropped, `onClose` called), which is right for deferred
|
|
1959
|
+
* execution — the session has nothing to do for possibly days — and wrong for
|
|
1960
|
+
* restart-survival, where the session is active and someone is mid-
|
|
1961
|
+
* conversation. This one changes nothing at all: no status emit, no listener
|
|
1962
|
+
* clear, no disposer. The host writes the value through to durable storage
|
|
1963
|
+
* after each turn and keeps the runner live and warm, so a restart rebuilds
|
|
1964
|
+
* from the last write through the existing `restore` path and the next message
|
|
1965
|
+
* costs no wake.
|
|
1966
|
+
*
|
|
1967
|
+
* The gate is `park()`'s minus the requirement that there be something parked:
|
|
1968
|
+
*
|
|
1969
|
+
* - `#abort` set is refused for the reason it always was — a `generate()` in
|
|
1970
|
+
* flight has produced messages that are not in the history yet, so the
|
|
1971
|
+
* snapshot would be of a turn that half-happened.
|
|
1972
|
+
* - Pending calls that are **not** all deferred are refused, which is
|
|
1973
|
+
* `park()`'s rule wearing a different hat. An in-process execution's result
|
|
1974
|
+
* is coming back to *this* runner and dies with the process; a restore would
|
|
1975
|
+
* wait on it forever, and `state.dispatched` is what would stop the rebuilt
|
|
1976
|
+
* runner from simply calling it again.
|
|
1977
|
+
* - Idle with nothing pending — the case `park()` exists to refuse — is
|
|
1978
|
+
* exactly the case this exists to allow.
|
|
1979
|
+
*/
|
|
1980
|
+
snapshot() {
|
|
1981
|
+
if (this.#closed || this.#parked || this.#abort) return void 0;
|
|
1982
|
+
if (this.#pendingToolCalls.size > 0 && !this.#restingOnDeferred()) return void 0;
|
|
1983
|
+
return this.#buildSnapshot();
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* The snapshot value itself, shared so a park and a write-through cannot
|
|
1987
|
+
* disagree about what a session *is*.
|
|
1988
|
+
*
|
|
1989
|
+
* The event log is filtered through {@link snapshotRetains} — the persisted
|
|
1990
|
+
* log drops stream deltas, which are superseded by the `assistant_message`
|
|
1991
|
+
* that flushes them and would otherwise be tens of times the size of the text
|
|
1992
|
+
* they spell. Parks get it too, and should: a park sits on disk for days.
|
|
1993
|
+
*
|
|
1994
|
+
* The `parked` list and `state.parkedAt` are honest under both callers. An
|
|
1995
|
+
* idle write-through has no pending calls, so `parked` is empty and the host
|
|
1996
|
+
* arms no watchdogs; `parkedAt` is "when this was taken", which is what
|
|
1997
|
+
* `#restore` needs to discount a turn's clock either way.
|
|
1998
|
+
*/
|
|
1999
|
+
#buildSnapshot() {
|
|
1145
2000
|
const parked = [...this.#pendingToolCalls.values()].map((call) => ({
|
|
1146
2001
|
executionId: call.toolCallId,
|
|
1147
2002
|
toolName: call.toolName,
|
|
@@ -1159,22 +2014,16 @@ var AiSdkRunner = class {
|
|
|
1159
2014
|
lastActivityAt: this.#lastActivityAt,
|
|
1160
2015
|
parkedAt: Date.now()
|
|
1161
2016
|
};
|
|
1162
|
-
|
|
2017
|
+
return {
|
|
1163
2018
|
engine: "provider",
|
|
1164
2019
|
id: this.id,
|
|
1165
2020
|
createdAt: this.createdAt,
|
|
1166
2021
|
seq: this.#seq,
|
|
1167
|
-
events:
|
|
2022
|
+
events: this.#events.filter((event) => snapshotRetains(event)),
|
|
1168
2023
|
vfs: this.#config.vfs?.snapshot(),
|
|
1169
2024
|
parked,
|
|
1170
2025
|
state
|
|
1171
2026
|
};
|
|
1172
|
-
this.#parked = true;
|
|
1173
|
-
this.#listeners.clear();
|
|
1174
|
-
try {
|
|
1175
|
-
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1176
|
-
} catch {}
|
|
1177
|
-
return snapshot;
|
|
1178
2027
|
}
|
|
1179
2028
|
sendMessage(text, attachments) {
|
|
1180
2029
|
if (this.#parked) throw new Error("session is parked");
|
|
@@ -1357,10 +2206,14 @@ var AiSdkRunner = class {
|
|
|
1357
2206
|
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1358
2207
|
} catch {}
|
|
1359
2208
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
2209
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
2210
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
2211
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
2212
|
+
eventAt(seq) {
|
|
2213
|
+
return this.#events.find((event) => event.seq === seq);
|
|
2214
|
+
}
|
|
2215
|
+
subscribe(listener, afterSeq = 0, options) {
|
|
2216
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
|
|
1364
2217
|
}
|
|
1365
2218
|
#scheduleTurn() {
|
|
1366
2219
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
@@ -1489,29 +2342,29 @@ var AiSdkRunner = class {
|
|
|
1489
2342
|
cacheWrite: 0,
|
|
1490
2343
|
cacheRead: 0
|
|
1491
2344
|
};
|
|
2345
|
+
let blocks = [];
|
|
2346
|
+
const textBuf = /* @__PURE__ */ new Map();
|
|
2347
|
+
const reasoningBuf = /* @__PURE__ */ new Map();
|
|
2348
|
+
const flush = () => {
|
|
2349
|
+
if (blocks.length === 0) return;
|
|
2350
|
+
this.#emit({
|
|
2351
|
+
type: "assistant_message",
|
|
2352
|
+
message: {
|
|
2353
|
+
role: "assistant",
|
|
2354
|
+
content: blocks,
|
|
2355
|
+
model: this.#modelId()
|
|
2356
|
+
},
|
|
2357
|
+
parentToolUseId: null,
|
|
2358
|
+
uuid: randomUUID()
|
|
2359
|
+
});
|
|
2360
|
+
blocks = [];
|
|
2361
|
+
};
|
|
1492
2362
|
try {
|
|
1493
2363
|
const result = await agent.stream({
|
|
1494
2364
|
messages: [...this.#messages],
|
|
1495
2365
|
abortSignal: abort.signal
|
|
1496
2366
|
});
|
|
1497
2367
|
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
2368
|
const emitToolResult = (toolCallId, content, isError) => {
|
|
1516
2369
|
flush();
|
|
1517
2370
|
this.#emit({
|
|
@@ -1641,6 +2494,15 @@ var AiSdkRunner = class {
|
|
|
1641
2494
|
this.#finishTurn(text);
|
|
1642
2495
|
} catch (error) {
|
|
1643
2496
|
if (this.#closed) return;
|
|
2497
|
+
for (const [, thinking] of reasoningBuf) if (thinking) blocks.push({
|
|
2498
|
+
type: "thinking",
|
|
2499
|
+
thinking
|
|
2500
|
+
});
|
|
2501
|
+
for (const [, text] of textBuf) if (text) blocks.push({
|
|
2502
|
+
type: "text",
|
|
2503
|
+
text
|
|
2504
|
+
});
|
|
2505
|
+
flush();
|
|
1644
2506
|
const message = error instanceof Error ? error.message : String(error);
|
|
1645
2507
|
this.#numTurns += 1;
|
|
1646
2508
|
this.#emit({
|
|
@@ -1741,9 +2603,7 @@ var AiSdkRunner = class {
|
|
|
1741
2603
|
this.#lastActivityAt = event.ts;
|
|
1742
2604
|
this.#activityCount += transcriptActivity(body);
|
|
1743
2605
|
this.#events.push(event);
|
|
1744
|
-
|
|
1745
|
-
listener(event);
|
|
1746
|
-
} catch {}
|
|
2606
|
+
this.#subscribers.emit(event);
|
|
1747
2607
|
}
|
|
1748
2608
|
};
|
|
1749
2609
|
function turnUsage(accum) {
|
|
@@ -1761,7 +2621,7 @@ function errorText(error) {
|
|
|
1761
2621
|
return error instanceof Error ? error.message : String(error);
|
|
1762
2622
|
}
|
|
1763
2623
|
//#endregion
|
|
1764
|
-
//#region src/claude
|
|
2624
|
+
//#region src/engines/claude/auth.ts
|
|
1765
2625
|
/**
|
|
1766
2626
|
* The native Claude Code binary the Agent SDK itself spawns, resolved the way
|
|
1767
2627
|
* the SDK resolves it: the platform-specific optional dependency installed next
|
|
@@ -1815,7 +2675,7 @@ function checkClaudeAuth(env, options = {}) {
|
|
|
1815
2675
|
});
|
|
1816
2676
|
}
|
|
1817
2677
|
//#endregion
|
|
1818
|
-
//#region src/quickjs-executor.ts
|
|
2678
|
+
//#region src/executors/quickjs-executor.ts
|
|
1819
2679
|
/**
|
|
1820
2680
|
* In-process execution backend: runs a tool's untrusted script in the QuickJS
|
|
1821
2681
|
* WASM guest. Always settles inline — nothing downstream assumes that, which is
|
|
@@ -1913,7 +2773,7 @@ function isHostAllowed(url, allowedHosts) {
|
|
|
1913
2773
|
});
|
|
1914
2774
|
}
|
|
1915
2775
|
//#endregion
|
|
1916
|
-
//#region src/pending-registry.ts
|
|
2776
|
+
//#region src/lib/pending-registry.ts
|
|
1917
2777
|
var PendingRequestRegistry = class {
|
|
1918
2778
|
#slots = /* @__PURE__ */ new Map();
|
|
1919
2779
|
get size() {
|
|
@@ -2020,7 +2880,7 @@ function toEntry(slot) {
|
|
|
2020
2880
|
};
|
|
2021
2881
|
}
|
|
2022
2882
|
//#endregion
|
|
2023
|
-
//#region src/browser-bridge-executor.ts
|
|
2883
|
+
//#region src/executors/browser-bridge-executor.ts
|
|
2024
2884
|
/**
|
|
2025
2885
|
* Executes tool calls in the attached client's own sandbox. The first backend
|
|
2026
2886
|
* that genuinely returns `pending`: dispatch puts a request on the wire and
|
|
@@ -2131,7 +2991,7 @@ function toExecutionResult(outcome) {
|
|
|
2131
2991
|
};
|
|
2132
2992
|
}
|
|
2133
2993
|
//#endregion
|
|
2134
|
-
//#region src/deferred-executor.ts
|
|
2994
|
+
//#region src/executors/deferred-executor.ts
|
|
2135
2995
|
/**
|
|
2136
2996
|
* The executor for work that outlives the session's process residency: dispatch
|
|
2137
2997
|
* hands the call off and returns `pending` **without holding a promise**, because
|
|
@@ -2178,7 +3038,7 @@ var DeferredExecutor = class {
|
|
|
2178
3038
|
}
|
|
2179
3039
|
};
|
|
2180
3040
|
//#endregion
|
|
2181
|
-
//#region src/tools.ts
|
|
3041
|
+
//#region src/engines/provider/tools.ts
|
|
2182
3042
|
const MAX_FILE_BYTES = 1024 * 1024;
|
|
2183
3043
|
/**
|
|
2184
3044
|
* Build the capability-scoped tool set for a session.
|
|
@@ -2394,7 +3254,7 @@ function truncate(text) {
|
|
|
2394
3254
|
return text.length > MAX_FILE_BYTES ? text.slice(0, MAX_FILE_BYTES) : text;
|
|
2395
3255
|
}
|
|
2396
3256
|
//#endregion
|
|
2397
|
-
//#region src/web-fetch.ts
|
|
3257
|
+
//#region src/engines/provider/web-fetch.ts
|
|
2398
3258
|
const MAX_CACHE_ENTRIES = 64;
|
|
2399
3259
|
const MAX_REDIRECTS = 5;
|
|
2400
3260
|
function createWebFetch(options = {}) {
|
|
@@ -2606,7 +3466,7 @@ function decodeEntities(text) {
|
|
|
2606
3466
|
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
3467
|
}
|
|
2608
3468
|
//#endregion
|
|
2609
|
-
//#region src/
|
|
3469
|
+
//#region src/engines/provider/session.ts
|
|
2610
3470
|
/** Which capability a wired backend yields, for grant filtering. */
|
|
2611
3471
|
const CAPABILITY_TOOLS = {
|
|
2612
3472
|
search: "web_search",
|
|
@@ -3315,15 +4175,29 @@ function userQuestionsFromCodex(questions) {
|
|
|
3315
4175
|
}))
|
|
3316
4176
|
}));
|
|
3317
4177
|
}
|
|
3318
|
-
/**
|
|
3319
|
-
*
|
|
3320
|
-
*
|
|
4178
|
+
/**
|
|
4179
|
+
* The text of a history `userMessage` item: its content entries' text parts
|
|
4180
|
+
* joined.
|
|
4181
|
+
*
|
|
4182
|
+
* Image parts have no replayable representation — the bytes went to the model,
|
|
4183
|
+
* not into the rollout we can render from — so they are named rather than
|
|
4184
|
+
* dropped. A prompt that was *only* an image used to produce an empty string,
|
|
4185
|
+
* which the caller read as "nothing to replay" and skipped: the turn lost its
|
|
4186
|
+
* user row and, with it, the prompt mark the scrubber navigates by, so a resumed
|
|
4187
|
+
* thread had answers with no visible question. A word in place of the picture is
|
|
4188
|
+
* a smaller lie than a turn that never happened.
|
|
4189
|
+
*/
|
|
3321
4190
|
function historyUserText(item) {
|
|
3322
4191
|
if (!Array.isArray(item.content)) return "";
|
|
3323
|
-
|
|
4192
|
+
let images = 0;
|
|
4193
|
+
const text = item.content.map((part) => {
|
|
3324
4194
|
const candidate = part;
|
|
3325
|
-
|
|
4195
|
+
if (candidate?.type === "text" && typeof candidate.text === "string") return candidate.text;
|
|
4196
|
+
if (typeof candidate?.type === "string" && candidate.type.toLowerCase().includes("image")) images += 1;
|
|
4197
|
+
return "";
|
|
3326
4198
|
}).filter(Boolean).join("\n");
|
|
4199
|
+
if (text) return text;
|
|
4200
|
+
return images > 0 ? `[${images === 1 ? "image" : `${images} images`}]` : "";
|
|
3327
4201
|
}
|
|
3328
4202
|
/** The AskUserQuestion answer convention (question text → chosen label(s),
|
|
3329
4203
|
* comma-joined) mapped back to codex's id-keyed shape. Questions the client
|
|
@@ -3486,7 +4360,7 @@ var CodexRunner = class {
|
|
|
3486
4360
|
/** {@link CodexRunnerConfig.cwd}, checked once in the constructor. */
|
|
3487
4361
|
#cwd;
|
|
3488
4362
|
#events = [];
|
|
3489
|
-
#
|
|
4363
|
+
#subscribers = new SubscriberSet();
|
|
3490
4364
|
#seq = 0;
|
|
3491
4365
|
#activityCount = 0;
|
|
3492
4366
|
#status = "starting";
|
|
@@ -3827,10 +4701,14 @@ var CodexRunner = class {
|
|
|
3827
4701
|
});
|
|
3828
4702
|
this.#setStatus("closed");
|
|
3829
4703
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
4704
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
4705
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
4706
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
4707
|
+
eventAt(seq) {
|
|
4708
|
+
return this.#events.find((event) => event.seq === seq);
|
|
4709
|
+
}
|
|
4710
|
+
subscribe(listener, afterSeq = 0, options) {
|
|
4711
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
|
|
3834
4712
|
}
|
|
3835
4713
|
#scheduleTurn() {
|
|
3836
4714
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
@@ -4155,131 +5033,137 @@ var CodexRunner = class {
|
|
|
4155
5033
|
}
|
|
4156
5034
|
#handleNotification(method, params) {
|
|
4157
5035
|
if (this.#closed) return;
|
|
5036
|
+
this.#notifications[method]?.(params);
|
|
5037
|
+
}
|
|
5038
|
+
/** Reasoning deltas arrive on two methods that differ only in which section
|
|
5039
|
+
* counter they advance; the section key carries the method so the two streams
|
|
5040
|
+
* never share a boundary. Section boundaries (a new summary/content entry)
|
|
5041
|
+
* render as paragraph breaks — the completed item joins sections with '\n\n'. */
|
|
5042
|
+
#reasoningDelta(method) {
|
|
5043
|
+
return (params) => {
|
|
5044
|
+
const active = this.#activeTurn;
|
|
5045
|
+
if (!active) return;
|
|
5046
|
+
const payload = params;
|
|
5047
|
+
if (typeof payload?.delta !== "string" || !payload.delta) return;
|
|
5048
|
+
const index = payload.contentIndex ?? payload.summaryIndex ?? 0;
|
|
5049
|
+
const key = `${payload.itemId ?? ""}:${method}`;
|
|
5050
|
+
const previous = active.sectionIndex.get(key);
|
|
5051
|
+
active.sectionIndex.set(key, index);
|
|
5052
|
+
const separator = previous !== void 0 && index > previous ? "\n\n" : "";
|
|
5053
|
+
this.#emitDelta({
|
|
5054
|
+
type: "thinking_delta",
|
|
5055
|
+
thinking: separator + payload.delta
|
|
5056
|
+
});
|
|
5057
|
+
};
|
|
5058
|
+
}
|
|
5059
|
+
/** One item-progress handler serves `item/started` and `item/updated`. */
|
|
5060
|
+
#itemProgress = (params) => {
|
|
4158
5061
|
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;
|
|
5062
|
+
if (!active) return;
|
|
5063
|
+
const item = params?.item;
|
|
5064
|
+
if (item) this.#handleItemProgress(item, active);
|
|
5065
|
+
};
|
|
5066
|
+
/** The notification dispatch table — every method the child emits that this
|
|
5067
|
+
* runner maps, in one place. Handlers read `this.#activeTurn` themselves:
|
|
5068
|
+
* dispatch is synchronous, so the read is the same one the old switch made. */
|
|
5069
|
+
#notifications = {
|
|
5070
|
+
"thread/started": (params) => {
|
|
5071
|
+
const thread = params?.thread;
|
|
5072
|
+
if (typeof thread?.id === "string") this.#sdkSessionId = thread.id;
|
|
5073
|
+
},
|
|
5074
|
+
"turn/started": (params) => {
|
|
5075
|
+
const active = this.#activeTurn;
|
|
5076
|
+
const turn = params?.turn;
|
|
5077
|
+
if (active && turn && !active.turnId) active.turnId = turn.id;
|
|
5078
|
+
},
|
|
5079
|
+
"turn/completed": (params) => {
|
|
5080
|
+
const active = this.#activeTurn;
|
|
5081
|
+
const turn = params?.turn;
|
|
5082
|
+
if (active && turn) active.resolve(turn);
|
|
5083
|
+
},
|
|
5084
|
+
"item/started": this.#itemProgress,
|
|
5085
|
+
"item/updated": this.#itemProgress,
|
|
5086
|
+
"item/completed": (params) => {
|
|
5087
|
+
const active = this.#activeTurn;
|
|
5088
|
+
if (!active) return;
|
|
5089
|
+
const item = params?.item;
|
|
5090
|
+
if (item) this.#handleItemCompleted(item, active);
|
|
5091
|
+
},
|
|
5092
|
+
"item/agentMessage/delta": (params) => {
|
|
5093
|
+
if (!this.#activeTurn) return;
|
|
5094
|
+
const delta = params?.delta;
|
|
5095
|
+
if (typeof delta === "string" && delta) this.#emitDelta({
|
|
5096
|
+
type: "text_delta",
|
|
5097
|
+
text: delta
|
|
5098
|
+
});
|
|
5099
|
+
},
|
|
5100
|
+
"item/reasoning/textDelta": this.#reasoningDelta("item/reasoning/textDelta"),
|
|
5101
|
+
"item/reasoning/summaryTextDelta": this.#reasoningDelta("item/reasoning/summaryTextDelta"),
|
|
5102
|
+
"thread/tokenUsage/updated": (params) => {
|
|
5103
|
+
const active = this.#activeTurn;
|
|
5104
|
+
if (!active) return;
|
|
5105
|
+
const last = params?.tokenUsage?.last;
|
|
5106
|
+
if (!last) return;
|
|
5107
|
+
active.sawUsage = true;
|
|
5108
|
+
active.usage.inputTokens += last.inputTokens ?? 0;
|
|
5109
|
+
active.usage.cachedInputTokens += last.cachedInputTokens ?? 0;
|
|
5110
|
+
active.usage.cacheWriteInputTokens = (active.usage.cacheWriteInputTokens ?? 0) + (last.cacheWriteInputTokens ?? 0);
|
|
5111
|
+
active.usage.outputTokens += last.outputTokens ?? 0;
|
|
5112
|
+
active.usage.reasoningOutputTokens += last.reasoningOutputTokens ?? 0;
|
|
5113
|
+
const update = params;
|
|
5114
|
+
active.contextTokens = last.totalTokens ?? void 0;
|
|
5115
|
+
active.contextWindow = update.tokenUsage?.modelContextWindow ?? void 0;
|
|
5116
|
+
},
|
|
5117
|
+
"mcpServer/startupStatus/updated": (params) => {
|
|
5118
|
+
const update = params;
|
|
5119
|
+
if (typeof update?.name !== "string") return;
|
|
5120
|
+
this.#mcpStatus.set(update.name, {
|
|
5121
|
+
status: typeof update.status === "string" ? update.status : "starting",
|
|
5122
|
+
...update.error ? { error: update.error } : {},
|
|
5123
|
+
...update.failureReason ? { failureReason: update.failureReason } : {}
|
|
5124
|
+
});
|
|
5125
|
+
},
|
|
5126
|
+
"skills/changed": () => {
|
|
5127
|
+
const connection = this.#connection;
|
|
5128
|
+
if (connection) this.#refreshSkills(connection);
|
|
5129
|
+
},
|
|
5130
|
+
"account/rateLimits/updated": (params) => {
|
|
5131
|
+
this.#emitRateLimits(params?.rateLimits);
|
|
5132
|
+
},
|
|
5133
|
+
"turn/plan/updated": (params) => {
|
|
5134
|
+
const active = this.#activeTurn;
|
|
5135
|
+
if (!active) return;
|
|
5136
|
+
const plan = params?.plan;
|
|
5137
|
+
if (!Array.isArray(plan)) return;
|
|
5138
|
+
this.#emit({
|
|
5139
|
+
type: "sdk_event",
|
|
5140
|
+
payload: {
|
|
5141
|
+
type: "codex.todo_list",
|
|
5142
|
+
id: `${active.nonce}:plan`,
|
|
5143
|
+
items: plan.map((step) => ({
|
|
5144
|
+
text: step.step,
|
|
5145
|
+
completed: step.status === "completed"
|
|
5146
|
+
}))
|
|
4272
5147
|
}
|
|
5148
|
+
});
|
|
5149
|
+
},
|
|
5150
|
+
"serverRequest/resolved": (params) => {
|
|
5151
|
+
const requestId = params?.requestId;
|
|
5152
|
+
if (requestId === void 0) return;
|
|
5153
|
+
for (const [id, pending] of this.#approvals) if (pending.wireId === requestId) {
|
|
5154
|
+
this.#settleApproval(id, pending, {
|
|
5155
|
+
behavior: "deny",
|
|
5156
|
+
message: "resolved by codex"
|
|
5157
|
+
}, "policy");
|
|
4273
5158
|
return;
|
|
4274
5159
|
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
default: return;
|
|
5160
|
+
},
|
|
5161
|
+
"error": (params) => {
|
|
5162
|
+
const active = this.#activeTurn;
|
|
5163
|
+
const error = params?.error;
|
|
5164
|
+
if (active && typeof error?.message === "string") active.lastError = error.message;
|
|
4281
5165
|
}
|
|
4282
|
-
}
|
|
5166
|
+
};
|
|
4283
5167
|
/** Answer a server→client request: the ask channels become pending
|
|
4284
5168
|
* permission requests; anything else gets a JSON-RPC -32601 rather than a
|
|
4285
5169
|
* hang (an unanswered server request wedges the turn). */
|
|
@@ -4425,83 +5309,89 @@ var CodexRunner = class {
|
|
|
4425
5309
|
}
|
|
4426
5310
|
#handleItemCompleted(item, active) {
|
|
4427
5311
|
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;
|
|
5312
|
+
const handler = this.#itemCompleted[item.type];
|
|
5313
|
+
if (handler) {
|
|
5314
|
+
handler(item, active, id);
|
|
5315
|
+
return;
|
|
5316
|
+
}
|
|
5317
|
+
const unknown = item;
|
|
5318
|
+
this.#emit({
|
|
5319
|
+
type: "sdk_event",
|
|
5320
|
+
payload: {
|
|
5321
|
+
type: `codex.${unknown.type}`,
|
|
5322
|
+
item: unknown
|
|
4476
5323
|
}
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
5324
|
+
});
|
|
5325
|
+
}
|
|
5326
|
+
/**
|
|
5327
|
+
* The completed-item mapping, one handler per member of the {@link AppServerItem}
|
|
5328
|
+
* union. The mapped type is the invariant made checkable: model a new item
|
|
5329
|
+
* type in `types.ts` and this table fails to compile until it says what the
|
|
5330
|
+
* item becomes on the wire — the old switch silently fell through to the
|
|
5331
|
+
* unknown-item passthrough instead. (The runtime still receives types the
|
|
5332
|
+
* union has never heard of; those take the passthrough above.)
|
|
5333
|
+
*/
|
|
5334
|
+
#itemCompleted = {
|
|
5335
|
+
userMessage: () => {},
|
|
5336
|
+
agentMessage: (item, active, id) => {
|
|
5337
|
+
const text = typeof item.text === "string" ? item.text : "";
|
|
5338
|
+
this.#emitAssistant(id, [{
|
|
5339
|
+
type: "text",
|
|
5340
|
+
text
|
|
5341
|
+
}]);
|
|
5342
|
+
active.finalText = text;
|
|
5343
|
+
},
|
|
5344
|
+
reasoning: (item, _active, id) => {
|
|
5345
|
+
const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
|
|
5346
|
+
const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
|
|
5347
|
+
const thinking = (summary.length > 0 ? summary : content).join("\n\n");
|
|
5348
|
+
if (thinking) this.#emitAssistant(id, [{
|
|
5349
|
+
type: "thinking",
|
|
5350
|
+
thinking
|
|
5351
|
+
}]);
|
|
5352
|
+
},
|
|
5353
|
+
commandExecution: (item, active, id) => {
|
|
5354
|
+
if (!active.toolUseEmitted.has(id)) {
|
|
4482
5355
|
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;
|
|
5356
|
+
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
4488
5357
|
}
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
5358
|
+
const exitCode = item.exitCode ?? void 0;
|
|
5359
|
+
const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
|
|
5360
|
+
const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
|
|
5361
|
+
this.#emitToolResult(id, output, failed);
|
|
5362
|
+
},
|
|
5363
|
+
fileChange: (item, _active, id) => {
|
|
5364
|
+
this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
|
|
5365
|
+
const lines = item.changes.map((change) => {
|
|
5366
|
+
return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
|
|
5367
|
+
});
|
|
5368
|
+
const only = item.changes.length === 1 ? item.changes[0] : void 0;
|
|
5369
|
+
this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0);
|
|
5370
|
+
},
|
|
5371
|
+
mcpToolCall: (item, active, id) => {
|
|
5372
|
+
if (!active.toolUseEmitted.has(id)) {
|
|
5373
|
+
active.toolUseEmitted.add(id);
|
|
5374
|
+
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
4502
5375
|
}
|
|
5376
|
+
const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
|
|
5377
|
+
this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
|
|
5378
|
+
},
|
|
5379
|
+
webSearch: (item, _active, id) => {
|
|
5380
|
+
this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
|
|
5381
|
+
this.#emitToolResult(id, "", false);
|
|
5382
|
+
},
|
|
5383
|
+
imageGeneration: (item, active, id) => {
|
|
5384
|
+
active.toolUseEmitted.add(id);
|
|
5385
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
5386
|
+
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
5387
|
+
const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
|
|
5388
|
+
this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
|
|
5389
|
+
},
|
|
5390
|
+
imageView: (item, _active, id) => {
|
|
5391
|
+
this.#emitToolUse(id, "CodexImageView", { path: item.path });
|
|
5392
|
+
this.#emitToolResult(id, item.path, false);
|
|
4503
5393
|
}
|
|
4504
|
-
}
|
|
5394
|
+
};
|
|
4505
5395
|
#emitDelta(delta) {
|
|
4506
5396
|
if (this.#config.includePartialMessages === false) return;
|
|
4507
5397
|
this.#emit({
|
|
@@ -4543,7 +5433,7 @@ var CodexRunner = class {
|
|
|
4543
5433
|
uuid: `${id}-use`
|
|
4544
5434
|
});
|
|
4545
5435
|
}
|
|
4546
|
-
#emitToolResult(toolUseId, content, isError) {
|
|
5436
|
+
#emitToolResult(toolUseId, content, isError, patch) {
|
|
4547
5437
|
this.#emit({
|
|
4548
5438
|
type: "user_message",
|
|
4549
5439
|
message: {
|
|
@@ -4557,6 +5447,7 @@ var CodexRunner = class {
|
|
|
4557
5447
|
},
|
|
4558
5448
|
parentToolUseId: null,
|
|
4559
5449
|
synthetic: true,
|
|
5450
|
+
patch,
|
|
4560
5451
|
uuid: `${toolUseId}-result`
|
|
4561
5452
|
});
|
|
4562
5453
|
}
|
|
@@ -4683,9 +5574,7 @@ var CodexRunner = class {
|
|
|
4683
5574
|
this.#lastActivityAt = event.ts;
|
|
4684
5575
|
this.#activityCount += transcriptActivity(body);
|
|
4685
5576
|
this.#events.push(event);
|
|
4686
|
-
|
|
4687
|
-
listener(event);
|
|
4688
|
-
} catch {}
|
|
5577
|
+
this.#subscribers.emit(event);
|
|
4689
5578
|
}
|
|
4690
5579
|
};
|
|
4691
5580
|
//#endregion
|
|
@@ -5119,6 +6008,6 @@ function getEngineAdapter(engine) {
|
|
|
5119
6008
|
return ADAPTERS[engine ?? "claude"];
|
|
5120
6009
|
}
|
|
5121
6010
|
//#endregion
|
|
5122
|
-
export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withHostTools, withMcpTools };
|
|
6011
|
+
export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, replaySlice, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, truncateResultBlocks, withHostTools, withMcpTools };
|
|
5123
6012
|
|
|
5124
6013
|
//# sourceMappingURL=index.mjs.map
|