@workerdeck/core 0.13.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/README.md +74 -6
- package/build/index.d.mts +200 -38
- package/build/index.mjs +758 -288
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
package/build/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
-
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, transcriptActivity } from "@workerdeck/protocol";
|
|
3
|
+
import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, replayCoalesceKey, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
|
|
5
5
|
import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
7
|
import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -10,7 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import { lookup } from "node:dns/promises";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
//#region src/attachments.ts
|
|
13
|
+
//#region src/lib/attachments.ts
|
|
14
14
|
/** The four the Anthropic API accepts. Notably absent: image/heic — an iPhone's
|
|
15
15
|
* native photo format, which clients must transcode before upload. */
|
|
16
16
|
const IMAGE_TYPES = new Set([
|
|
@@ -103,7 +103,7 @@ function decodeText(base64) {
|
|
|
103
103
|
return Buffer.from(base64, "base64").toString("utf8");
|
|
104
104
|
}
|
|
105
105
|
//#endregion
|
|
106
|
-
//#region src/input-queue.ts
|
|
106
|
+
//#region src/lib/input-queue.ts
|
|
107
107
|
/**
|
|
108
108
|
* Push-based single-consumer AsyncIterable bridging imperative sendMessage() calls
|
|
109
109
|
* into the streaming `prompt` the Agent SDK consumes.
|
|
@@ -162,7 +162,160 @@ var InputQueue = class {
|
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
164
|
//#endregion
|
|
165
|
-
//#region src/
|
|
165
|
+
//#region src/lib/patch.ts
|
|
166
|
+
/**
|
|
167
|
+
* Turning an engine's edit output into the wire's {@link FilePatch}.
|
|
168
|
+
*
|
|
169
|
+
* Both engines know exactly which lines of which file changed, and both say so
|
|
170
|
+
* in their own vocabulary: the Claude SDK hands over a `structuredPatch` array
|
|
171
|
+
* on `SDKUserMessage.tool_use_result`, codex puts a unified diff string on each
|
|
172
|
+
* `fileChange` item. A client can reconstruct neither — it has never seen the
|
|
173
|
+
* file — so anything not normalized here is a diff that renders without line
|
|
174
|
+
* numbers.
|
|
175
|
+
*
|
|
176
|
+
* Normalizing in the runner rather than in each client is the point: one shape
|
|
177
|
+
* reaches the wire, and the dashboard, the extension and the phone all render
|
|
178
|
+
* from it without a per-engine branch or a diff parser of their own.
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* The most lines a patch may put on the wire.
|
|
182
|
+
*
|
|
183
|
+
* A patch is replayed on every attach and captured into parking snapshots, so
|
|
184
|
+
* "the diff is big" must not become "this session is expensive to open forever".
|
|
185
|
+
* Whole hunks are kept or dropped — half a hunk has misleading line numbers —
|
|
186
|
+
* and the drop is flagged so a renderer can say the diff is partial instead of
|
|
187
|
+
* presenting it as the whole change.
|
|
188
|
+
*/
|
|
189
|
+
const MAX_PATCH_LINES = 400;
|
|
190
|
+
function capHunks(hunks) {
|
|
191
|
+
const kept = [];
|
|
192
|
+
let lines = 0;
|
|
193
|
+
for (const hunk of hunks) {
|
|
194
|
+
if (lines + hunk.lines.length > MAX_PATCH_LINES && kept.length > 0) return {
|
|
195
|
+
hunks: kept,
|
|
196
|
+
truncated: true
|
|
197
|
+
};
|
|
198
|
+
kept.push(hunk);
|
|
199
|
+
lines += hunk.lines.length;
|
|
200
|
+
}
|
|
201
|
+
return { hunks: kept };
|
|
202
|
+
}
|
|
203
|
+
/** Structural, not `instanceof`: this reads a field the SDK types as `unknown`,
|
|
204
|
+
* and a shape check is the only honest way to know what arrived. */
|
|
205
|
+
function isHunk(value) {
|
|
206
|
+
const hunk = value;
|
|
207
|
+
return !!hunk && typeof hunk.oldStart === "number" && typeof hunk.oldLines === "number" && typeof hunk.newStart === "number" && typeof hunk.newLines === "number" && Array.isArray(hunk.lines) && hunk.lines.every((line) => typeof line === "string");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A {@link FilePatch} from the Claude SDK's structured tool output
|
|
211
|
+
* (`SDKUserMessage.tool_use_result` for Edit/Write/NotebookEdit).
|
|
212
|
+
*
|
|
213
|
+
* Everything else on that object is deliberately left behind — `originalFile`
|
|
214
|
+
* alone is the entire pre-edit file, which is precisely what must not be logged
|
|
215
|
+
* (see `FilePatch`'s own note).
|
|
216
|
+
*/
|
|
217
|
+
function filePatchFromToolResult(result) {
|
|
218
|
+
const output = result;
|
|
219
|
+
if (!output || !Array.isArray(output.structuredPatch)) return void 0;
|
|
220
|
+
const hunks = output.structuredPatch.filter(isHunk);
|
|
221
|
+
if (hunks.length === 0) return void 0;
|
|
222
|
+
const { hunks: kept, truncated } = capHunks(hunks);
|
|
223
|
+
return {
|
|
224
|
+
...typeof output.filePath === "string" && { path: output.filePath },
|
|
225
|
+
...output.type === "create" || output.originalFile === null ? { kind: "create" } : output.type === "update" || typeof output.originalFile === "string" ? { kind: "update" } : {},
|
|
226
|
+
hunks: kept,
|
|
227
|
+
...truncated && { truncated }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** `@@ -oldStart,oldLines +newStart,newLines @@` — the counts are optional and
|
|
231
|
+
* mean 1 when absent, which is what a single-line hunk looks like. */
|
|
232
|
+
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
233
|
+
/**
|
|
234
|
+
* A {@link FilePatch} from a unified diff — codex's `fileChange.diff`.
|
|
235
|
+
*
|
|
236
|
+
* Only the hunks are read. A diff's `---`/`+++` header names the file, but codex
|
|
237
|
+
* already reports the path on the change itself, and a header path is often
|
|
238
|
+
* relative or `/dev/null`, so the caller's path is the one worth trusting.
|
|
239
|
+
*
|
|
240
|
+
* Returns undefined when there is no hunk header at all: that is not a unified
|
|
241
|
+
* diff, and inventing hunk numbers for it would put wrong line numbers on screen
|
|
242
|
+
* — worse than none.
|
|
243
|
+
*/
|
|
244
|
+
function parseUnifiedDiff(diff, path) {
|
|
245
|
+
const hunks = [];
|
|
246
|
+
let current;
|
|
247
|
+
for (const line of diff.split("\n")) {
|
|
248
|
+
const header = HUNK_HEADER.exec(line);
|
|
249
|
+
if (header) {
|
|
250
|
+
current = {
|
|
251
|
+
oldStart: Number(header[1]),
|
|
252
|
+
oldLines: header[2] === void 0 ? 1 : Number(header[2]),
|
|
253
|
+
newStart: Number(header[3]),
|
|
254
|
+
newLines: header[4] === void 0 ? 1 : Number(header[4]),
|
|
255
|
+
lines: []
|
|
256
|
+
};
|
|
257
|
+
hunks.push(current);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!current) continue;
|
|
261
|
+
if (line.startsWith(" ") || line.startsWith("-") || line.startsWith("+")) current.lines.push(line);
|
|
262
|
+
else if (line === "") current.lines.push(" ");
|
|
263
|
+
else current = void 0;
|
|
264
|
+
}
|
|
265
|
+
if (hunks.length === 0) return void 0;
|
|
266
|
+
const { hunks: kept, truncated } = capHunks(hunks);
|
|
267
|
+
return {
|
|
268
|
+
...path && { path },
|
|
269
|
+
hunks: kept,
|
|
270
|
+
...truncated && { truncated }
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/lib/normalize.ts
|
|
275
|
+
/** Does this message answer exactly one tool call? A patch is per-file-edit and
|
|
276
|
+
* the message says nothing about which of two results it describes, so anything
|
|
277
|
+
* else gets no patch rather than a diff pinned to the wrong call. */
|
|
278
|
+
function singleToolResult(message) {
|
|
279
|
+
const content = message.content;
|
|
280
|
+
if (!Array.isArray(content)) return false;
|
|
281
|
+
return content.filter((block) => block.type === "tool_result").length === 1;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The wrappers the CLI writes into the transcript when the *harness* is talking
|
|
285
|
+
* to the model rather than a person talking to the session.
|
|
286
|
+
*
|
|
287
|
+
* Deliberately a text test, and only these two. The live path has structure to
|
|
288
|
+
* go on (`isSynthetic`, `origin.kind`), but **the resumed path has none**: the
|
|
289
|
+
* SDK's `SessionMessage` carries exactly `message`, `uuid`, `session_id`,
|
|
290
|
+
* `parent_tool_use_id`, `parent_agent_id` and `timestamp` — every one of
|
|
291
|
+
* `isMeta`, `isSidechain`, `promptSource` and `origin` is dropped between the
|
|
292
|
+
* stored JSONL and what `getSessionMessages` hands back (verified against real
|
|
293
|
+
* transcripts). So on resume this is the only signal there is, and without it a
|
|
294
|
+
* `<task-notification>` blob comes back as a blue user row and a scrubber mark,
|
|
295
|
+
* as if someone had typed it.
|
|
296
|
+
*
|
|
297
|
+
* `<local-command-caveat>` is here for symmetry and cheap insurance: the SDK
|
|
298
|
+
* filters `isMeta` entries out of a resumed transcript itself today, which is
|
|
299
|
+
* not a contract anyone wrote down.
|
|
300
|
+
*
|
|
301
|
+
* What is *not* here matters as much:
|
|
302
|
+
* - `<local-command-stdout>` — the reducer turns it into a notice row on
|
|
303
|
+
* purpose; marking it synthetic would delete a row both paths show.
|
|
304
|
+
* - `<command-name>` — that is a person running a slash command. The reducer
|
|
305
|
+
* renders it as the command line they typed; hiding it would erase the turn's
|
|
306
|
+
* cause.
|
|
307
|
+
*/
|
|
308
|
+
const SYNTHETIC_USER_PREFIXES = ["<task-notification>", "<local-command-caveat>"];
|
|
309
|
+
/** First text block's leading tag, for the test above. Tool results and images
|
|
310
|
+
* carry no text and are never synthetic by this rule (a tool result is already
|
|
311
|
+
* a tool result to every renderer). */
|
|
312
|
+
function isSyntheticUserText(message) {
|
|
313
|
+
const content = message.content;
|
|
314
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((block) => block.type === "text")?.text : void 0;
|
|
315
|
+
if (typeof text !== "string") return false;
|
|
316
|
+
const head = text.trimStart();
|
|
317
|
+
return SYNTHETIC_USER_PREFIXES.some((prefix) => head.startsWith(prefix));
|
|
318
|
+
}
|
|
166
319
|
function toApiMessage(message) {
|
|
167
320
|
const m = message;
|
|
168
321
|
return {
|
|
@@ -351,14 +504,18 @@ function normalizeSdkMessage(msg) {
|
|
|
351
504
|
parentToolUseId: msg.parent_tool_use_id,
|
|
352
505
|
uuid: msg.uuid
|
|
353
506
|
};
|
|
354
|
-
case "user":
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
507
|
+
case "user": {
|
|
508
|
+
const message = toApiMessage(msg.message);
|
|
509
|
+
return {
|
|
510
|
+
type: "user_message",
|
|
511
|
+
message,
|
|
512
|
+
parentToolUseId: msg.parent_tool_use_id,
|
|
513
|
+
replay: "isReplay" in msg && msg.isReplay === true ? true : void 0,
|
|
514
|
+
synthetic: msg.isSynthetic === true || msg.origin?.kind === "task-notification" || isSyntheticUserText(message) ? true : void 0,
|
|
515
|
+
patch: singleToolResult(message) ? filePatchFromToolResult(msg.tool_use_result) : void 0,
|
|
516
|
+
uuid: msg.uuid
|
|
517
|
+
};
|
|
518
|
+
}
|
|
362
519
|
case "stream_event": return {
|
|
363
520
|
type: "stream_delta",
|
|
364
521
|
event: msg.event,
|
|
@@ -376,6 +533,10 @@ function normalizeSdkMessage(msg) {
|
|
|
376
533
|
errors: msg.subtype === "success" ? void 0 : msg.errors,
|
|
377
534
|
usage: msg.usage
|
|
378
535
|
};
|
|
536
|
+
case "conversation_reset": return {
|
|
537
|
+
type: "conversation_reset",
|
|
538
|
+
sdkSessionId: msg.new_conversation_id
|
|
539
|
+
};
|
|
379
540
|
case "rate_limit_event": return {
|
|
380
541
|
type: "rate_limit",
|
|
381
542
|
info: {
|
|
@@ -399,7 +560,41 @@ function normalizeSdkMessage(msg) {
|
|
|
399
560
|
}
|
|
400
561
|
}
|
|
401
562
|
//#endregion
|
|
402
|
-
//#region src/
|
|
563
|
+
//#region src/lib/replay.ts
|
|
564
|
+
/**
|
|
565
|
+
* Which buffered events a coalesced replay should skip: everything superseded
|
|
566
|
+
* by a later event with the same {@link replayCoalesceKey}.
|
|
567
|
+
*
|
|
568
|
+
* A **backwards** scan, keeping the first occurrence of each key — which is the
|
|
569
|
+
* whole trick. Walking forwards would need a second pass to know which of the
|
|
570
|
+
* fifty context readings was the last one; walking backwards, the first one you
|
|
571
|
+
* meet *is* the last one, and everything after it (in scan order) is history.
|
|
572
|
+
*
|
|
573
|
+
* Note what this does **not** do: it never reorders and never touches an event
|
|
574
|
+
* with no key. Transcript content is an ordered fold — a stream delta
|
|
575
|
+
* accumulates onto a message, a tool result attaches to a call that came
|
|
576
|
+
* earlier, a turn result finalizes — so it must arrive exactly as it was
|
|
577
|
+
* emitted. Only last-write-wins *state* is eligible, and `replayCoalesceKey`
|
|
578
|
+
* is where that judgement lives.
|
|
579
|
+
*
|
|
580
|
+
* `afterSeq` is honoured so the scan agrees with the caller's replay window: an
|
|
581
|
+
* event the caller was never going to send must not suppress one it was.
|
|
582
|
+
*/
|
|
583
|
+
function staleReplaySeqs(events, afterSeq) {
|
|
584
|
+
const stale = /* @__PURE__ */ new Set();
|
|
585
|
+
const seen = /* @__PURE__ */ new Set();
|
|
586
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
587
|
+
const event = events[index];
|
|
588
|
+
if (event.seq <= afterSeq) break;
|
|
589
|
+
const key = replayCoalesceKey(event);
|
|
590
|
+
if (key === void 0) continue;
|
|
591
|
+
if (seen.has(key)) stale.add(event.seq);
|
|
592
|
+
else seen.add(key);
|
|
593
|
+
}
|
|
594
|
+
return stale;
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region src/engines/claude/runner.ts
|
|
403
598
|
const DEFAULT_APPROVAL_TIMEOUT_MS$1 = 3e5;
|
|
404
599
|
/**
|
|
405
600
|
* One live Agent SDK session: owns the query() call, the streaming input queue, the
|
|
@@ -410,10 +605,21 @@ var SessionRunner = class {
|
|
|
410
605
|
id;
|
|
411
606
|
createdAt;
|
|
412
607
|
#config;
|
|
608
|
+
/** {@link SessionRunnerConfig.cwd}, checked once in the constructor. */
|
|
609
|
+
#cwd;
|
|
413
610
|
#events = [];
|
|
414
611
|
#listeners = /* @__PURE__ */ new Set();
|
|
415
612
|
#seq = 0;
|
|
416
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;
|
|
417
623
|
#status = "starting";
|
|
418
624
|
#statusDetail;
|
|
419
625
|
#sdkSessionId;
|
|
@@ -430,10 +636,15 @@ var SessionRunner = class {
|
|
|
430
636
|
/** Last plan reported by the usage poll, so `plan_info` is emitted on change
|
|
431
637
|
* rather than once per turn. */
|
|
432
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;
|
|
433
642
|
#started = false;
|
|
434
643
|
#closed = false;
|
|
435
644
|
#runPromise;
|
|
436
645
|
constructor(config, id = randomUUID()) {
|
|
646
|
+
if (!config.cwd) throw new Error("the claude engine requires a cwd");
|
|
647
|
+
this.#cwd = config.cwd;
|
|
437
648
|
this.#config = config;
|
|
438
649
|
this.#permissionMode = config.permissionMode;
|
|
439
650
|
this.id = id;
|
|
@@ -460,7 +671,7 @@ var SessionRunner = class {
|
|
|
460
671
|
id: this.id,
|
|
461
672
|
sdkSessionId: this.#sdkSessionId,
|
|
462
673
|
status: this.#status,
|
|
463
|
-
cwd: this.#
|
|
674
|
+
cwd: this.#cwd,
|
|
464
675
|
profile: this.#config.profile,
|
|
465
676
|
engine: "claude",
|
|
466
677
|
capabilities: ENGINE_CAPABILITIES.claude,
|
|
@@ -473,15 +684,27 @@ var SessionRunner = class {
|
|
|
473
684
|
activityCount: this.#activityCount,
|
|
474
685
|
pendingPermissionCount: this.#pending.size,
|
|
475
686
|
meta: this.#config.meta,
|
|
687
|
+
scope: this.#config.scope,
|
|
476
688
|
title: this.#title(),
|
|
477
689
|
totalCostUsd: this.#totalCostUsd,
|
|
478
690
|
numTurns: this.#numTurns,
|
|
479
691
|
lastActivityAt: this.#lastActivityAt
|
|
480
692
|
};
|
|
481
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
|
+
*/
|
|
482
704
|
#title() {
|
|
483
705
|
const metaTitle = this.#config.meta?.title;
|
|
484
706
|
if (typeof metaTitle === "string" && metaTitle.length > 0) return metaTitle;
|
|
707
|
+
if (this.#engineTitle) return this.#engineTitle;
|
|
485
708
|
const prompt = this.#config.prompt;
|
|
486
709
|
if (!prompt) return void 0;
|
|
487
710
|
return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
|
|
@@ -611,9 +834,24 @@ var SessionRunner = class {
|
|
|
611
834
|
/**
|
|
612
835
|
* Replay buffered events with seq > afterSeq, then deliver live events.
|
|
613
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.
|
|
614
846
|
*/
|
|
615
|
-
subscribe(listener, afterSeq = 0) {
|
|
616
|
-
|
|
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
|
+
}
|
|
617
855
|
this.#listeners.add(listener);
|
|
618
856
|
return () => this.#listeners.delete(listener);
|
|
619
857
|
}
|
|
@@ -665,20 +903,23 @@ var SessionRunner = class {
|
|
|
665
903
|
const historyFn = c.historyFn ?? ((sessionId, options) => getSessionMessages(sessionId, options));
|
|
666
904
|
let messages;
|
|
667
905
|
try {
|
|
668
|
-
messages = await historyFn(c.resume, { dir:
|
|
906
|
+
messages = await historyFn(c.resume, { dir: this.#cwd });
|
|
669
907
|
} catch {
|
|
670
908
|
return;
|
|
671
909
|
}
|
|
672
910
|
for (const m of messages) {
|
|
673
911
|
if (this.#closed) return;
|
|
674
|
-
if (m.type === "user")
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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({
|
|
682
923
|
type: "assistant_message",
|
|
683
924
|
message: toApiMessage(m.message),
|
|
684
925
|
parentToolUseId: m.parent_tool_use_id,
|
|
@@ -690,7 +931,7 @@ var SessionRunner = class {
|
|
|
690
931
|
#buildOptions() {
|
|
691
932
|
const c = this.#config;
|
|
692
933
|
return {
|
|
693
|
-
cwd:
|
|
934
|
+
cwd: this.#cwd,
|
|
694
935
|
permissionMode: c.permissionMode,
|
|
695
936
|
allowedTools: c.allowedTools,
|
|
696
937
|
disallowedTools: c.disallowedTools,
|
|
@@ -733,6 +974,7 @@ var SessionRunner = class {
|
|
|
733
974
|
this.#fetchCapabilities();
|
|
734
975
|
this.#fetchContextUsage();
|
|
735
976
|
this.#fetchRateLimits();
|
|
977
|
+
this.#fetchEngineTitle();
|
|
736
978
|
return;
|
|
737
979
|
}
|
|
738
980
|
if (msg.type === "system" && msg.subtype === "session_state_changed") {
|
|
@@ -744,12 +986,17 @@ var SessionRunner = class {
|
|
|
744
986
|
const body = normalizeSdkMessage(msg);
|
|
745
987
|
if (body) {
|
|
746
988
|
this.#emit(body);
|
|
989
|
+
if (body.type === "conversation_reset") {
|
|
990
|
+
if (body.sdkSessionId) this.#sdkSessionId = body.sdkSessionId;
|
|
991
|
+
this.#fetchContextUsage();
|
|
992
|
+
}
|
|
747
993
|
if (body.type === "turn_result") {
|
|
748
994
|
this.#totalCostUsd = body.totalCostUsd;
|
|
749
995
|
this.#numTurns = body.numTurns;
|
|
750
996
|
if (this.#pending.size === 0) this.#setStatus("idle");
|
|
751
997
|
this.#fetchContextUsage();
|
|
752
998
|
this.#fetchRateLimits();
|
|
999
|
+
this.#fetchEngineTitle();
|
|
753
1000
|
}
|
|
754
1001
|
}
|
|
755
1002
|
}
|
|
@@ -779,6 +1026,44 @@ var SessionRunner = class {
|
|
|
779
1026
|
});
|
|
780
1027
|
} catch {}
|
|
781
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
|
+
}
|
|
782
1067
|
/** Snapshot the context window after a turn and surface it as an event. Optional-chained
|
|
783
1068
|
* and best-effort for the same reasons as #fetchCapabilities. */
|
|
784
1069
|
async #fetchContextUsage() {
|
|
@@ -960,6 +1245,7 @@ var SessionRunner = class {
|
|
|
960
1245
|
};
|
|
961
1246
|
this.#lastActivityAt = event.ts;
|
|
962
1247
|
this.#activityCount += transcriptActivity(body);
|
|
1248
|
+
if (body.type === "conversation_reset") this.#resetSeq = event.seq;
|
|
963
1249
|
this.#events.push(event);
|
|
964
1250
|
for (const listener of this.#listeners) try {
|
|
965
1251
|
listener(event);
|
|
@@ -981,7 +1267,7 @@ function recommendedAnswers(input) {
|
|
|
981
1267
|
return answers;
|
|
982
1268
|
}
|
|
983
1269
|
//#endregion
|
|
984
|
-
//#region src/
|
|
1270
|
+
//#region src/engines/provider/runner.ts
|
|
985
1271
|
/** Permission modes this engine can honor. The rest of the protocol vocabulary
|
|
986
1272
|
* (acceptEdits/plan/auto) is Claude Code CLI semantics with no meaning here —
|
|
987
1273
|
* setPermissionMode rejects them, which the server surfaces as protocol_error. */
|
|
@@ -1099,7 +1385,7 @@ var AiSdkRunner = class {
|
|
|
1099
1385
|
return {
|
|
1100
1386
|
id: this.id,
|
|
1101
1387
|
status: this.#status,
|
|
1102
|
-
cwd: this.#config.cwd ??
|
|
1388
|
+
cwd: this.#config.cwd ?? "",
|
|
1103
1389
|
profile: this.#config.profile,
|
|
1104
1390
|
engine: "provider",
|
|
1105
1391
|
capabilities: ENGINE_CAPABILITIES.provider,
|
|
@@ -1110,6 +1396,7 @@ var AiSdkRunner = class {
|
|
|
1110
1396
|
activityCount: this.#activityCount,
|
|
1111
1397
|
pendingPermissionCount: 0,
|
|
1112
1398
|
meta: this.#config.meta,
|
|
1399
|
+
scope: this.#config.scope,
|
|
1113
1400
|
title: this.#title(),
|
|
1114
1401
|
numTurns: this.#numTurns || void 0,
|
|
1115
1402
|
lastActivityAt: this.#lastActivityAt
|
|
@@ -1351,8 +1638,13 @@ var AiSdkRunner = class {
|
|
|
1351
1638
|
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1352
1639
|
} catch {}
|
|
1353
1640
|
}
|
|
1354
|
-
subscribe(listener, afterSeq = 0) {
|
|
1355
|
-
|
|
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
|
+
}
|
|
1356
1648
|
this.#listeners.add(listener);
|
|
1357
1649
|
return () => this.#listeners.delete(listener);
|
|
1358
1650
|
}
|
|
@@ -1483,29 +1775,29 @@ var AiSdkRunner = class {
|
|
|
1483
1775
|
cacheWrite: 0,
|
|
1484
1776
|
cacheRead: 0
|
|
1485
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
|
+
};
|
|
1486
1795
|
try {
|
|
1487
1796
|
const result = await agent.stream({
|
|
1488
1797
|
messages: [...this.#messages],
|
|
1489
1798
|
abortSignal: abort.signal
|
|
1490
1799
|
});
|
|
1491
1800
|
const partials = this.#config.includePartialMessages !== false;
|
|
1492
|
-
let blocks = [];
|
|
1493
|
-
const textBuf = /* @__PURE__ */ new Map();
|
|
1494
|
-
const reasoningBuf = /* @__PURE__ */ new Map();
|
|
1495
|
-
const flush = () => {
|
|
1496
|
-
if (blocks.length === 0) return;
|
|
1497
|
-
this.#emit({
|
|
1498
|
-
type: "assistant_message",
|
|
1499
|
-
message: {
|
|
1500
|
-
role: "assistant",
|
|
1501
|
-
content: blocks,
|
|
1502
|
-
model: this.#modelId()
|
|
1503
|
-
},
|
|
1504
|
-
parentToolUseId: null,
|
|
1505
|
-
uuid: randomUUID()
|
|
1506
|
-
});
|
|
1507
|
-
blocks = [];
|
|
1508
|
-
};
|
|
1509
1801
|
const emitToolResult = (toolCallId, content, isError) => {
|
|
1510
1802
|
flush();
|
|
1511
1803
|
this.#emit({
|
|
@@ -1635,6 +1927,15 @@ var AiSdkRunner = class {
|
|
|
1635
1927
|
this.#finishTurn(text);
|
|
1636
1928
|
} catch (error) {
|
|
1637
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();
|
|
1638
1939
|
const message = error instanceof Error ? error.message : String(error);
|
|
1639
1940
|
this.#numTurns += 1;
|
|
1640
1941
|
this.#emit({
|
|
@@ -1694,6 +1995,17 @@ var AiSdkRunner = class {
|
|
|
1694
1995
|
if (!prompt) return void 0;
|
|
1695
1996
|
return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
|
|
1696
1997
|
}
|
|
1998
|
+
/**
|
|
1999
|
+
* This session's MCP servers, as the host assembled them.
|
|
2000
|
+
*
|
|
2001
|
+
* Always answers — an empty list when no MCP was wired — because the
|
|
2002
|
+
* alternative (undefined, which the server turns into a 501) says "this
|
|
2003
|
+
* engine cannot tell you", and this engine can: the host that built the
|
|
2004
|
+
* session is the only party who knows, and it has been asked.
|
|
2005
|
+
*/
|
|
2006
|
+
async mcpServers() {
|
|
2007
|
+
return await this.#config.reportMcpServers?.() ?? [];
|
|
2008
|
+
}
|
|
1697
2009
|
/** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
|
|
1698
2010
|
* it (undefined) restores the derived title. The engine is never told. */
|
|
1699
2011
|
setTitle(title) {
|
|
@@ -1744,7 +2056,7 @@ function errorText(error) {
|
|
|
1744
2056
|
return error instanceof Error ? error.message : String(error);
|
|
1745
2057
|
}
|
|
1746
2058
|
//#endregion
|
|
1747
|
-
//#region src/claude
|
|
2059
|
+
//#region src/engines/claude/auth.ts
|
|
1748
2060
|
/**
|
|
1749
2061
|
* The native Claude Code binary the Agent SDK itself spawns, resolved the way
|
|
1750
2062
|
* the SDK resolves it: the platform-specific optional dependency installed next
|
|
@@ -1798,7 +2110,7 @@ function checkClaudeAuth(env, options = {}) {
|
|
|
1798
2110
|
});
|
|
1799
2111
|
}
|
|
1800
2112
|
//#endregion
|
|
1801
|
-
//#region src/quickjs-executor.ts
|
|
2113
|
+
//#region src/executors/quickjs-executor.ts
|
|
1802
2114
|
/**
|
|
1803
2115
|
* In-process execution backend: runs a tool's untrusted script in the QuickJS
|
|
1804
2116
|
* WASM guest. Always settles inline — nothing downstream assumes that, which is
|
|
@@ -1896,7 +2208,7 @@ function isHostAllowed(url, allowedHosts) {
|
|
|
1896
2208
|
});
|
|
1897
2209
|
}
|
|
1898
2210
|
//#endregion
|
|
1899
|
-
//#region src/pending-registry.ts
|
|
2211
|
+
//#region src/lib/pending-registry.ts
|
|
1900
2212
|
var PendingRequestRegistry = class {
|
|
1901
2213
|
#slots = /* @__PURE__ */ new Map();
|
|
1902
2214
|
get size() {
|
|
@@ -2003,7 +2315,7 @@ function toEntry(slot) {
|
|
|
2003
2315
|
};
|
|
2004
2316
|
}
|
|
2005
2317
|
//#endregion
|
|
2006
|
-
//#region src/browser-bridge-executor.ts
|
|
2318
|
+
//#region src/executors/browser-bridge-executor.ts
|
|
2007
2319
|
/**
|
|
2008
2320
|
* Executes tool calls in the attached client's own sandbox. The first backend
|
|
2009
2321
|
* that genuinely returns `pending`: dispatch puts a request on the wire and
|
|
@@ -2114,7 +2426,7 @@ function toExecutionResult(outcome) {
|
|
|
2114
2426
|
};
|
|
2115
2427
|
}
|
|
2116
2428
|
//#endregion
|
|
2117
|
-
//#region src/deferred-executor.ts
|
|
2429
|
+
//#region src/executors/deferred-executor.ts
|
|
2118
2430
|
/**
|
|
2119
2431
|
* The executor for work that outlives the session's process residency: dispatch
|
|
2120
2432
|
* hands the call off and returns `pending` **without holding a promise**, because
|
|
@@ -2161,7 +2473,7 @@ var DeferredExecutor = class {
|
|
|
2161
2473
|
}
|
|
2162
2474
|
};
|
|
2163
2475
|
//#endregion
|
|
2164
|
-
//#region src/tools.ts
|
|
2476
|
+
//#region src/engines/provider/tools.ts
|
|
2165
2477
|
const MAX_FILE_BYTES = 1024 * 1024;
|
|
2166
2478
|
/**
|
|
2167
2479
|
* Build the capability-scoped tool set for a session.
|
|
@@ -2327,28 +2639,57 @@ function createToolContext(options) {
|
|
|
2327
2639
|
/** Add host-side MCP tools to a context. They are ALWAYS authoritative: they run
|
|
2328
2640
|
* server-side with server credentials, and must never be handed to a browser. */
|
|
2329
2641
|
function withMcpTools(context, mcpTools) {
|
|
2642
|
+
return withHostTools(context, Object.fromEntries(Object.entries(mcpTools).map(([name, mcpTool]) => [name, {
|
|
2643
|
+
tool: mcpTool,
|
|
2644
|
+
trust: "authoritative"
|
|
2645
|
+
}])), "MCP tool");
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Add host-supplied tools to a context at an explicit trust level.
|
|
2649
|
+
*
|
|
2650
|
+
* The trust level is the whole point of the seam: {@link withMcpTools} can only
|
|
2651
|
+
* produce authoritative tools, so a host tool that *should* be sandboxed — and
|
|
2652
|
+
* therefore executable in the browser tab that asked for it — had no way to be
|
|
2653
|
+
* expressed at all. Here the host says which it is, and the contradictions are
|
|
2654
|
+
* refused rather than silently resolved:
|
|
2655
|
+
*
|
|
2656
|
+
* - a `sandboxed` tool carrying `execute` would run inline in this process with
|
|
2657
|
+
* the gateway's ambient authority, which is exactly what sandboxing it was
|
|
2658
|
+
* meant to prevent;
|
|
2659
|
+
* - an `authoritative` tool *without* `execute` would park the turn on a call no
|
|
2660
|
+
* executor claims, and the session would simply stop.
|
|
2661
|
+
*/
|
|
2662
|
+
function withHostTools(context, hostTools, kind = "host tool") {
|
|
2663
|
+
const entries = Object.entries(hostTools);
|
|
2664
|
+
if (entries.length === 0) return context;
|
|
2330
2665
|
const definitions = [...context.definitions];
|
|
2331
2666
|
const tools = { ...context.tools };
|
|
2332
|
-
|
|
2333
|
-
|
|
2667
|
+
const sandboxedToolNames = [...context.sandboxedToolNames];
|
|
2668
|
+
for (const [name, { tool: hostTool, trust }] of entries) {
|
|
2669
|
+
if (name in tools) throw new Error(`${kind} '${name}' collides with an existing tool of the same name`);
|
|
2670
|
+
const executes = typeof hostTool.execute === "function";
|
|
2671
|
+
if (trust === "sandboxed" && executes) throw new Error(`${kind} '${name}' is declared sandboxed but has an \`execute\` — it would run in this process with full authority. Drop \`execute\` so it rides the ToolExecutor seam.`);
|
|
2672
|
+
if (trust === "authoritative" && !executes) throw new Error(`${kind} '${name}' is declared authoritative but has no \`execute\` — nothing would ever answer its calls and the turn would stall.`);
|
|
2334
2673
|
definitions.push({
|
|
2335
2674
|
name,
|
|
2336
|
-
trust
|
|
2337
|
-
tool:
|
|
2675
|
+
trust,
|
|
2676
|
+
tool: hostTool
|
|
2338
2677
|
});
|
|
2339
|
-
tools[name] =
|
|
2678
|
+
tools[name] = hostTool;
|
|
2679
|
+
if (trust === "sandboxed") sandboxedToolNames.push(name);
|
|
2340
2680
|
}
|
|
2341
2681
|
return {
|
|
2342
2682
|
...context,
|
|
2343
2683
|
tools,
|
|
2344
|
-
definitions
|
|
2684
|
+
definitions,
|
|
2685
|
+
sandboxedToolNames
|
|
2345
2686
|
};
|
|
2346
2687
|
}
|
|
2347
2688
|
function truncate(text) {
|
|
2348
2689
|
return text.length > MAX_FILE_BYTES ? text.slice(0, MAX_FILE_BYTES) : text;
|
|
2349
2690
|
}
|
|
2350
2691
|
//#endregion
|
|
2351
|
-
//#region src/web-fetch.ts
|
|
2692
|
+
//#region src/engines/provider/web-fetch.ts
|
|
2352
2693
|
const MAX_CACHE_ENTRIES = 64;
|
|
2353
2694
|
const MAX_REDIRECTS = 5;
|
|
2354
2695
|
function createWebFetch(options = {}) {
|
|
@@ -2560,7 +2901,7 @@ function decodeEntities(text) {
|
|
|
2560
2901
|
return text.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).replace(/&#x([\da-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16))).replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'|'/g, "'").replace(/&/g, "&");
|
|
2561
2902
|
}
|
|
2562
2903
|
//#endregion
|
|
2563
|
-
//#region src/
|
|
2904
|
+
//#region src/engines/provider/session.ts
|
|
2564
2905
|
/** Which capability a wired backend yields, for grant filtering. */
|
|
2565
2906
|
const CAPABILITY_TOOLS = {
|
|
2566
2907
|
search: "web_search",
|
|
@@ -2581,7 +2922,7 @@ const CAPABILITY_TOOLS = {
|
|
|
2581
2922
|
* the host wired, which is what a host that ignores profiles gets.
|
|
2582
2923
|
*/
|
|
2583
2924
|
function createEngineSession(options) {
|
|
2584
|
-
const vfs = options.config.vfs ?? createVfs(options.config.restore
|
|
2925
|
+
const vfs = options.config.vfs ?? createVfs(options.config.restore ? options.config.restore.vfs : options.seedVfs);
|
|
2585
2926
|
const executor = options.selectExecutor();
|
|
2586
2927
|
const granted = options.config.capabilities ?? options.profile?.session?.capabilities;
|
|
2587
2928
|
const isGranted = (key) => granted === void 0 || granted.includes(CAPABILITY_TOOLS[key]);
|
|
@@ -2602,8 +2943,12 @@ function createEngineSession(options) {
|
|
|
2602
2943
|
webFetch,
|
|
2603
2944
|
onFileDelivered: options.capabilities?.deliverFiles === false || !isGranted("deliverFiles") ? void 0 : (file) => runner?.emitFileDelivered(file)
|
|
2604
2945
|
});
|
|
2605
|
-
const
|
|
2606
|
-
const
|
|
2946
|
+
const declaredServers = options.profile?.session?.mcpServers;
|
|
2947
|
+
const connected = options.mcp?.tools ?? options.mcpTools;
|
|
2948
|
+
requireDeclaredServers(options.profile?.name ?? "(unnamed)", declaredServers, options.mcp, connected);
|
|
2949
|
+
const mcpTools = selectMcpTools(connected, declaredServers);
|
|
2950
|
+
const withMcp = mcpTools ? withMcpTools(base, mcpTools) : base;
|
|
2951
|
+
const context = options.tools ? withHostTools(withMcp, options.tools) : withMcp;
|
|
2607
2952
|
runner = new AiSdkRunner({
|
|
2608
2953
|
...options.config,
|
|
2609
2954
|
languageModel: options.resolveModel(options.profile, options.config),
|
|
@@ -2613,11 +2958,42 @@ function createEngineSession(options) {
|
|
|
2613
2958
|
executor,
|
|
2614
2959
|
executableTools: context.sandboxedToolNames,
|
|
2615
2960
|
executionBackend: options.backend ?? "server",
|
|
2616
|
-
executionLimits: options.executionLimits
|
|
2617
|
-
|
|
2961
|
+
executionLimits: options.executionLimits,
|
|
2962
|
+
reportMcpServers: options.mcp ? () => Promise.resolve(declaredServers === void 0 ? options.mcp.servers : options.mcp.servers.filter((s) => declaredServers.includes(s.name))) : void 0
|
|
2963
|
+
}, options.id);
|
|
2618
2964
|
return runner;
|
|
2619
2965
|
}
|
|
2620
2966
|
/**
|
|
2967
|
+
* Refuse to build a session whose profile names an MCP server that isn't there.
|
|
2968
|
+
*
|
|
2969
|
+
* A profile's `mcpServers` list is a **declaration**, not a filter: an embedder
|
|
2970
|
+
* who wrote it meant the agent to have those tools. Honouring it partially is
|
|
2971
|
+
* the worst failure mode this engine has — the session starts, reports healthy,
|
|
2972
|
+
* and the agent apologises its way through every request that needed the server,
|
|
2973
|
+
* with one warning line in a log nobody is reading.
|
|
2974
|
+
*
|
|
2975
|
+
* With a {@link McpConnection} the check is exact (did this server connect?).
|
|
2976
|
+
* With a bare tool set all we can see is whether any tool carries the server's
|
|
2977
|
+
* namespace, so a genuinely tool-less server would trip it — the fix there is to
|
|
2978
|
+
* pass `mcp` rather than to weaken this.
|
|
2979
|
+
*/
|
|
2980
|
+
function requireDeclaredServers(profileName, declared, mcp, tools) {
|
|
2981
|
+
if (!declared || declared.length === 0) return;
|
|
2982
|
+
const missing = declared.filter((name) => {
|
|
2983
|
+
if (mcp) {
|
|
2984
|
+
const server = mcp.servers.find((s) => s.name === name);
|
|
2985
|
+
return !server || server.status !== "connected";
|
|
2986
|
+
}
|
|
2987
|
+
return !Object.keys(tools ?? {}).some((tool) => tool.split("__")[0] === name);
|
|
2988
|
+
});
|
|
2989
|
+
if (missing.length === 0) return;
|
|
2990
|
+
const reasons = missing.map((name) => {
|
|
2991
|
+
const error = mcp?.servers.find((s) => s.name === name)?.error;
|
|
2992
|
+
return error ? `${name} (${error})` : name;
|
|
2993
|
+
}).join(", ");
|
|
2994
|
+
throw new Error(`profile '${profileName}' declares MCP server(s) that are not connected: ${reasons}. A session missing a declared server is a session whose agent silently cannot do its job.`);
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2621
2997
|
* Restrict a connected tool set to the MCP servers a profile grants, by the
|
|
2622
2998
|
* `<server>__<tool>` namespace {@link connectMcpTools} assigns. Undefined `servers`
|
|
2623
2999
|
* = no declaration, so every connected server passes through.
|
|
@@ -2637,31 +3013,88 @@ function selectMcpTools(tools, servers) {
|
|
|
2637
3013
|
* Server-side only, with server credentials: these tools are authoritative and
|
|
2638
3014
|
* must never be bridged to a browser. `@ai-sdk/mcp` is imported lazily and is an
|
|
2639
3015
|
* optional dependency — an operator who wires no MCP servers never needs it.
|
|
3016
|
+
*
|
|
3017
|
+
* **A stateless MCP server must answer `GET` with 405.** The client opens the
|
|
3018
|
+
* SSE stream with a `GET` before it sends anything, and a POST-only server
|
|
3019
|
+
* mounted under a framework's default 404 makes the whole connect fail with an
|
|
3020
|
+
* error that names neither the method nor the route. This is the single most
|
|
3021
|
+
* common way an otherwise-correct MCP mount fails.
|
|
2640
3022
|
*/
|
|
2641
3023
|
async function connectMcpTools(servers, options = {}) {
|
|
2642
3024
|
const entries = Object.entries(servers);
|
|
2643
3025
|
if (entries.length === 0) return {
|
|
2644
3026
|
tools: {},
|
|
3027
|
+
servers: [],
|
|
2645
3028
|
close: async () => {}
|
|
2646
3029
|
};
|
|
2647
3030
|
const { createMCPClient } = await import("@ai-sdk/mcp");
|
|
2648
3031
|
const clients = [];
|
|
2649
3032
|
const tools = {};
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
3033
|
+
const statuses = [];
|
|
3034
|
+
const closeAll = async () => {
|
|
3035
|
+
await Promise.allSettled(clients.map((c) => c.close()));
|
|
3036
|
+
};
|
|
3037
|
+
for (const [name, server] of entries) {
|
|
3038
|
+
const identity = describeServer(server);
|
|
3039
|
+
try {
|
|
3040
|
+
const client = await createMCPClient({
|
|
3041
|
+
transport: toTransport(server),
|
|
3042
|
+
onUncaughtError: (error) => options.onError?.(name, error)
|
|
3043
|
+
});
|
|
3044
|
+
clients.push(client);
|
|
3045
|
+
const connected = await client.tools();
|
|
3046
|
+
for (const [toolName, mcpTool] of Object.entries(connected)) tools[`${name}__${toolName}`] = mcpTool;
|
|
3047
|
+
statuses.push({
|
|
3048
|
+
name,
|
|
3049
|
+
status: "connected",
|
|
3050
|
+
...identity,
|
|
3051
|
+
tools: Object.entries(connected).map(([toolName, mcpTool]) => toToolInfo(toolName, mcpTool))
|
|
3052
|
+
});
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3055
|
+
statuses.push({
|
|
3056
|
+
name,
|
|
3057
|
+
status: "failed",
|
|
3058
|
+
error: message,
|
|
3059
|
+
...identity
|
|
3060
|
+
});
|
|
3061
|
+
options.onError?.(name, error);
|
|
3062
|
+
if (options.required) {
|
|
3063
|
+
await closeAll();
|
|
3064
|
+
throw new Error(`MCP server '${name}' failed to connect: ${message}`);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
2659
3067
|
}
|
|
2660
3068
|
return {
|
|
2661
3069
|
tools,
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
3070
|
+
servers: statuses,
|
|
3071
|
+
close: closeAll
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
/** The connection's identity, minus its secrets — `headers` never travel. */
|
|
3075
|
+
function describeServer(server) {
|
|
3076
|
+
if ("url" in server) return {
|
|
3077
|
+
transport: server.type === "sse" ? "sse" : "http",
|
|
3078
|
+
url: server.url
|
|
3079
|
+
};
|
|
3080
|
+
return {
|
|
3081
|
+
transport: "stdio",
|
|
3082
|
+
command: server.command,
|
|
3083
|
+
args: server.args
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* The AI SDK hands back its own `Tool`, whose `inputSchema` may be a zod schema
|
|
3088
|
+
* or a `jsonSchema()` wrapper. Only the latter carries a JSON Schema document,
|
|
3089
|
+
* so that is the only case where parameters are reported — `McpServerToolInfo`
|
|
3090
|
+
* models the absence deliberately, and inventing one here would be worse.
|
|
3091
|
+
*/
|
|
3092
|
+
function toToolInfo(name, mcpTool) {
|
|
3093
|
+
const { description, inputSchema } = mcpTool ?? {};
|
|
3094
|
+
return {
|
|
3095
|
+
name,
|
|
3096
|
+
description: typeof description === "string" ? description : void 0,
|
|
3097
|
+
inputSchema: inputSchema?.jsonSchema
|
|
2665
3098
|
};
|
|
2666
3099
|
}
|
|
2667
3100
|
/**
|
|
@@ -2797,9 +3230,9 @@ const claudeAdapter = {
|
|
|
2797
3230
|
};
|
|
2798
3231
|
return { available: "unknown" };
|
|
2799
3232
|
},
|
|
2800
|
-
createRunner({ config, restore }) {
|
|
3233
|
+
createRunner({ config, restore, id }) {
|
|
2801
3234
|
if (restore) throw new Error("the Claude engine cannot rebuild a parked session");
|
|
2802
|
-
return new SessionRunner(config);
|
|
3235
|
+
return new SessionRunner(config, id);
|
|
2803
3236
|
},
|
|
2804
3237
|
/**
|
|
2805
3238
|
* The Agent SDK's on-disk session store, mapped browser-safe. The SDK reads
|
|
@@ -3177,15 +3610,29 @@ function userQuestionsFromCodex(questions) {
|
|
|
3177
3610
|
}))
|
|
3178
3611
|
}));
|
|
3179
3612
|
}
|
|
3180
|
-
/**
|
|
3181
|
-
*
|
|
3182
|
-
*
|
|
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
|
+
*/
|
|
3183
3625
|
function historyUserText(item) {
|
|
3184
3626
|
if (!Array.isArray(item.content)) return "";
|
|
3185
|
-
|
|
3627
|
+
let images = 0;
|
|
3628
|
+
const text = item.content.map((part) => {
|
|
3186
3629
|
const candidate = part;
|
|
3187
|
-
|
|
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 "";
|
|
3188
3633
|
}).filter(Boolean).join("\n");
|
|
3634
|
+
if (text) return text;
|
|
3635
|
+
return images > 0 ? `[${images === 1 ? "image" : `${images} images`}]` : "";
|
|
3189
3636
|
}
|
|
3190
3637
|
/** The AskUserQuestion answer convention (question text → chosen label(s),
|
|
3191
3638
|
* comma-joined) mapped back to codex's id-keyed shape. Questions the client
|
|
@@ -3345,6 +3792,8 @@ var CodexRunner = class {
|
|
|
3345
3792
|
id;
|
|
3346
3793
|
createdAt;
|
|
3347
3794
|
#config;
|
|
3795
|
+
/** {@link CodexRunnerConfig.cwd}, checked once in the constructor. */
|
|
3796
|
+
#cwd;
|
|
3348
3797
|
#events = [];
|
|
3349
3798
|
#listeners = /* @__PURE__ */ new Set();
|
|
3350
3799
|
#seq = 0;
|
|
@@ -3407,6 +3856,8 @@ var CodexRunner = class {
|
|
|
3407
3856
|
const mode = config.permissionMode ?? "default";
|
|
3408
3857
|
if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
|
|
3409
3858
|
if (config.forkSession) throw new Error("the codex engine cannot fork a resumed thread");
|
|
3859
|
+
if (!config.cwd) throw new Error("the codex engine requires a cwd");
|
|
3860
|
+
this.#cwd = config.cwd;
|
|
3410
3861
|
this.#config = config;
|
|
3411
3862
|
this.#permissionMode = mode;
|
|
3412
3863
|
this.#model = config.model;
|
|
@@ -3442,7 +3893,7 @@ var CodexRunner = class {
|
|
|
3442
3893
|
id: this.id,
|
|
3443
3894
|
sdkSessionId: this.#sdkSessionId,
|
|
3444
3895
|
status: this.#status,
|
|
3445
|
-
cwd: this.#
|
|
3896
|
+
cwd: this.#cwd,
|
|
3446
3897
|
profile: this.#config.profile,
|
|
3447
3898
|
engine: "codex",
|
|
3448
3899
|
capabilities: ENGINE_CAPABILITIES.codex,
|
|
@@ -3454,6 +3905,7 @@ var CodexRunner = class {
|
|
|
3454
3905
|
activityCount: this.#activityCount,
|
|
3455
3906
|
pendingPermissionCount: this.#approvals.size,
|
|
3456
3907
|
meta: this.#config.meta,
|
|
3908
|
+
scope: this.#config.scope,
|
|
3457
3909
|
title: this.#title(),
|
|
3458
3910
|
totalCostUsd: this.#totalCostUsd,
|
|
3459
3911
|
numTurns: this.#numTurns || void 0,
|
|
@@ -3684,8 +4136,13 @@ var CodexRunner = class {
|
|
|
3684
4136
|
});
|
|
3685
4137
|
this.#setStatus("closed");
|
|
3686
4138
|
}
|
|
3687
|
-
subscribe(listener, afterSeq = 0) {
|
|
3688
|
-
|
|
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
|
+
}
|
|
3689
4146
|
this.#listeners.add(listener);
|
|
3690
4147
|
return () => this.#listeners.delete(listener);
|
|
3691
4148
|
}
|
|
@@ -3738,7 +4195,7 @@ var CodexRunner = class {
|
|
|
3738
4195
|
}
|
|
3739
4196
|
if (!this.#threadLoaded) {
|
|
3740
4197
|
const options = {
|
|
3741
|
-
cwd: this.#
|
|
4198
|
+
cwd: this.#cwd,
|
|
3742
4199
|
approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
|
|
3743
4200
|
sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode]
|
|
3744
4201
|
};
|
|
@@ -3779,7 +4236,7 @@ var CodexRunner = class {
|
|
|
3779
4236
|
if (this.#skillsRefresh) return this.#skillsRefresh;
|
|
3780
4237
|
const run = (async () => {
|
|
3781
4238
|
try {
|
|
3782
|
-
const result = await connection.request("skills/list", { cwds: [this.#
|
|
4239
|
+
const result = await connection.request("skills/list", { cwds: [this.#cwd] });
|
|
3783
4240
|
if (this.#closed) return;
|
|
3784
4241
|
const entries = Array.isArray(result?.data) ? result.data : [];
|
|
3785
4242
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3981,7 +4438,7 @@ var CodexRunner = class {
|
|
|
3981
4438
|
const params = {
|
|
3982
4439
|
threadId: this.#sdkSessionId,
|
|
3983
4440
|
input: turn.input,
|
|
3984
|
-
cwd: this.#
|
|
4441
|
+
cwd: this.#cwd,
|
|
3985
4442
|
approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
|
|
3986
4443
|
sandboxPolicy: TURN_SANDBOX_BY_MODE[this.#permissionMode]
|
|
3987
4444
|
};
|
|
@@ -4012,131 +4469,137 @@ var CodexRunner = class {
|
|
|
4012
4469
|
}
|
|
4013
4470
|
#handleNotification(method, params) {
|
|
4014
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) => {
|
|
4015
4497
|
const active = this.#activeTurn;
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
this.#emitRateLimits(params?.rateLimits);
|
|
4102
|
-
return;
|
|
4103
|
-
case "turn/plan/updated": {
|
|
4104
|
-
if (!active) return;
|
|
4105
|
-
const plan = params?.plan;
|
|
4106
|
-
if (!Array.isArray(plan)) return;
|
|
4107
|
-
this.#emit({
|
|
4108
|
-
type: "sdk_event",
|
|
4109
|
-
payload: {
|
|
4110
|
-
type: "codex.todo_list",
|
|
4111
|
-
id: `${active.nonce}:plan`,
|
|
4112
|
-
items: plan.map((step) => ({
|
|
4113
|
-
text: step.step,
|
|
4114
|
-
completed: step.status === "completed"
|
|
4115
|
-
}))
|
|
4116
|
-
}
|
|
4117
|
-
});
|
|
4118
|
-
return;
|
|
4119
|
-
}
|
|
4120
|
-
case "serverRequest/resolved": {
|
|
4121
|
-
const requestId = params?.requestId;
|
|
4122
|
-
if (requestId === void 0) return;
|
|
4123
|
-
for (const [id, pending] of this.#approvals) if (pending.wireId === requestId) {
|
|
4124
|
-
this.#settleApproval(id, pending, {
|
|
4125
|
-
behavior: "deny",
|
|
4126
|
-
message: "resolved by codex"
|
|
4127
|
-
}, "policy");
|
|
4128
|
-
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
|
+
}))
|
|
4129
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");
|
|
4130
4594
|
return;
|
|
4131
4595
|
}
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
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;
|
|
4138
4601
|
}
|
|
4139
|
-
}
|
|
4602
|
+
};
|
|
4140
4603
|
/** Answer a server→client request: the ask channels become pending
|
|
4141
4604
|
* permission requests; anything else gets a JSON-RPC -32601 rather than a
|
|
4142
4605
|
* hang (an unanswered server request wedges the turn). */
|
|
@@ -4282,83 +4745,89 @@ var CodexRunner = class {
|
|
|
4282
4745
|
}
|
|
4283
4746
|
#handleItemCompleted(item, active) {
|
|
4284
4747
|
const id = `${active.nonce}:${item.id}`;
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
case "reasoning": {
|
|
4297
|
-
const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
|
|
4298
|
-
const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
|
|
4299
|
-
const thinking = (summary.length > 0 ? summary : content).join("\n\n");
|
|
4300
|
-
if (thinking) this.#emitAssistant(id, [{
|
|
4301
|
-
type: "thinking",
|
|
4302
|
-
thinking
|
|
4303
|
-
}]);
|
|
4304
|
-
return;
|
|
4305
|
-
}
|
|
4306
|
-
case "commandExecution": {
|
|
4307
|
-
if (!active.toolUseEmitted.has(id)) {
|
|
4308
|
-
active.toolUseEmitted.add(id);
|
|
4309
|
-
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
4310
|
-
}
|
|
4311
|
-
const exitCode = item.exitCode ?? void 0;
|
|
4312
|
-
const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
|
|
4313
|
-
const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
|
|
4314
|
-
this.#emitToolResult(id, output, failed);
|
|
4315
|
-
return;
|
|
4316
|
-
}
|
|
4317
|
-
case "fileChange": {
|
|
4318
|
-
this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
|
|
4319
|
-
const lines = item.changes.map((change) => {
|
|
4320
|
-
return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
|
|
4321
|
-
});
|
|
4322
|
-
this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined");
|
|
4323
|
-
return;
|
|
4324
|
-
}
|
|
4325
|
-
case "mcpToolCall": {
|
|
4326
|
-
if (!active.toolUseEmitted.has(id)) {
|
|
4327
|
-
active.toolUseEmitted.add(id);
|
|
4328
|
-
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
4329
|
-
}
|
|
4330
|
-
const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
|
|
4331
|
-
this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
|
|
4332
|
-
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
|
|
4333
4759
|
}
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
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)) {
|
|
4339
4791
|
active.toolUseEmitted.add(id);
|
|
4340
|
-
this.#emitToolUse(id,
|
|
4341
|
-
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
4342
|
-
const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
|
|
4343
|
-
this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
|
|
4344
|
-
return;
|
|
4792
|
+
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
4345
4793
|
}
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
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);
|
|
4359
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);
|
|
4360
4829
|
}
|
|
4361
|
-
}
|
|
4830
|
+
};
|
|
4362
4831
|
#emitDelta(delta) {
|
|
4363
4832
|
if (this.#config.includePartialMessages === false) return;
|
|
4364
4833
|
this.#emit({
|
|
@@ -4400,7 +4869,7 @@ var CodexRunner = class {
|
|
|
4400
4869
|
uuid: `${id}-use`
|
|
4401
4870
|
});
|
|
4402
4871
|
}
|
|
4403
|
-
#emitToolResult(toolUseId, content, isError) {
|
|
4872
|
+
#emitToolResult(toolUseId, content, isError, patch) {
|
|
4404
4873
|
this.#emit({
|
|
4405
4874
|
type: "user_message",
|
|
4406
4875
|
message: {
|
|
@@ -4414,6 +4883,7 @@ var CodexRunner = class {
|
|
|
4414
4883
|
},
|
|
4415
4884
|
parentToolUseId: null,
|
|
4416
4885
|
synthetic: true,
|
|
4886
|
+
patch,
|
|
4417
4887
|
uuid: `${toolUseId}-result`
|
|
4418
4888
|
});
|
|
4419
4889
|
}
|
|
@@ -4905,7 +5375,7 @@ const codexAdapter = {
|
|
|
4905
5375
|
capabilities: ENGINE_CAPABILITIES.codex,
|
|
4906
5376
|
catalog: CODEX_CATALOG,
|
|
4907
5377
|
checkAvailability: (profile, env) => checkCodexAvailability(profile, env),
|
|
4908
|
-
createRunner({ config, profile, restore }) {
|
|
5378
|
+
createRunner({ config, profile, restore, id }) {
|
|
4909
5379
|
if (restore) throw new Error("the codex engine cannot rebuild a parked session");
|
|
4910
5380
|
const executable = config.codexPathOverride ?? resolveBundledCodexExecutable();
|
|
4911
5381
|
if (!executable) throw new Error(NOT_INSTALLED);
|
|
@@ -4916,7 +5386,7 @@ const codexAdapter = {
|
|
|
4916
5386
|
executable,
|
|
4917
5387
|
...options
|
|
4918
5388
|
})
|
|
4919
|
-
});
|
|
5389
|
+
}, id);
|
|
4920
5390
|
},
|
|
4921
5391
|
async listSessions(options) {
|
|
4922
5392
|
const executable = resolveBundledCodexExecutable();
|
|
@@ -4976,6 +5446,6 @@ function getEngineAdapter(engine) {
|
|
|
4976
5446
|
return ADAPTERS[engine ?? "claude"];
|
|
4977
5447
|
}
|
|
4978
5448
|
//#endregion
|
|
4979
|
-
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, withMcpTools };
|
|
5449
|
+
export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withHostTools, withMcpTools };
|
|
4980
5450
|
|
|
4981
5451
|
//# sourceMappingURL=index.mjs.map
|