zidane 5.6.15 → 5.7.4

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.
Files changed (118) hide show
  1. package/README.md +3 -1
  2. package/dist/{agent-CkJp_ZOR.d.ts → agent-BNS2nx_T.d.ts} +535 -15
  3. package/dist/agent-BNS2nx_T.d.ts.map +1 -0
  4. package/dist/chat/pure.d.ts +4 -0
  5. package/dist/chat/pure.js +3 -0
  6. package/dist/chat.d.ts +31 -661
  7. package/dist/chat.d.ts.map +1 -1
  8. package/dist/chat.js +5 -3
  9. package/dist/chat.js.map +1 -1
  10. package/dist/contexts/docker.d.ts +1 -1
  11. package/dist/contexts/docker.d.ts.map +1 -1
  12. package/dist/contexts/docker.js.map +1 -1
  13. package/dist/{contexts-BOtMvzli.js → contexts-BD2U_xpi.js} +2 -2
  14. package/dist/{contexts-BOtMvzli.js.map → contexts-BD2U_xpi.js.map} +1 -1
  15. package/dist/contexts.d.ts +3 -3
  16. package/dist/contexts.js +1 -1
  17. package/dist/edit-utils-DnfNoj16.js +574 -0
  18. package/dist/edit-utils-DnfNoj16.js.map +1 -0
  19. package/dist/{errors-DdZXnyXE.js → errors-CoQnKRf1.js} +32 -2
  20. package/dist/{errors-DdZXnyXE.js.map → errors-CoQnKRf1.js.map} +1 -1
  21. package/dist/fetch-url-CPxfiXDa.js +518 -0
  22. package/dist/fetch-url-CPxfiXDa.js.map +1 -0
  23. package/dist/{image-sniff-CmlNMPMr.js → image-sniff-B7uFSNO1.js} +1 -1
  24. package/dist/{image-sniff-CmlNMPMr.js.map → image-sniff-B7uFSNO1.js.map} +1 -1
  25. package/dist/{index-CtIS28mN.d.ts → index-CZOwAJIX.d.ts} +2 -2
  26. package/dist/index-CZOwAJIX.d.ts.map +1 -0
  27. package/dist/{index-CsWckg9p.d.ts → index-Ck_AWt8P.d.ts} +3 -4
  28. package/dist/index-Ck_AWt8P.d.ts.map +1 -0
  29. package/dist/{index-BXQC3I4d.d.ts → index-KiS7w0dC.d.ts} +3 -3
  30. package/dist/index-KiS7w0dC.d.ts.map +1 -0
  31. package/dist/index.d.ts +6 -6
  32. package/dist/index.js +13 -12
  33. package/dist/index.js.map +1 -1
  34. package/dist/{interpolate-C55ZIcKz.js → interpolate-TySiqKzc.js} +23 -23
  35. package/dist/{interpolate-C55ZIcKz.js.map → interpolate-TySiqKzc.js.map} +1 -1
  36. package/dist/{login-CY9uShjX.js → login-BDeqENSe.js} +7 -58
  37. package/dist/login-BDeqENSe.js.map +1 -0
  38. package/dist/{mcp-DDOc8hOM.js → mcp-Kqzz-Rs_.js} +5 -5
  39. package/dist/{mcp-DDOc8hOM.js.map → mcp-Kqzz-Rs_.js.map} +1 -1
  40. package/dist/mcp.d.ts +2 -2
  41. package/dist/mcp.js +1 -1
  42. package/dist/{messages-B-tuI2Ur.js → messages-CvRQTdbR.js} +93 -30
  43. package/dist/messages-CvRQTdbR.js.map +1 -0
  44. package/dist/{presets-CMkLtFFW.js → presets-JuOnSI-i.js} +2 -2
  45. package/dist/{presets-CMkLtFFW.js.map → presets-JuOnSI-i.js.map} +1 -1
  46. package/dist/presets.d.ts +3 -3
  47. package/dist/presets.js +1 -1
  48. package/dist/{providers-CRQQDuxx.js → providers-h4HJPbbv.js} +485 -31
  49. package/dist/providers-h4HJPbbv.js.map +1 -0
  50. package/dist/providers.d.ts +2 -2
  51. package/dist/providers.js +3 -3
  52. package/dist/restate.d.ts +1 -1
  53. package/dist/restate.d.ts.map +1 -1
  54. package/dist/restate.js.map +1 -1
  55. package/dist/session/sqlite.d.ts +1 -1
  56. package/dist/session/sqlite.d.ts.map +1 -1
  57. package/dist/session/sqlite.js +1 -1
  58. package/dist/session/sqlite.js.map +1 -1
  59. package/dist/{session-BCT6eYxo.js → session-BzLou2_-.js} +2 -2
  60. package/dist/{session-BCT6eYxo.js.map → session-BzLou2_-.js.map} +1 -1
  61. package/dist/session.d.ts +2 -2
  62. package/dist/session.js +2 -2
  63. package/dist/skills.d.ts +3 -3
  64. package/dist/skills.js +1 -1
  65. package/dist/skills.js.map +1 -1
  66. package/dist/{stats-CIv4j3Sz.js → stats-DAKBEKjc.js} +12 -2
  67. package/dist/stats-DAKBEKjc.js.map +1 -0
  68. package/dist/{stdio-loader-OOOXzUvm.js → stdio-loader-Ce68wUmM.js} +4 -4
  69. package/dist/stdio-loader-Ce68wUmM.js.map +1 -0
  70. package/dist/tool-formatters-CU-j3a3e.d.ts +1471 -0
  71. package/dist/tool-formatters-CU-j3a3e.d.ts.map +1 -0
  72. package/dist/tools/fetch-url.d.ts +70 -0
  73. package/dist/tools/fetch-url.d.ts.map +1 -0
  74. package/dist/tools/fetch-url.js +2 -0
  75. package/dist/tools/web-search.d.ts +7 -0
  76. package/dist/tools/web-search.d.ts.map +1 -0
  77. package/dist/tools/web-search.js +190 -0
  78. package/dist/tools/web-search.js.map +1 -0
  79. package/dist/{tools-0Kolu2bY.js → tools-BGtJK0vo.js} +1365 -420
  80. package/dist/tools-BGtJK0vo.js.map +1 -0
  81. package/dist/tools.d.ts +3 -3
  82. package/dist/tools.js +1 -1
  83. package/dist/{turn-operations-DkLoiyF4.js → transcript-anchors-BTSZAPVc.js} +147 -2713
  84. package/dist/transcript-anchors-BTSZAPVc.js.map +1 -0
  85. package/dist/{transcript-anchors-C8IqWH4x.d.ts → transcript-anchors-DX90kXc4.d.ts} +13 -1299
  86. package/dist/transcript-anchors-DX90kXc4.d.ts.map +1 -0
  87. package/dist/tui.d.ts +58 -28
  88. package/dist/tui.d.ts.map +1 -1
  89. package/dist/tui.js +1348 -422
  90. package/dist/tui.js.map +1 -1
  91. package/dist/turn-operations-CCHfR9eC.js +1938 -0
  92. package/dist/turn-operations-CCHfR9eC.js.map +1 -0
  93. package/dist/turn-operations-DDIl4YVk.d.ts +658 -0
  94. package/dist/turn-operations-DDIl4YVk.d.ts.map +1 -0
  95. package/dist/{types-oKPBdCmL.js → types-BPw_i5vb.js} +1 -1
  96. package/dist/types-BPw_i5vb.js.map +1 -0
  97. package/dist/{types-2PMY5Rlc.d.ts → types-CEAMIUXw.d.ts} +1 -1
  98. package/dist/types-CEAMIUXw.d.ts.map +1 -0
  99. package/dist/types.d.ts +4 -4
  100. package/dist/types.js +3 -3
  101. package/docs/CHAT.md +53 -6
  102. package/docs/SKILL.md +3 -0
  103. package/docs/TUI.md +7 -0
  104. package/package.json +18 -2
  105. package/dist/agent-CkJp_ZOR.d.ts.map +0 -1
  106. package/dist/index-BXQC3I4d.d.ts.map +0 -1
  107. package/dist/index-CsWckg9p.d.ts.map +0 -1
  108. package/dist/index-CtIS28mN.d.ts.map +0 -1
  109. package/dist/login-CY9uShjX.js.map +0 -1
  110. package/dist/messages-B-tuI2Ur.js.map +0 -1
  111. package/dist/providers-CRQQDuxx.js.map +0 -1
  112. package/dist/stats-CIv4j3Sz.js.map +0 -1
  113. package/dist/stdio-loader-OOOXzUvm.js.map +0 -1
  114. package/dist/tools-0Kolu2bY.js.map +0 -1
  115. package/dist/transcript-anchors-C8IqWH4x.d.ts.map +0 -1
  116. package/dist/turn-operations-DkLoiyF4.js.map +0 -1
  117. package/dist/types-2PMY5Rlc.d.ts.map +0 -1
  118. package/dist/types-oKPBdCmL.js.map +0 -1
@@ -1,16 +1,21 @@
1
- import { n as createProcessContext } from "./contexts-BOtMvzli.js";
2
- import { c as errorMessage, i as AgentProviderError, n as AgentBudgetExceededError, o as AgentToolPairingError, t as AgentAbortedError, u as toTypedError } from "./errors-DdZXnyXE.js";
3
- import { n as toolOutputByteLength, t as DEFAULT_AGENT_CLOCK } from "./types-oKPBdCmL.js";
4
- import { E as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-B-tuI2Ur.js";
5
- import { t as reconcileImageMediaType } from "./image-sniff-CmlNMPMr.js";
6
- import { t as connectMcpServers } from "./mcp-DDOc8hOM.js";
7
- import { _ as validateResourcePath, b as createSkillActivationState, d as escapeXml, n as resolveSkills, p as installAllowedToolsGate, t as interpolateShellCommands, u as buildCatalog } from "./interpolate-C55ZIcKz.js";
8
- import { n as formatTokenUsage, t as flattenTurns } from "./stats-CIv4j3Sz.js";
1
+ import { c as formatTaskStatus, f as buildContextBreakdown, h as utf8ByteLength, l as formatTaskSummary, n as stripLineNumberPrefixes, r as styleReplacementForVia, s as formatDuration, t as resolveOldString, u as previewLine } from "./edit-utils-DnfNoj16.js";
2
+ import { a as createCursorOAuthProvider, c as anthropic, d as ANTHROPIC_EXTRA_MODELS, f as FAST_MODE_OPTIONS, n as openai, o as generatePkce, r as local, s as cerebras, t as openrouter } from "./providers-h4HJPbbv.js";
3
+ import { f as toTypedError, i as AgentProviderError, l as errorMessage, n as AgentBudgetExceededError, o as AgentToolPairingError, t as AgentAbortedError } from "./errors-CoQnKRf1.js";
4
+ import { E as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, k as renderSystemForWire, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-CvRQTdbR.js";
5
+ import { t as reconcileImageMediaType } from "./image-sniff-B7uFSNO1.js";
6
+ import { n as createProcessContext } from "./contexts-BD2U_xpi.js";
7
+ import { n as toolOutputByteLength, t as DEFAULT_AGENT_CLOCK } from "./types-BPw_i5vb.js";
8
+ import { b as escapeXml, f as installAllowedToolsGate, g as validateResourcePath, n as resolveSkills, t as interpolateShellCommands, u as buildCatalog, y as createSkillActivationState } from "./interpolate-TySiqKzc.js";
9
+ import { t as connectMcpServers } from "./mcp-Kqzz-Rs_.js";
10
+ import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
9
11
  import { dirname, isAbsolute, join, resolve } from "node:path";
10
12
  import { createHooks } from "hookable";
11
- import { homedir } from "node:os";
12
- import { glob, mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
13
+ import { getModel, getModels } from "@earendil-works/pi-ai";
13
14
  import { Buffer } from "node:buffer";
15
+ import { refreshAnthropicToken, refreshOpenAICodexToken, registerOAuthProvider } from "@earendil-works/pi-ai/oauth";
16
+ import { createServer } from "node:http";
17
+ import { randomBytes } from "node:crypto";
18
+ import { glob, mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
14
19
  //#region src/aliasing.ts
15
20
  /**
16
21
  * Build alias lookup maps from a `toolAliases` record.
@@ -144,126 +149,966 @@ function rewriteMessagesToWire(messages, maps) {
144
149
  }));
145
150
  }
146
151
  //#endregion
147
- //#region src/chat/format.ts
148
- /** Compact token formatter 12_415 → "12.4k", 1_234_567 → "1.23M". */
149
- function fmtTokens(n) {
150
- if (n < 1e3) return String(n);
151
- if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 2 : 1)}k`;
152
- return `${(n / 1e6).toFixed(2)}M`;
153
- }
154
- /** Compact relative-time formatter — "just now / 5m / 3h / 2d". */
155
- function ageString(ts, now = Date.now()) {
156
- const m = Math.floor((now - ts) / 6e4);
157
- if (m < 1) return "just now";
158
- if (m < 60) return `${m}m ago`;
159
- const h = Math.floor(m / 60);
160
- if (h < 24) return `${h}h ago`;
161
- return `${Math.floor(h / 24)}d ago`;
162
- }
163
- /** Six-char short form of a session id for headers and lists. */
164
- function shortId(id) {
165
- return id.replace(/-/g, "").slice(0, 6);
166
- }
167
- /**
168
- * Single-line preview of a multi-line string, capped at `max` chars and
169
- * ellipsis-terminated when truncated.
170
- *
171
- * Whitespace runs (newlines, tabs, multiple spaces) collapse into one
172
- * space so the rendered output stays on a single visual row no matter
173
- * how the input was shaped. Used by every transcript "preview" surface
174
- * (spawn-start task, `tool: shell (background): <command>`,
175
- * `<task-notification>` summary line, etc.) — without the whitespace
176
- * collapse, a 60-char `slice` on a string with an inline `\n\n` paints
177
- * the second paragraph below the first, producing the visible
178
- * "preview text spills onto multiple lines" bug (and, downstream,
179
- * misaligned spawn markers when the wrapped lines collide with
180
- * other events).
181
- *
182
- * Reserves one slot for the `…` so the displayed width is exactly
183
- * `max` when truncation kicks in.
184
- */
185
- function previewLine(s, max) {
186
- const single = s.replace(/\s+/g, " ").trim();
187
- if (single.length <= max) return single;
188
- return `${single.slice(0, max - 1)}…`;
189
- }
190
- /**
191
- * Compact human-readable duration formatter shared by background-task
192
- * surfaces (the `<task-notification>` summary, the TUI banner, the
193
- * `shell_kill` tool result, etc.).
194
- *
195
- * Format ladder:
196
- * - `< 1s` → `"Nms"`
197
- * - `< 10s` → `"N.Ns"` (one decimal)
198
- * - `< 1m` → `"Ns"` (whole seconds)
199
- * - `< 1h` → `"NmNs"` / `"Nm"` when seconds round to 0
200
- * - `≥ 1h` → `"NhNm"` / `"Nh"` when minutes round to 0
201
- *
202
- * Single source of truth so a 60s task renders the same across the
203
- * model-facing XML summary and the user-facing banner. Earlier
204
- * separate formatters disagreed (XML said `"60.0s"`, banner said `"1m"`)
205
- * which was confusing to the user reading both side by side.
206
- */
207
- function formatDuration(ms) {
208
- if (ms < 0) ms = 0;
209
- if (ms < 1e3) return `${ms}ms`;
210
- if (ms < 6e4) return `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)}s`;
211
- const minutes = Math.floor(ms / 6e4);
212
- const seconds = Math.floor(ms % 6e4 / 1e3);
213
- if (minutes < 60) return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
214
- const hours = Math.floor(minutes / 60);
215
- const remMinutes = minutes % 60;
216
- return remMinutes > 0 ? `${hours}h${remMinutes}m` : `${hours}h`;
217
- }
218
- /**
219
- * Status label for a terminated background task — `"exited <code>"`
220
- * for natural exits, `"killed"` (with the signal name when known)
221
- * for our-issued SIGTERMs.
222
- *
223
- * Pulled out as its own function so the `<task-notification>` XML
224
- * summary, the TUI banner header, the `shell_kill` tool result, and
225
- * future surfaces all read the same string.
226
- */
227
- function formatTaskStatus(info) {
228
- return info.status === "killed" ? `killed${info.signal ? ` (${info.signal})` : ""}` : `exited ${info.exitCode}`;
229
- }
230
- /**
231
- * One-line summary of a terminated background task — the shape used by
232
- * the `<task-notification>` XML's `<summary>` tag AND the TUI banner's
233
- * `event.text` fallback string. Three dot-separated segments:
234
- *
235
- * `<command preview · status · duration>`
236
- *
237
- * Centralizes the format so live + replay + wire all agree, and so a
238
- * future cosmetic tweak (separator glyph, segment ordering) lands in
239
- * exactly one place.
240
- */
241
- function formatTaskSummary(info, maxCommandChars = 80) {
242
- return `${previewLine(info.command, maxCommandChars)} · ${formatTaskStatus(info)} · ${formatDuration(info.durationMs)}`;
243
- }
244
- /**
245
- * Compact an absolute path for display: replace the user's `$HOME`
246
- * prefix with `~` (so `/Users/yael/Code/zidane` → `~/Code/zidane`),
247
- * and optionally left-truncate with an ellipsis when the result
248
- * still exceeds `maxWidth` (so the path's *tail* — the part the user
249
- * recognizes — stays visible: `…/zidane` rather than `/Users/yaeluil…`).
250
- *
251
- * `maxWidth` is the maximum *display width* in cells. Omit to skip
252
- * truncation. Paths outside `$HOME` are returned verbatim modulo
253
- * truncation. The ellipsis (`…`) counts as one cell.
254
- *
255
- * `home` overrides `os.homedir()` for tests; production callers leave
256
- * it undefined and pay the cheap one-syscall lookup per call.
152
+ //#region src/chat/oauth-page/server.ts
153
+ const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1";
154
+ function buildRequestHandler(opts, pending) {
155
+ return (req, res) => {
156
+ try {
157
+ const url = new URL(req.url || "", "http://localhost");
158
+ if (url.pathname !== opts.path) {
159
+ respondError(res, 404, opts, "Callback route not found.");
160
+ return;
161
+ }
162
+ const error = url.searchParams.get("error");
163
+ if (error) {
164
+ respondError(res, 400, opts, `${opts.providerName} authentication did not complete.`, `Error: ${error}`);
165
+ return;
166
+ }
167
+ const code = url.searchParams.get("code");
168
+ const state = url.searchParams.get("state");
169
+ if (!code || !state) {
170
+ respondError(res, 400, opts, "Missing code or state parameter.");
171
+ return;
172
+ }
173
+ if (state !== opts.expectedState) {
174
+ respondError(res, 400, opts, "State mismatch.");
175
+ return;
176
+ }
177
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
178
+ res.end(opts.renderPage({
179
+ kind: "success",
180
+ provider: opts.providerName,
181
+ message: opts.successMessage
182
+ }));
183
+ pending.settle({
184
+ code,
185
+ state
186
+ });
187
+ } catch {
188
+ respondError(res, 500, opts, "Internal error while processing the callback.");
189
+ }
190
+ };
191
+ }
192
+ function respondError(res, status, opts, message, details) {
193
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
194
+ res.end(opts.renderPage({
195
+ kind: "error",
196
+ provider: opts.providerName,
197
+ message,
198
+ details
199
+ }));
200
+ }
201
+ function buildStubHandle(redirectUri, server) {
202
+ return {
203
+ redirectUri,
204
+ waitForCode: async () => null,
205
+ cancelWait: () => {},
206
+ close: () => {
207
+ try {
208
+ server?.close();
209
+ } catch {}
210
+ }
211
+ };
212
+ }
213
+ /**
214
+ * Start the loopback HTTP callback server. The returned handle is the
215
+ * caller's contract they `await waitForCode()`, race it against manual
216
+ * paste, then `close()` in a `finally`.
217
+ */
218
+ async function startCallbackServer(opts) {
219
+ const onListenError = opts.onListenError ?? "reject";
220
+ const redirectUri = `http://localhost:${opts.port}${opts.path}`;
221
+ return new Promise((resolve, reject) => {
222
+ let settled = false;
223
+ const pending = { settle: () => {} };
224
+ const waitPromise = new Promise((resolveWait) => {
225
+ pending.settle = (value) => {
226
+ if (settled) return;
227
+ settled = true;
228
+ resolveWait(value);
229
+ };
230
+ });
231
+ const server = createServer(buildRequestHandler(opts, pending));
232
+ server.on("error", (err) => {
233
+ if (onListenError === "resolveWithStub") {
234
+ pending.settle(null);
235
+ resolve(buildStubHandle(redirectUri, server));
236
+ return;
237
+ }
238
+ reject(err);
239
+ });
240
+ server.listen(opts.port, CALLBACK_HOST, () => {
241
+ resolve({
242
+ redirectUri,
243
+ waitForCode: () => waitPromise,
244
+ cancelWait: () => pending.settle(null),
245
+ close: () => {
246
+ try {
247
+ server.close();
248
+ } catch {}
249
+ }
250
+ });
251
+ });
252
+ });
253
+ }
254
+ //#endregion
255
+ //#region src/chat/oauth-page/anthropic.ts
256
+ const CLIENT_ID$1 = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
257
+ const AUTHORIZE_URL$1 = "https://claude.ai/oauth/authorize";
258
+ const TOKEN_URL$1 = "https://platform.claude.com/v1/oauth/token";
259
+ const CALLBACK_PORT$1 = 53692;
260
+ const CALLBACK_PATH$1 = "/callback";
261
+ const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
262
+ const PROVIDER_NAME$1 = "Anthropic";
263
+ /**
264
+ * Parse what the user pasted into the manual-code prompt. Accepts:
265
+ * - the bare authorization code
266
+ * - the full `redirect_uri?code=...&state=...` URL
267
+ * - the `code#state` shorthand Anthropic surfaces on the page
268
+ * - a raw query string with `code=...&state=...`
269
+ *
270
+ * Matches pi-ai's parse table so an existing user's muscle memory still works.
271
+ */
272
+ function parseAuthorizationInput$1(input) {
273
+ const value = input.trim();
274
+ if (!value) return {};
275
+ try {
276
+ const url = new URL(value);
277
+ return {
278
+ code: url.searchParams.get("code") ?? void 0,
279
+ state: url.searchParams.get("state") ?? void 0
280
+ };
281
+ } catch {}
282
+ if (value.includes("#")) {
283
+ const [code, state] = value.split("#", 2);
284
+ return {
285
+ code,
286
+ state
287
+ };
288
+ }
289
+ if (value.includes("code=")) {
290
+ const params = new URLSearchParams(value);
291
+ return {
292
+ code: params.get("code") ?? void 0,
293
+ state: params.get("state") ?? void 0
294
+ };
295
+ }
296
+ return { code: value };
297
+ }
298
+ async function postJson(url, body) {
299
+ const response = await fetch(url, {
300
+ method: "POST",
301
+ headers: {
302
+ "Content-Type": "application/json",
303
+ "Accept": "application/json"
304
+ },
305
+ body: JSON.stringify(body),
306
+ signal: AbortSignal.timeout(3e4)
307
+ });
308
+ const responseBody = await response.text();
309
+ if (!response.ok) throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);
310
+ return responseBody;
311
+ }
312
+ async function exchangeAuthorizationCode$1(code, state, verifier, redirectUri) {
313
+ const responseBody = await postJson(TOKEN_URL$1, {
314
+ grant_type: "authorization_code",
315
+ client_id: CLIENT_ID$1,
316
+ code,
317
+ state,
318
+ redirect_uri: redirectUri,
319
+ code_verifier: verifier
320
+ });
321
+ const tokenData = JSON.parse(responseBody);
322
+ return {
323
+ refresh: tokenData.refresh_token,
324
+ access: tokenData.access_token,
325
+ expires: Date.now() + tokenData.expires_in * 1e3 - 300 * 1e3
326
+ };
327
+ }
328
+ async function loginAnthropicWithCustomPage(options) {
329
+ const { verifier, challenge } = await generatePkce();
330
+ const server = await startCallbackServer({
331
+ port: CALLBACK_PORT$1,
332
+ path: CALLBACK_PATH$1,
333
+ expectedState: verifier,
334
+ providerName: PROVIDER_NAME$1,
335
+ renderPage: options.renderPage,
336
+ successMessage: "Anthropic authentication completed. You can close this window.",
337
+ onListenError: "reject"
338
+ });
339
+ let code;
340
+ let state;
341
+ try {
342
+ const authParams = new URLSearchParams({
343
+ code: "true",
344
+ client_id: CLIENT_ID$1,
345
+ response_type: "code",
346
+ redirect_uri: server.redirectUri,
347
+ scope: SCOPES,
348
+ code_challenge: challenge,
349
+ code_challenge_method: "S256",
350
+ state: verifier
351
+ });
352
+ options.onAuth({
353
+ url: `${AUTHORIZE_URL$1}?${authParams.toString()}`,
354
+ instructions: "Complete login in your browser. If the browser is on another machine, paste the final redirect URL here."
355
+ });
356
+ if (options.onManualCodeInput) {
357
+ let manualInput;
358
+ let manualError;
359
+ const manualPromise = options.onManualCodeInput().then((input) => {
360
+ manualInput = input;
361
+ server.cancelWait();
362
+ }).catch((err) => {
363
+ manualError = err instanceof Error ? err : new Error(String(err));
364
+ server.cancelWait();
365
+ });
366
+ const result = await server.waitForCode();
367
+ if (manualError) throw manualError;
368
+ if (result?.code) {
369
+ code = result.code;
370
+ state = result.state;
371
+ } else if (manualInput) {
372
+ const parsed = parseAuthorizationInput$1(manualInput);
373
+ if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
374
+ code = parsed.code;
375
+ state = parsed.state ?? verifier;
376
+ }
377
+ if (!code) {
378
+ await manualPromise;
379
+ if (manualError) throw manualError;
380
+ if (manualInput) {
381
+ const parsed = parseAuthorizationInput$1(manualInput);
382
+ if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
383
+ code = parsed.code;
384
+ state = parsed.state ?? verifier;
385
+ }
386
+ }
387
+ } else {
388
+ const result = await server.waitForCode();
389
+ if (result?.code) {
390
+ code = result.code;
391
+ state = result.state;
392
+ }
393
+ }
394
+ if (!code) {
395
+ const parsed = parseAuthorizationInput$1(await options.onPrompt({
396
+ message: "Paste the authorization code or full redirect URL:",
397
+ placeholder: server.redirectUri
398
+ }));
399
+ if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
400
+ code = parsed.code;
401
+ state = parsed.state ?? verifier;
402
+ }
403
+ if (!code) throw new Error("Missing authorization code");
404
+ if (!state) throw new Error("Missing OAuth state");
405
+ options.onProgress?.("Exchanging authorization code for tokens...");
406
+ return await exchangeAuthorizationCode$1(code, state, verifier, server.redirectUri);
407
+ } finally {
408
+ server.close();
409
+ }
410
+ }
411
+ /**
412
+ * Build an `OAuthProviderInterface` that behaves identically to pi-ai's
413
+ * `anthropicOAuthProvider` except for the callback page HTML. Drop this
414
+ * onto `ProviderDescriptor.oauthProvider` to override.
415
+ */
416
+ function createAnthropicOAuthProviderWithCustomPage(renderPage) {
417
+ return {
418
+ id: "anthropic",
419
+ name: "Anthropic (Claude Pro/Max)",
420
+ usesCallbackServer: true,
421
+ async login(callbacks) {
422
+ return loginAnthropicWithCustomPage({
423
+ renderPage,
424
+ onAuth: callbacks.onAuth,
425
+ onPrompt: callbacks.onPrompt,
426
+ onProgress: callbacks.onProgress,
427
+ onManualCodeInput: callbacks.onManualCodeInput
428
+ });
429
+ },
430
+ async refreshToken(credentials) {
431
+ return refreshAnthropicToken(credentials.refresh);
432
+ },
433
+ getApiKey(credentials) {
434
+ return credentials.access;
435
+ }
436
+ };
437
+ }
438
+ //#endregion
439
+ //#region src/chat/oauth-page/openai-codex.ts
440
+ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
441
+ const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
442
+ const TOKEN_URL = "https://auth.openai.com/oauth/token";
443
+ const CALLBACK_PORT = 1455;
444
+ const CALLBACK_PATH = "/auth/callback";
445
+ const SCOPE = "openid profile email offline_access";
446
+ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
447
+ const PROVIDER_NAME = "OpenAI";
448
+ function createState() {
449
+ return randomBytes(16).toString("hex");
450
+ }
451
+ function parseAuthorizationInput(input) {
452
+ const value = input.trim();
453
+ if (!value) return {};
454
+ try {
455
+ const url = new URL(value);
456
+ return {
457
+ code: url.searchParams.get("code") ?? void 0,
458
+ state: url.searchParams.get("state") ?? void 0
459
+ };
460
+ } catch {}
461
+ if (value.includes("#")) {
462
+ const [code, state] = value.split("#", 2);
463
+ return {
464
+ code,
465
+ state
466
+ };
467
+ }
468
+ if (value.includes("code=")) {
469
+ const params = new URLSearchParams(value);
470
+ return {
471
+ code: params.get("code") ?? void 0,
472
+ state: params.get("state") ?? void 0
473
+ };
474
+ }
475
+ return { code: value };
476
+ }
477
+ function decodeJwt(token) {
478
+ try {
479
+ const parts = token.split(".");
480
+ if (parts.length !== 3) return null;
481
+ const payload = parts[1] ?? "";
482
+ const decoded = atob(payload);
483
+ return JSON.parse(decoded);
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+ function getAccountId(accessToken) {
489
+ const accountId = (decodeJwt(accessToken)?.[JWT_CLAIM_PATH])?.chatgpt_account_id;
490
+ return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
491
+ }
492
+ async function exchangeAuthorizationCode(code, verifier, redirectUri) {
493
+ const response = await fetch(TOKEN_URL, {
494
+ method: "POST",
495
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
496
+ body: new URLSearchParams({
497
+ grant_type: "authorization_code",
498
+ client_id: CLIENT_ID,
499
+ code,
500
+ code_verifier: verifier,
501
+ redirect_uri: redirectUri
502
+ })
503
+ });
504
+ if (!response.ok) {
505
+ const text = await response.text().catch(() => "");
506
+ return {
507
+ type: "failed",
508
+ message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`
509
+ };
510
+ }
511
+ const json = await response.json();
512
+ if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") return {
513
+ type: "failed",
514
+ message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`
515
+ };
516
+ return {
517
+ type: "success",
518
+ access: json.access_token,
519
+ refresh: json.refresh_token,
520
+ expires: Date.now() + json.expires_in * 1e3
521
+ };
522
+ }
523
+ async function loginOpenAICodexWithCustomPage(options) {
524
+ const { verifier, challenge } = await generatePkce();
525
+ const state = createState();
526
+ const originator = options.originator ?? "pi";
527
+ const server = await startCallbackServer({
528
+ port: CALLBACK_PORT,
529
+ path: CALLBACK_PATH,
530
+ expectedState: state,
531
+ providerName: PROVIDER_NAME,
532
+ renderPage: options.renderPage,
533
+ successMessage: "OpenAI authentication completed. You can close this window.",
534
+ onListenError: "resolveWithStub"
535
+ });
536
+ const authUrl = new URL(AUTHORIZE_URL);
537
+ authUrl.searchParams.set("response_type", "code");
538
+ authUrl.searchParams.set("client_id", CLIENT_ID);
539
+ authUrl.searchParams.set("redirect_uri", server.redirectUri);
540
+ authUrl.searchParams.set("scope", SCOPE);
541
+ authUrl.searchParams.set("code_challenge", challenge);
542
+ authUrl.searchParams.set("code_challenge_method", "S256");
543
+ authUrl.searchParams.set("state", state);
544
+ authUrl.searchParams.set("id_token_add_organizations", "true");
545
+ authUrl.searchParams.set("codex_cli_simplified_flow", "true");
546
+ authUrl.searchParams.set("originator", originator);
547
+ options.onAuth({
548
+ url: authUrl.toString(),
549
+ instructions: "A browser window should open. Complete login to finish."
550
+ });
551
+ let code;
552
+ try {
553
+ if (options.onManualCodeInput) {
554
+ let manualCode;
555
+ let manualError;
556
+ const manualPromise = options.onManualCodeInput().then((input) => {
557
+ manualCode = input;
558
+ server.cancelWait();
559
+ }).catch((err) => {
560
+ manualError = err instanceof Error ? err : new Error(String(err));
561
+ server.cancelWait();
562
+ });
563
+ const result = await server.waitForCode();
564
+ if (manualError) throw manualError;
565
+ if (result?.code) code = result.code;
566
+ else if (manualCode) {
567
+ const parsed = parseAuthorizationInput(manualCode);
568
+ if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
569
+ code = parsed.code;
570
+ }
571
+ if (!code) {
572
+ await manualPromise;
573
+ if (manualError) throw manualError;
574
+ if (manualCode) {
575
+ const parsed = parseAuthorizationInput(manualCode);
576
+ if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
577
+ code = parsed.code;
578
+ }
579
+ }
580
+ } else {
581
+ const result = await server.waitForCode();
582
+ if (result?.code) code = result.code;
583
+ }
584
+ if (!code) {
585
+ const parsed = parseAuthorizationInput(await options.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }));
586
+ if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
587
+ code = parsed.code;
588
+ }
589
+ if (!code) throw new Error("Missing authorization code");
590
+ const tokenResult = await exchangeAuthorizationCode(code, verifier, server.redirectUri);
591
+ if (tokenResult.type !== "success") throw new Error(tokenResult.message);
592
+ const accountId = getAccountId(tokenResult.access);
593
+ if (!accountId) throw new Error("Failed to extract accountId from token");
594
+ return {
595
+ access: tokenResult.access,
596
+ refresh: tokenResult.refresh,
597
+ expires: tokenResult.expires,
598
+ accountId
599
+ };
600
+ } finally {
601
+ server.close();
602
+ }
603
+ }
604
+ /**
605
+ * Build an `OAuthProviderInterface` that behaves identically to pi-ai's
606
+ * `openaiCodexOAuthProvider` except for the callback page HTML.
607
+ */
608
+ function createOpenAICodexOAuthProviderWithCustomPage(renderPage) {
609
+ return {
610
+ id: "openai-codex",
611
+ name: "ChatGPT Plus/Pro (Codex Subscription)",
612
+ usesCallbackServer: true,
613
+ async login(callbacks) {
614
+ return loginOpenAICodexWithCustomPage({
615
+ renderPage,
616
+ onAuth: callbacks.onAuth,
617
+ onPrompt: callbacks.onPrompt,
618
+ onProgress: callbacks.onProgress,
619
+ onManualCodeInput: callbacks.onManualCodeInput
620
+ });
621
+ },
622
+ async refreshToken(credentials) {
623
+ return refreshOpenAICodexToken(credentials.refresh);
624
+ },
625
+ getApiKey(credentials) {
626
+ return credentials.access;
627
+ }
628
+ };
629
+ }
630
+ //#endregion
631
+ //#region src/chat/oauth-page/render.ts
632
+ function escapeHtml(value) {
633
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
634
+ }
635
+ /**
636
+ * Default zidane-themed page. Visually neutral but distinguishable from
637
+ * pi-ai's stock page — dark background, mono headings, no logo (host can
638
+ * pass a custom renderer to add one).
639
+ */
640
+ const renderDefaultCallbackPage = (page) => {
641
+ const heading = escapeHtml(page.kind === "success" ? `Signed in to ${page.provider}` : `Could not sign in to ${page.provider}`);
642
+ const message = escapeHtml(page.message);
643
+ const details = page.details ? escapeHtml(page.details) : void 0;
644
+ return `<!doctype html>
645
+ <html lang="en">
646
+ <head>
647
+ <meta charset="utf-8" />
648
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
649
+ <title>${heading}</title>
650
+ <style>
651
+ :root {
652
+ --text: #f4f4f5;
653
+ --text-dim: #a1a1aa;
654
+ --accent: ${page.kind === "success" ? "#22d3ee" : "#f87171"};
655
+ --page-bg: #0a0a0a;
656
+ --panel-bg: #131316;
657
+ --border: #27272a;
658
+ --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
659
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
660
+ }
661
+ * { box-sizing: border-box; }
662
+ html { color-scheme: dark; }
663
+ body {
664
+ margin: 0;
665
+ min-height: 100vh;
666
+ display: flex;
667
+ align-items: center;
668
+ justify-content: center;
669
+ padding: 24px;
670
+ background: var(--page-bg);
671
+ color: var(--text);
672
+ font-family: var(--font-sans);
673
+ }
674
+ main {
675
+ width: 100%;
676
+ max-width: 520px;
677
+ padding: 32px;
678
+ background: var(--panel-bg);
679
+ border: 1px solid var(--border);
680
+ border-radius: 12px;
681
+ }
682
+ .label {
683
+ font-family: var(--font-mono);
684
+ font-size: 12px;
685
+ letter-spacing: 0.08em;
686
+ text-transform: uppercase;
687
+ color: var(--accent);
688
+ margin-bottom: 12px;
689
+ }
690
+ h1 {
691
+ margin: 0 0 12px;
692
+ font-size: 22px;
693
+ font-weight: 600;
694
+ letter-spacing: -0.01em;
695
+ color: var(--text);
696
+ }
697
+ p {
698
+ margin: 0;
699
+ line-height: 1.6;
700
+ color: var(--text-dim);
701
+ font-size: 14px;
702
+ }
703
+ .details {
704
+ margin-top: 18px;
705
+ padding: 12px 14px;
706
+ background: var(--page-bg);
707
+ border: 1px solid var(--border);
708
+ border-radius: 8px;
709
+ font-family: var(--font-mono);
710
+ font-size: 12px;
711
+ color: var(--text-dim);
712
+ white-space: pre-wrap;
713
+ word-break: break-word;
714
+ }
715
+ </style>
716
+ </head>
717
+ <body>
718
+ <main>
719
+ <div class="label">zidane · OAuth · ${escapeHtml(page.provider)}</div>
720
+ <h1>${heading}</h1>
721
+ <p>${message}</p>
722
+ ${details ? `<div class="details">${details}</div>` : ""}
723
+ </main>
724
+ </body>
725
+ </html>`;
726
+ };
727
+ //#endregion
728
+ //#region src/chat/oauth-page/index.ts
729
+ /**
730
+ * Bundle helper — returns both Anthropic + OpenAI Codex providers wired
731
+ * with the same renderer. Hosts that want different renderers per provider
732
+ * should call the individual `create…WithCustomPage` factories instead.
733
+ */
734
+ function createCustomCallbackOAuthProviders(renderPage) {
735
+ return {
736
+ anthropic: createAnthropicOAuthProviderWithCustomPage(renderPage),
737
+ openaiCodex: createOpenAICodexOAuthProviderWithCustomPage(renderPage)
738
+ };
739
+ }
740
+ let cursorRegistered = false;
741
+ /**
742
+ * Register Cursor with pi-ai's OAuth registry.
743
+ *
744
+ * Unlike Anthropic / Codex, Cursor is **not** built into pi-ai, so
745
+ * `getOAuthApiKey('cursor', …)` (used by `resolveOAuthApiKey` for lazy
746
+ * token refresh) would throw "Unknown OAuth provider" until we register it.
747
+ * Call once at startup from both the CLI auth path and the TUI. Idempotent.
257
748
  */
258
- function compactPath(path, maxWidth, home) {
259
- const h = home ?? homedir();
260
- let display = path;
261
- if (h) {
262
- if (path === h) display = "~";
263
- else if (path.startsWith(`${h}/`)) display = `~${path.slice(h.length)}`;
749
+ function registerCursorOAuthProvider() {
750
+ const provider = createCursorOAuthProvider();
751
+ if (!cursorRegistered) {
752
+ registerOAuthProvider(provider);
753
+ cursorRegistered = true;
264
754
  }
265
- if (maxWidth !== void 0 && maxWidth > 1 && display.length > maxWidth) return `…${display.slice(display.length - maxWidth + 1)}`;
266
- return display;
755
+ return provider;
756
+ }
757
+ //#endregion
758
+ //#region src/chat/providers.ts
759
+ /**
760
+ * pi-ai's stock Anthropic + Codex OAuth providers bake their own callback
761
+ * HTML; we route both through `renderDefaultCallbackPage` so the post-redirect
762
+ * page matches zidane's theme. The override lives in `src/chat/oauth-page/`
763
+ * — see that folder's `index.ts` for the deletion path once pi-ai exposes
764
+ * a `renderCallbackPage` hook upstream.
765
+ */
766
+ const { anthropic: anthropicOAuthProvider, openaiCodex: openaiCodexOAuthProvider } = createCustomCallbackOAuthProviders(renderDefaultCallbackPage);
767
+ registerCursorOAuthProvider();
768
+ /** Convenience accessor — returns `credentialFileKey ?? key`. */
769
+ function credKeyOf(desc) {
770
+ return desc.credentialFileKey ?? desc.key;
771
+ }
772
+ /** Convenience accessor — returns `piProviderId ?? key`. */
773
+ function piIdOf(desc) {
774
+ return desc.piProviderId ?? desc.key;
775
+ }
776
+ const anthropicDescriptor = {
777
+ key: "anthropic",
778
+ label: "Anthropic",
779
+ factory: anthropic,
780
+ defaultModel: "claude-opus-4-8",
781
+ envKey: "ANTHROPIC_API_KEY",
782
+ apiKeyPlaceholder: "sk-ant-…",
783
+ oauthProvider: anthropicOAuthProvider,
784
+ oauthHint: "Claude Pro/Max subscription",
785
+ extraModels: ANTHROPIC_EXTRA_MODELS,
786
+ optionsFor: (id) => FAST_MODE_OPTIONS[id] ? [FAST_MODE_OPTIONS[id]] : void 0
787
+ };
788
+ const openaiDescriptor = {
789
+ key: "openai",
790
+ label: "OpenAI",
791
+ factory: openai,
792
+ defaultModel: "gpt-5.5",
793
+ envKey: "OPENAI_CODEX_API_KEY",
794
+ credentialFileKey: "openai-codex",
795
+ piProviderId: "openai-codex",
796
+ apiKeyPlaceholder: "sk-… or eyJ… (Codex)",
797
+ oauthProvider: openaiCodexOAuthProvider
798
+ };
799
+ const openrouterDescriptor = {
800
+ key: "openrouter",
801
+ label: "OpenRouter",
802
+ factory: openrouter,
803
+ defaultModel: "anthropic/claude-sonnet-4-6",
804
+ envKey: "OPENROUTER_API_KEY",
805
+ apiKeyPlaceholder: "sk-or-…"
806
+ };
807
+ const cerebrasDescriptor = {
808
+ key: "cerebras",
809
+ label: "Cerebras",
810
+ factory: cerebras,
811
+ defaultModel: "zai-glm-4.7",
812
+ envKey: "CEREBRAS_API_KEY",
813
+ apiKeyPlaceholder: "csk-…"
814
+ };
815
+ /**
816
+ * Conservative context-window assumption for a user-configured local model.
817
+ * Local runtimes expose no reliable per-model metadata over the OpenAI-compat
818
+ * `/v1/models` endpoint (it returns ids, not context sizes), so we pick a
819
+ * floor that fits the smallest mainstream OSS models (Llama 3.x 8B, Qwen2.5)
820
+ * without over-promising. The footer's context indicator and auto-compaction
821
+ * lean on this — under-estimating is the safe direction (compact early rather
822
+ * than overflow the server's real window).
823
+ *
824
+ * Users running larger-context models (e.g. a 128K Qwen) can raise it via the
825
+ * `LOCAL_LLM_CONTEXT_WINDOW` env var / customField, which supports a single
826
+ * value or a per-model map (resolved by {@link resolveContextWindow}).
827
+ */
828
+ const LOCAL_DEFAULT_CONTEXT_WINDOW = 8192;
829
+ /**
830
+ * Local OpenAI-compatible LLM (Ollama, vLLM, LM Studio, Lemonade, llama.cpp).
831
+ *
832
+ * No fixed base URL or model catalogue — both come from the user via
833
+ * `customFields`. No `envKey`, so the wizard skips the API-key prompt and
834
+ * treats the customFields-only credential as the auth signal (see
835
+ * `applyApiKeyEnv` + `detectAuth`).
836
+ *
837
+ * Vision / cache / reasoning are off by default in the factory — flip them
838
+ * by passing a custom descriptor that calls `local({ capabilities })`.
839
+ *
840
+ * `models` is a getter, not a static list: there's no fixed catalogue to ship,
841
+ * but once the user has configured a default model (mirrored into
842
+ * `LOCAL_LLM_DEFAULT_MODEL` by `applyApiKeyEnv`), we surface it as a
843
+ * single-entry list so the model picker + footer aren't empty. Resolved at
844
+ * access time because the env var is populated at TUI launch, after this
845
+ * module loads. Empty list when unconfigured — the picker hides, the user
846
+ * types a slug at the prompt as before.
847
+ */
848
+ const localDescriptor = {
849
+ key: "local",
850
+ label: "Local LLM",
851
+ factory: local,
852
+ get models() {
853
+ const configured = process.env.LOCAL_LLM_DEFAULT_MODEL?.trim();
854
+ if (!configured) return [];
855
+ return [{
856
+ id: configured,
857
+ name: configured,
858
+ contextWindow: resolveContextWindow(process.env.LOCAL_LLM_CONTEXT_WINDOW, configured) ?? LOCAL_DEFAULT_CONTEXT_WINDOW,
859
+ input: ["text"]
860
+ }];
861
+ },
862
+ customFields: [
863
+ {
864
+ key: "baseURL",
865
+ label: "Base URL",
866
+ envVar: "LOCAL_LLM_BASE_URL",
867
+ placeholder: "http://localhost:11434/v1",
868
+ hint: "Where your local LLM server is reachable. Examples: Ollama → http://localhost:11434/v1, vLLM → http://localhost:8000/v1, LM Studio → http://localhost:1234/v1.",
869
+ required: true
870
+ },
871
+ {
872
+ key: "apiKey",
873
+ label: "API key",
874
+ envVar: "LOCAL_LLM_API_KEY",
875
+ placeholder: "leave blank if your server is unauthenticated"
876
+ },
877
+ {
878
+ key: "model",
879
+ label: "Default model",
880
+ envVar: "LOCAL_LLM_DEFAULT_MODEL",
881
+ placeholder: "e.g. llama3.1:8b, qwen2.5-coder, mistral-small",
882
+ hint: "Model id your server exposes. Leave blank to pick later from the model picker."
883
+ },
884
+ {
885
+ key: "contextWindow",
886
+ label: "Context window (tokens)",
887
+ envVar: "LOCAL_LLM_CONTEXT_WINDOW",
888
+ placeholder: "e.g. 32768 · or per-model: llama3.1:8b=32768, qwen2.5-coder=128k, 64k",
889
+ hint: "Context length(s) for your local model(s) — local runtimes don't advertise this, so set it here to enable the context indicator and auto-compaction. Use a single value (32768, 128k) for all models, or a comma-separated map of `model=window` with an optional bare value as the fallback. Press"
890
+ }
891
+ ],
892
+ contextWindowEnvVar: "LOCAL_LLM_CONTEXT_WINDOW"
893
+ };
894
+ /**
895
+ * Default provider registry. Passed verbatim when `runTui` is invoked without
896
+ * an explicit `providers` option. Hosts that want to override per-provider
897
+ * metadata can spread this and replace specific entries:
898
+ *
899
+ * ```ts
900
+ * runTui({ providers: { ...BUILTIN_PROVIDERS, anthropic: myOwnAnthropicDescriptor } })
901
+ * ```
902
+ *
903
+ * `cursor` is intentionally NOT registered here: OAuth works, but inference
904
+ * isn't wired (`cursor.stream()` throws — Cursor speaks a protobuf/HTTP-2
905
+ * agent protocol, not OpenAI-compat). Shipping it in the picker only lets users
906
+ * select a model that errors every turn. The descriptor stays exported so a
907
+ * host can opt in (`{ ...BUILTIN_PROVIDERS, cursor: cursorDescriptor }`) once
908
+ * the transport lands — re-add it here at that point.
909
+ */
910
+ const BUILTIN_PROVIDERS = {
911
+ anthropic: anthropicDescriptor,
912
+ openai: openaiDescriptor,
913
+ openrouter: openrouterDescriptor,
914
+ cerebras: cerebrasDescriptor,
915
+ local: localDescriptor
916
+ };
917
+ /**
918
+ * Resolve the model list for a given provider. Honors `descriptor.models`
919
+ * when set; otherwise queries pi-ai via `descriptor.piProviderId`. Returns
920
+ * `[]` for descriptors with no known mapping (custom providers without a
921
+ * model list) — callers should hide the model picker in that case.
922
+ */
923
+ function modelsForDescriptor(descriptor) {
924
+ if (descriptor.models) return descriptor.models;
925
+ let bundled;
926
+ try {
927
+ bundled = getModels(piIdOf(descriptor));
928
+ } catch {
929
+ bundled = [];
930
+ }
931
+ if (descriptor.extraModels?.length) {
932
+ const bundledIds = new Set(bundled.map((m) => m.id));
933
+ bundled = [...descriptor.extraModels.filter((m) => !bundledIds.has(m.id)), ...bundled];
934
+ }
935
+ return descriptor.optionsFor ? bundled.map((m) => decorateOptions(m, descriptor)) : bundled;
936
+ }
937
+ /**
938
+ * Attach descriptor-resolved {@link ModelOption}s to a model that doesn't
939
+ * already declare its own. Pure / non-mutating — returns the input untouched
940
+ * when the model has options already or the descriptor resolves none.
941
+ */
942
+ function decorateOptions(model, descriptor) {
943
+ if (model.options?.length || !descriptor.optionsFor) return model;
944
+ const options = descriptor.optionsFor(model.id);
945
+ return options?.length ? {
946
+ ...model,
947
+ options
948
+ } : model;
949
+ }
950
+ /**
951
+ * Resolve a single model's metadata via the descriptor's model source.
952
+ * Mirrors {@link modelsForDescriptor} routing: descriptor's own list wins,
953
+ * pi-ai's registry is the fallback. Returns `null` when the model isn't
954
+ * known.
955
+ */
956
+ function getModelInfo(descriptor, modelId) {
957
+ if (descriptor.models) return descriptor.models.find((m) => m.id === modelId) ?? null;
958
+ try {
959
+ const bundled = getModel(piIdOf(descriptor), modelId);
960
+ if (bundled) return decorateOptions(bundled, descriptor);
961
+ } catch {}
962
+ const extra = descriptor.extraModels?.find((m) => m.id === modelId);
963
+ return extra ? decorateOptions(extra, descriptor) : null;
964
+ }
965
+ /**
966
+ * Parse a user-supplied context-window string into a token count.
967
+ *
968
+ * Accepts a plain integer (`"32768"`), or a number with a `k`/`m` suffix
969
+ * (×1_000 / ×1_000_000, case-insensitive, optional space) — so `"128k"`
970
+ * yields `128_000`, matching how the footer renders windows (`fmtTokens`).
971
+ * Fractional values are floored (`"1.5k"` → `1500`). Returns `null` for
972
+ * empty, malformed, zero, or negative input so callers fall through to
973
+ * "window unknown" rather than a bogus ceiling.
974
+ */
975
+ function parseContextWindow(raw) {
976
+ if (raw == null) return null;
977
+ const trimmed = raw.trim();
978
+ if (trimmed.length === 0) return null;
979
+ const match = /^(\d+(?:\.\d+)?)\s*([km])?$/i.exec(trimmed);
980
+ if (!match) return null;
981
+ const suffix = match[2]?.toLowerCase();
982
+ const mult = suffix === "k" ? 1e3 : suffix === "m" ? 1e6 : 1;
983
+ const value = Math.floor(Number.parseFloat(match[1]) * mult);
984
+ return value >= 1 ? value : null;
985
+ }
986
+ /**
987
+ * Resolve a per-model context window from a user-supplied spec string.
988
+ *
989
+ * A spec is a comma-separated list of entries. An entry is either:
990
+ * - `model=window` — a per-model override (split on the **first** `=`, so
991
+ * model ids containing `:` or `/` survive), or
992
+ * - a bare `window` — the fallback applied to any model not named above.
993
+ *
994
+ * Windows are parsed by {@link parseContextWindow} (plain int or k/m suffix).
995
+ * Whitespace around entries and the `=` is ignored; malformed entries are
996
+ * skipped rather than poisoning the whole spec; a later duplicate key wins.
997
+ *
998
+ * Lookup order for `modelId`: exact map entry → bare fallback → `null`. A
999
+ * lone bare value (`"32768"`) therefore behaves as a single global window,
1000
+ * keeping the simple single-model config working.
1001
+ *
1002
+ * @example resolveContextWindow('llama3.1:8b=32768, qwen=128k, 64k', 'qwen') // 128000
1003
+ */
1004
+ function resolveContextWindow(spec, modelId) {
1005
+ if (spec == null) return null;
1006
+ const overrides = /* @__PURE__ */ new Map();
1007
+ let fallback = null;
1008
+ for (const entry of spec.split(",")) {
1009
+ const eq = entry.indexOf("=");
1010
+ if (eq === -1) {
1011
+ const bare = parseContextWindow(entry);
1012
+ if (bare != null) fallback = bare;
1013
+ continue;
1014
+ }
1015
+ const key = entry.slice(0, eq).trim();
1016
+ const window = parseContextWindow(entry.slice(eq + 1));
1017
+ if (key.length > 0 && window != null) overrides.set(key, window);
1018
+ }
1019
+ return overrides.get(modelId) ?? fallback;
1020
+ }
1021
+ /**
1022
+ * Look up the model's max context window via the descriptor's model source.
1023
+ * Falls back to {@link ProviderDescriptor.contextWindowEnvVar} (parsed via
1024
+ * {@link resolveContextWindow}, which supports a per-model map) when the
1025
+ * registry has nothing — this is how local LLMs, whose runtimes don't
1026
+ * advertise a window, get one. Returns `null` when neither source resolves a
1027
+ * value (custom slugs, providers without a registry or a configured window);
1028
+ * callers should hide the context indicator and skip auto-compaction then.
1029
+ */
1030
+ function getContextWindow(descriptor, modelId) {
1031
+ const fromRegistry = getModelInfo(descriptor, modelId)?.contextWindow;
1032
+ if (fromRegistry != null) return fromRegistry;
1033
+ if (descriptor.contextWindowEnvVar) return resolveContextWindow(process.env[descriptor.contextWindowEnvVar], modelId);
1034
+ return null;
1035
+ }
1036
+ /**
1037
+ * Reserved output budget subtracted from the raw context window when
1038
+ * computing the effective ceiling for compaction / warning thresholds.
1039
+ *
1040
+ * Aligned with Claude Code's `COMPACT_MAX_OUTPUT_TOKENS = 20_000`
1041
+ * (`utils/context.ts:12`) — covers p99.99 of summary + response output.
1042
+ * A turn that consumed N input tokens leaves `effectiveWindow - N`
1043
+ * headroom for the next prompt's response; once headroom shrinks
1044
+ * below the threshold, auto-compaction fires.
1045
+ */
1046
+ const OUTPUT_RESERVE_TOKENS = 2e4;
1047
+ /**
1048
+ * Effective context window — what the next user turn can actually pack
1049
+ * before the provider reserves space for the assistant's reply. Equals
1050
+ * `rawWindow - OUTPUT_RESERVE_TOKENS`, clamped to `>= 1` so a tiny
1051
+ * (or absurd) window doesn't yield zero / negative thresholds.
1052
+ *
1053
+ * Used by both the footer's context indicator and the auto-compact
1054
+ * trigger so the two surfaces agree on "how full are we, really".
1055
+ * Pass `null` through unchanged so callers can pipe `getContextWindow`
1056
+ * directly without an intermediate check.
1057
+ */
1058
+ function effectiveContextWindow(rawWindow) {
1059
+ if (rawWindow === null) return null;
1060
+ return Math.max(1, rawWindow - OUTPUT_RESERVE_TOKENS);
1061
+ }
1062
+ /**
1063
+ * Whether the given model exposes a reasoning / extended-thinking knob.
1064
+ * Drives the TUI's effort picker visibility — the `ctrl+n` shortcut and
1065
+ * the bottom-bar `effortName` segment only surface when this is `true`.
1066
+ * Returns `false` for unknown models (no registry entry → no advertised
1067
+ * capability).
1068
+ */
1069
+ function modelSupportsReasoning(descriptor, modelId) {
1070
+ return getModelInfo(descriptor, modelId)?.reasoning === true;
1071
+ }
1072
+ /**
1073
+ * Custom {@link ModelOption}s the given model supports (e.g. fast mode), or `[]`
1074
+ * when none. Drives the TUI/GUI options picker visibility — surfaces only when
1075
+ * non-empty.
1076
+ */
1077
+ function modelOptionsFor(descriptor, modelId) {
1078
+ return getModelInfo(descriptor, modelId)?.options ?? [];
1079
+ }
1080
+ /**
1081
+ * Normalize a model-options blob to an enabled-only `{ id: true }` map.
1082
+ *
1083
+ * Single source of truth shared by every surface that reads or persists model
1084
+ * options — the TUI run path, the GUI engine reader, and the GUI IPC handlers.
1085
+ * Tolerant of arbitrary input (persisted JSON metadata may be anything): a
1086
+ * non-object → `{}`, and only entries strictly equal to `true` survive. This
1087
+ * keeps the persisted shape minimal (no `{ fast: false }` noise) and means
1088
+ * callers never forward a disabled option to `agent.run`.
1089
+ */
1090
+ function enabledModelOptions(raw) {
1091
+ if (!raw || typeof raw !== "object") return {};
1092
+ const out = {};
1093
+ for (const [id, on] of Object.entries(raw)) if (on === true) out[id] = true;
1094
+ return out;
1095
+ }
1096
+ /**
1097
+ * Resolve a model's remembered options for a (re)pick: keep only options the
1098
+ * model still declares AND that are enabled, dropping any that no longer apply
1099
+ * (e.g. switching away from a model that supported `fast`). Returns `undefined`
1100
+ * when nothing applies so callers can omit the field entirely.
1101
+ *
1102
+ * Shared by the TUI pick path and the launch-time resume path so "which options
1103
+ * survive a model switch / relaunch" is decided in exactly one place.
1104
+ */
1105
+ function restoreModelOptions(descriptor, modelId, remembered) {
1106
+ const validIds = new Set(modelOptionsFor(descriptor, modelId).map((o) => o.id));
1107
+ if (validIds.size === 0) return void 0;
1108
+ const enabled = enabledModelOptions(remembered?.[modelId]);
1109
+ const filtered = {};
1110
+ for (const id of Object.keys(enabled)) if (validIds.has(id)) filtered[id] = true;
1111
+ return Object.keys(filtered).length > 0 ? filtered : void 0;
267
1112
  }
268
1113
  //#endregion
269
1114
  //#region src/tools/read-state.ts
@@ -686,7 +1531,7 @@ function buildPersistedStub(input) {
686
1531
  const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output, PERSISTENCE_PREVIEW_BYTES);
687
1532
  const previewMarker = previewSlice.length < input.output.length ? `\n…(${input.originalBytes - previewBytes} more bytes in persisted file)` : "";
688
1533
  return [
689
- `${PERSISTED_STUB_PREFIX}${escapeAttr(input.toolName)}" bytes="${input.originalBytes}" path="${escapeAttr(input.persistedPath)}">`,
1534
+ `${PERSISTED_STUB_PREFIX}${escapeXml(input.toolName)}" bytes="${input.originalBytes}" path="${escapeXml(input.persistedPath)}">`,
690
1535
  `${previewSlice}${previewMarker}`,
691
1536
  "</persisted-output>"
692
1537
  ].join("\n");
@@ -739,14 +1584,6 @@ async function writeAtomic(path, content) {
739
1584
  }
740
1585
  }
741
1586
  /**
742
- * Byte length of a UTF-8 string. `Buffer.byteLength` is the canonical
743
- * answer; pulling it through a small helper keeps `node:buffer` out of
744
- * call sites that don't otherwise need it.
745
- */
746
- function byteLength(text) {
747
- return Buffer.byteLength(text, "utf8");
748
- }
749
- /**
750
1587
  * Take the first `cap` bytes of `text` without splitting a UTF-8
751
1588
  * codepoint. Returns the substring AND its exact UTF-8 byte length so
752
1589
  * the caller doesn't repeat the byte walk (the truncation marker in
@@ -770,7 +1607,7 @@ function sliceFirstBytes(text, cap) {
770
1607
  slice: "",
771
1608
  bytes: 0
772
1609
  };
773
- const total = byteLength(text);
1610
+ const total = utf8ByteLength(text);
774
1611
  if (total <= cap) return {
775
1612
  slice: text,
776
1613
  bytes: total
@@ -778,7 +1615,7 @@ function sliceFirstBytes(text, cap) {
778
1615
  let bytes = 0;
779
1616
  let charIdx = 0;
780
1617
  for (const ch of text) {
781
- const chBytes = byteLength(ch);
1618
+ const chBytes = utf8ByteLength(ch);
782
1619
  if (bytes + chBytes > cap) break;
783
1620
  bytes += chBytes;
784
1621
  charIdx += ch.length;
@@ -788,18 +1625,6 @@ function sliceFirstBytes(text, cap) {
788
1625
  bytes
789
1626
  };
790
1627
  }
791
- /**
792
- * Escape `&`, `"`, and `<` for safe inclusion in an XML attribute. The
793
- * stub format embeds tool names and filesystem paths verbatim — both
794
- * theoretically could contain one of these (Windows paths can't, but a
795
- * tool alias is user-controlled). Keeping the attribute well-formed
796
- * prevents a malicious or unusual tool name from breaking the wrapper
797
- * parse. `&` is escaped first so the subsequent replacements don't
798
- * double-encode the entities we just emitted.
799
- */
800
- function escapeAttr(text) {
801
- return text.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
802
- }
803
1628
  //#endregion
804
1629
  //#region src/tools/validation.ts
805
1630
  const TRUE_STRINGS = new Set([
@@ -1857,6 +2682,7 @@ async function executeTurn(ctx, turn, priorUsage) {
1857
2682
  maxTokens: ctx.maxTokens ?? 16384,
1858
2683
  thinking: ctx.thinking,
1859
2684
  thinkingBudget: effectiveThinkingBudget,
2685
+ ...ctx.modelOptions ? { modelOptions: ctx.modelOptions } : {},
1860
2686
  cache: ctx.cache ?? true,
1861
2687
  signal: ctx.signal
1862
2688
  };
@@ -1917,6 +2743,22 @@ async function executeTurn(ctx, turn, priorUsage) {
1917
2743
  turnId
1918
2744
  });
1919
2745
  },
2746
+ onServerToolUse({ id, name, input }) {
2747
+ ctx.hooks.callHook("stream:server_tool_use", {
2748
+ id,
2749
+ name,
2750
+ input,
2751
+ turnId
2752
+ });
2753
+ },
2754
+ onServerToolResult({ toolUseId, toolName, content }) {
2755
+ ctx.hooks.callHook("stream:server_tool_result", {
2756
+ toolUseId,
2757
+ toolName,
2758
+ content,
2759
+ turnId
2760
+ });
2761
+ },
1920
2762
  onOAuthRefresh(refreshCtx) {
1921
2763
  return ctx.hooks.callHook("oauth:refresh", refreshCtx);
1922
2764
  }
@@ -2386,10 +3228,10 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
2386
3228
  let removeAbortListener;
2387
3229
  const cancellationPromise = new Promise((_, reject) => {
2388
3230
  if (perCallAbort.signal.aborted) {
2389
- reject(new Error(CANCELLED_BY_USER_SENTINEL));
3231
+ reject(/* @__PURE__ */ new Error(CANCELLED_BY_USER_SENTINEL));
2390
3232
  return;
2391
3233
  }
2392
- const onAbort = () => reject(new Error(CANCELLED_BY_USER_SENTINEL));
3234
+ const onAbort = () => reject(/* @__PURE__ */ new Error(CANCELLED_BY_USER_SENTINEL));
2393
3235
  perCallAbort.signal.addEventListener("abort", onAbort, { once: true });
2394
3236
  removeAbortListener = () => perCallAbort.signal.removeEventListener("abort", onAbort);
2395
3237
  });
@@ -2645,6 +3487,20 @@ async function executeToolBatch(ctx, toolCalls, turnId) {
2645
3487
  const drain = async () => {
2646
3488
  if (inFlight.size > 0) await Promise.all([...inFlight.values()]);
2647
3489
  };
3490
+ /**
3491
+ * Free a SINGLE slot: wait for the first in-flight call to settle rather
3492
+ * than the whole fleet. Used when an all-safe fleet hits `maxConcurrent` —
3493
+ * a sliding window dispatches the next safe call the moment ANY sibling
3494
+ * finishes, instead of stalling the entire batch on the slowest one (which
3495
+ * a full `drain()` would do). `dispatch()` never rejects (it captures both
3496
+ * outcomes into `results[]`), so `Promise.race` here can't throw. Each
3497
+ * dispatched promise carries a `.finally` that deletes its own slot from
3498
+ * `inFlight` BEFORE the promise resolves, so on return `inFlight.size` has
3499
+ * dropped by at least one and a dispatch is guaranteed to fit under the cap.
3500
+ */
3501
+ const waitForSlot = async () => {
3502
+ if (inFlight.size > 0) await Promise.race([...inFlight.values()]);
3503
+ };
2648
3504
  /** Whether every in-flight call is concurrency-safe. */
2649
3505
  const fleetAllSafe = () => {
2650
3506
  for (const idx of inFlight.keys()) if (!safe[idx]) return false;
@@ -2666,7 +3522,8 @@ async function executeToolBatch(ctx, toolCalls, turnId) {
2666
3522
  };
2667
3523
  try {
2668
3524
  for (let i = 0; i < N; i++) {
2669
- if (!safe[i] || !fleetAllSafe() || inFlight.size >= maxConcurrent) await drain();
3525
+ if (!safe[i] || !fleetAllSafe()) await drain();
3526
+ else if (inFlight.size >= maxConcurrent) await waitForSlot();
2670
3527
  if (ctx.signal.aborted) {
2671
3528
  await drain();
2672
3529
  fillUnstarted(i, INTERRUPT_MESSAGE_FOR_TOOL_USE);
@@ -3758,6 +4615,81 @@ function createToolSearchTool(options) {
3758
4615
  //#endregion
3759
4616
  //#region src/agent.ts
3760
4617
  /**
4618
+ * Single builder for a {@link ContextAssembly}, shared by the per-run snapshot
4619
+ * and the pre-run assembly so the two can't drift on optionality / field
4620
+ * ordering. Callers pass every piece (optional fields may be `undefined`); this
4621
+ * normalizes the spread-conditional shape both producers previously inlined.
4622
+ */
4623
+ function assembleContextSnapshot(input) {
4624
+ return {
4625
+ modelId: input.modelId,
4626
+ system: input.system,
4627
+ baseSystem: input.baseSystem,
4628
+ ...input.rulesBlock ? { rulesBlock: input.rulesBlock } : {},
4629
+ ...input.rulesFiles?.length ? { rulesFiles: input.rulesFiles } : {},
4630
+ disclosedTools: input.disclosedTools,
4631
+ deferredEntries: input.deferredEntries,
4632
+ ...input.mcpInstructions ? { mcpInstructions: input.mcpInstructions } : {},
4633
+ ...input.skillsCatalog ? { skillsCatalog: input.skillsCatalog } : {},
4634
+ ...input.subagentDefs ? { subagentDefs: input.subagentDefs } : {},
4635
+ wireTools: input.wireTools
4636
+ };
4637
+ }
4638
+ /**
4639
+ * Lazily serialize an assembly's tool inputs into the byte-sized buckets the
4640
+ * context breakdown consumes. Deferred so the per-run snapshot stays cheap —
4641
+ * the `JSON.stringify` here only runs when `getContextBreakdown()` is called.
4642
+ *
4643
+ * MCP grouping preserves first-seen server order; native tools stay flat. The
4644
+ * output is byte-identical to the eager partition it replaced.
4645
+ */
4646
+ function materializeAssemblyTools(assembly) {
4647
+ const toolsJson = [];
4648
+ const disclosedMcp = /* @__PURE__ */ new Map();
4649
+ for (const t of assembly.disclosedTools) {
4650
+ const json = JSON.stringify({
4651
+ name: t.name,
4652
+ description: t.description,
4653
+ inputSchema: t.inputSchema
4654
+ });
4655
+ if (t.mcpServer !== void 0) {
4656
+ const bucket = disclosedMcp.get(t.mcpServer) ?? [];
4657
+ bucket.push({
4658
+ name: t.name,
4659
+ json
4660
+ });
4661
+ disclosedMcp.set(t.mcpServer, bucket);
4662
+ } else toolsJson.push(json);
4663
+ }
4664
+ const deferredToolsJson = [];
4665
+ const deferredMcp = /* @__PURE__ */ new Map();
4666
+ for (const entry of assembly.deferredEntries) {
4667
+ const json = JSON.stringify({
4668
+ name: entry.name,
4669
+ description: entry.description,
4670
+ inputSchema: entry.inputSchema
4671
+ });
4672
+ if (entry.server !== void 0) {
4673
+ const bucket = deferredMcp.get(entry.server) ?? [];
4674
+ bucket.push({
4675
+ name: entry.name,
4676
+ json
4677
+ });
4678
+ deferredMcp.set(entry.server, bucket);
4679
+ } else deferredToolsJson.push(json);
4680
+ }
4681
+ const toGroups = (m) => [...m.entries()].map(([server, list]) => ({
4682
+ server,
4683
+ tools: list
4684
+ }));
4685
+ return {
4686
+ toolsJson,
4687
+ deferredToolsJson,
4688
+ mcpGroups: toGroups(disclosedMcp),
4689
+ deferredMcpGroups: toGroups(deferredMcp)
4690
+ };
4691
+ }
4692
+ /**
3761
4693
  * Authoritative list of hook event names. Kept in sync with `AgentHooks` at
3762
4694
  * compile time: the `satisfies` assertion below rejects any drift.
3763
4695
  */
@@ -3800,6 +4732,8 @@ const HOOK_EVENT_SET = new Set([
3800
4732
  "stream:text",
3801
4733
  "stream:end",
3802
4734
  "stream:thinking",
4735
+ "stream:server_tool_use",
4736
+ "stream:server_tool_result",
3803
4737
  "stream:error",
3804
4738
  "stream:retry",
3805
4739
  "oauth:refresh",
@@ -3822,6 +4756,8 @@ const HOOK_EVENT_SET = new Set([
3822
4756
  "child:stream:text",
3823
4757
  "child:stream:thinking",
3824
4758
  "child:stream:end",
4759
+ "child:stream:server_tool_use",
4760
+ "child:stream:server_tool_result",
3825
4761
  "child:stream:error",
3826
4762
  "child:tool:gate",
3827
4763
  "child:mcp:tool:gate",
@@ -3939,6 +4875,7 @@ function resolveBehavior(agentBehavior, runBehavior) {
3939
4875
  maxTotalTokens: runBehavior?.maxTotalTokens ?? agentBehavior?.maxTotalTokens,
3940
4876
  maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
3941
4877
  thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
4878
+ modelOptions: runBehavior?.modelOptions ?? agentBehavior?.modelOptions,
3942
4879
  schema: runBehavior?.schema ?? agentBehavior?.schema,
3943
4880
  cache: runBehavior?.cache ?? agentBehavior?.cache ?? true,
3944
4881
  toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget,
@@ -4120,29 +5057,43 @@ function renderMcpInstructionsSection(instructions) {
4120
5057
  return parts.join("\n");
4121
5058
  }
4122
5059
  /**
4123
- * Pick the next safe value for `runCounter` so `run_${++counter}` mints
4124
- * an id that doesn't collide with any runId already referenced by the
4125
- * sessionwhether the runId lives in `session.runs` or only in
4126
- * `session.turns[].runId`. The regex matches the canonical `run_<int>`
4127
- * shape minted by this module; any caller-supplied custom id schemes
4128
- * are ignored (they don't conflict with `run_N`).
4129
- *
4130
- * Returning 0 for a sessionless / clean session preserves the original
4131
- * "first run is run_1" semantics.
5060
+ * Set of runIds belonging to subagent (depth > 0) runs in a session. Used to
5061
+ * filter child-run turns out of the resumed conversation and out of
5062
+ * last-turn-usage accounting both need "everything a subagent produced",
5063
+ * keyed by `runId`. Returns an empty set for a sessionless agent.
4132
5064
  */
4133
- function initialRunCounter(session) {
4134
- if (!session) return 0;
4135
- let max = 0;
4136
- const consider = (id) => {
4137
- if (!id) return;
4138
- const m = /^run_(\d+)$/.exec(id);
4139
- if (!m) return;
4140
- const n = Number.parseInt(m[1], 10);
4141
- if (Number.isFinite(n) && n > max) max = n;
4142
- };
4143
- for (const r of session.runs) consider(r.id);
4144
- for (const t of session.turns) consider(t.runId);
4145
- return max;
5065
+ function childRunIdSet(session) {
5066
+ const ids = /* @__PURE__ */ new Set();
5067
+ for (const r of session?.runs ?? []) if ((r.depth ?? 0) > 0) ids.add(r.id);
5068
+ return ids;
5069
+ }
5070
+ /**
5071
+ * Validate `agent.run()`'s prompt/resume preconditions and compute the
5072
+ * unresolved-tool-filtered resume view (returned for reuse in the seed block).
5073
+ *
5074
+ * `prompt` is required unless resuming a session that already has turns. The
5075
+ * resume guard inspects the trailing turn AFTER filtering unresolved tool_uses
5076
+ * (L1) a session that ended on an orphan assistant turn (process death
5077
+ * mid-tool, `kill -9`) gets its trailing assistant dropped before the
5078
+ * "trailing must be user" check, so a recoverable session doesn't get a
5079
+ * spurious `cannot resume without prompt` error.
5080
+ *
5081
+ * Throws on an unrecoverable resume request; otherwise returns the filtered
5082
+ * turns (or `undefined` when the session has no turns).
5083
+ */
5084
+ function validateAndPrepareResume(session, prompt) {
5085
+ const hasSessionTurns = !!session && session.turns.length > 0;
5086
+ if (!prompt && !hasSessionTurns) throw new Error("prompt is required when no session with existing turns is provided");
5087
+ let resumeFilteredTurns;
5088
+ if (hasSessionTurns) resumeFilteredTurns = filterUnresolvedToolUses(session.turns);
5089
+ if (!prompt && resumeFilteredTurns) {
5090
+ const lastTurn = resumeFilteredTurns.at(-1);
5091
+ if (lastTurn && lastTurn.role !== "user") {
5092
+ const detail = detectTurnInterruption(resumeFilteredTurns) === "completed" ? "last turn is a completed assistant message" : "last turn is mid-stream assistant content";
5093
+ throw new Error(`cannot resume without prompt: ${detail}. Pass a prompt to agent.run({ prompt: … }).`);
5094
+ }
5095
+ }
5096
+ return resumeFilteredTurns;
4146
5097
  }
4147
5098
  function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, readState: agentReadState, skills: agentSkills, mcpConnector, eager, hooks: initialHooks, clock: agentClock }) {
4148
5099
  const hooks = createHooks();
@@ -4160,16 +5111,43 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4160
5111
  let running = false;
4161
5112
  let idleResolve;
4162
5113
  let idlePromise;
5114
+ function resetRunScope() {
5115
+ running = false;
5116
+ abortController = void 0;
5117
+ idleResolve?.();
5118
+ idlePromise = void 0;
5119
+ idleResolve = void 0;
5120
+ }
4163
5121
  const pendingTaskNotifications = /* @__PURE__ */ new Map();
4164
5122
  const pendingToolCancels = /* @__PURE__ */ new Map();
4165
5123
  let executionHandle = null;
4166
5124
  let mcpConnection = null;
5125
+ let lastContextAssembly = null;
4167
5126
  let mcpWarmupPromise = null;
4168
5127
  const allMcpServers = mcpServers ?? [];
4169
5128
  const steeringQueue = [];
4170
5129
  const followUpQueue = [];
4171
5130
  let conversationTurns = session?.turns.slice() ?? [];
4172
- let runCounter = initialRunCounter(session);
5131
+ let runCounter = 0;
5132
+ let scannedRuns = 0;
5133
+ let scannedTurns = 0;
5134
+ const considerRunId = (id) => {
5135
+ if (!id) return;
5136
+ const m = /^run_(\d+)$/.exec(id);
5137
+ if (!m) return;
5138
+ const n = Number.parseInt(m[1], 10);
5139
+ if (Number.isFinite(n) && n > runCounter) runCounter = n;
5140
+ };
5141
+ function syncRunCounter() {
5142
+ if (!session) return;
5143
+ if (session.runs.length < scannedRuns) scannedRuns = 0;
5144
+ if (session.turns.length < scannedTurns) scannedTurns = 0;
5145
+ for (let i = scannedRuns; i < session.runs.length; i++) considerRunId(session.runs[i].id);
5146
+ for (let i = scannedTurns; i < session.turns.length; i++) considerRunId(session.turns[i].runId);
5147
+ scannedRuns = session.runs.length;
5148
+ scannedTurns = session.turns.length;
5149
+ }
5150
+ syncRunCounter();
4173
5151
  const skillsConfig = agentSkills;
4174
5152
  const skillsEnabledValue = skillsConfig?.enabled;
4175
5153
  const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
@@ -4213,24 +5191,14 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4213
5191
  const skillActivationState = createSkillActivationState({ maxActive: skillsConfig?.maxActive });
4214
5192
  async function run(options) {
4215
5193
  if (running) throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
4216
- const hasSessionTurns = session && session.turns.length > 0;
4217
- if (!options.prompt && !hasSessionTurns) throw new Error("prompt is required when no session with existing turns is provided");
4218
- let resumeFilteredTurns;
4219
- if (hasSessionTurns) resumeFilteredTurns = filterUnresolvedToolUses(session.turns);
4220
- if (!options.prompt && resumeFilteredTurns) {
4221
- const lastTurn = resumeFilteredTurns.at(-1);
4222
- if (lastTurn && lastTurn.role !== "user") {
4223
- const detail = detectTurnInterruption(resumeFilteredTurns) === "completed" ? "last turn is a completed assistant message" : "last turn is mid-stream assistant content";
4224
- throw new Error(`cannot resume without prompt: ${detail}. Pass a prompt to agent.run({ prompt: … }).`);
4225
- }
4226
- }
5194
+ const resumeFilteredTurns = validateAndPrepareResume(session, options.prompt);
4227
5195
  const clock = options.clock ?? agentClock ?? DEFAULT_AGENT_CLOCK;
4228
5196
  let externalAbortListener;
4229
5197
  const externalSignal = options.signal;
4230
5198
  running = true;
4231
5199
  try {
4232
5200
  abortController = new AbortController();
4233
- runCounter = Math.max(runCounter, initialRunCounter(session));
5201
+ syncRunCounter();
4234
5202
  const runId = `run_${++runCounter}`;
4235
5203
  const promptLabel = typeof options.prompt === "string" ? options.prompt : Array.isArray(options.prompt) ? options.prompt.filter((p) => p.type === "text").map((p) => p.text).join("\n") : "";
4236
5204
  session?.startRun(runId, promptLabel, {
@@ -4306,8 +5274,10 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4306
5274
  const thinking = options.thinking ?? "off";
4307
5275
  const model = options.model ?? provider.meta.defaultModel;
4308
5276
  const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
4309
- const { maxConcurrentTools, maxTurns, maxCostUsd, maxTotalTokens, maxTokens, retry, thinkingBudget, schema, cache, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, surfaceMcpInstructions, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing, maxConsecutivePauseTurns } = resolvedBehavior;
5277
+ const { maxConcurrentTools, maxTurns, maxCostUsd, maxTotalTokens, maxTokens, retry, thinkingBudget, modelOptions: behaviorModelOptions, schema, cache, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, surfaceMcpInstructions, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing, maxConsecutivePauseTurns } = resolvedBehavior;
5278
+ const modelOptions = options.modelOptions ?? behaviorModelOptions;
4310
5279
  let system = options.system || agentSystem || "You are a helpful assistant.";
5280
+ const baseSystemForBreakdown = renderSystemForWire(system);
4311
5281
  if (skillsCatalog) system = appendStaticSection(system, skillsCatalog);
4312
5282
  const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? {
4313
5283
  ...sourceTools,
@@ -4368,14 +5338,20 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4368
5338
  initialUnlocked.add(toolSearchTool.spec.name);
4369
5339
  }
4370
5340
  const discoveryToolName = shouldInjectToolSearch ? "tool_search" : hostDefinedToolSearch ? toolAliases?.tool_search ?? "tool_search" : null;
4371
- if (disclosure.lazyEntries.length > 0) system = appendStaticSection(system, buildSearchableCatalog(disclosure.lazyEntries, { discoveryToolName }));
5341
+ let searchableCatalogText;
5342
+ if (disclosure.lazyEntries.length > 0) {
5343
+ searchableCatalogText = buildSearchableCatalog(disclosure.lazyEntries, { discoveryToolName });
5344
+ system = appendStaticSection(system, searchableCatalogText);
5345
+ }
4372
5346
  if (surfaceMcpInstructions && mcpConnection?.instructions && mcpConnection.instructions.size > 0) {
4373
5347
  const section = renderMcpInstructionsSection(mcpConnection.instructions);
4374
5348
  if (section.length > 0) system = appendStaticSection(system, section);
4375
5349
  }
4376
5350
  const aliasMaps = buildAliasMaps(toolAliases, Object.keys(tools));
4377
5351
  augmentMcpDoubleUnderscoreAliases(aliasMaps, Object.keys(tools));
5352
+ let formattedToolsCache = null;
4378
5353
  function buildFormattedTools() {
5354
+ if (formattedToolsCache && formattedToolsCache.tailLen === dynamicUnlockOrder.length) return formattedToolsCache.value;
4379
5355
  const specs = [];
4380
5356
  for (const t of Object.values(tools)) {
4381
5357
  if (!initialUnlocked.has(t.spec.name)) continue;
@@ -4394,12 +5370,52 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4394
5370
  inputSchema: t.spec.inputSchema
4395
5371
  });
4396
5372
  }
4397
- return specs.length > 0 ? provider.formatTools(specs) : [];
5373
+ const value = specs.length > 0 ? provider.formatTools(specs) : [];
5374
+ formattedToolsCache = {
5375
+ tailLen: dynamicUnlockOrder.length,
5376
+ value
5377
+ };
5378
+ return value;
4398
5379
  }
4399
5380
  const formattedTools = buildFormattedTools();
5381
+ {
5382
+ const disclosedTools = [];
5383
+ for (const t of Object.values(tools)) {
5384
+ if (!unlocked.has(t.spec.name)) continue;
5385
+ const wireName = aliasMaps.aliasByCanonical.get(t.spec.name) ?? t.spec.name;
5386
+ disclosedTools.push({
5387
+ name: wireName,
5388
+ description: t.spec.description || "",
5389
+ inputSchema: t.spec.inputSchema,
5390
+ ...mcpToolNames.has(t.spec.name) ? { mcpServer: "mcp" } : {}
5391
+ });
5392
+ }
5393
+ const deferredEntries = disclosure.lazyEntries.map((entry) => ({
5394
+ name: entry.name,
5395
+ description: entry.description || "",
5396
+ inputSchema: entry.inputSchema,
5397
+ ...entry.server ? { server: entry.server } : {}
5398
+ }));
5399
+ const mcpInstructionsText = surfaceMcpInstructions && mcpConnection?.instructions && mcpConnection.instructions.size > 0 ? renderMcpInstructionsSection(mcpConnection.instructions) : void 0;
5400
+ const rulesBlock = options.contextSections?.rulesBlock;
5401
+ const rulesFiles = options.contextSections?.rulesFiles;
5402
+ lastContextAssembly = assembleContextSnapshot({
5403
+ modelId: model,
5404
+ system: renderSystemForWire(system),
5405
+ baseSystem: baseSystemForBreakdown,
5406
+ rulesBlock,
5407
+ rulesFiles,
5408
+ disclosedTools,
5409
+ deferredEntries,
5410
+ mcpInstructions: mcpInstructionsText,
5411
+ skillsCatalog,
5412
+ subagentDefs: searchableCatalogText,
5413
+ wireTools: formattedTools
5414
+ });
5415
+ }
4400
5416
  const turns = [];
4401
5417
  if (session && session.turns.length > 0 && (session.runs.length > 0 || !options.prompt) && !options.parentRunId) {
4402
- const childRunIds = new Set(session.runs.filter((r) => (r.depth ?? 0) > 0).map((r) => r.id));
5418
+ const childRunIds = childRunIdSet(session);
4403
5419
  const resumed = childRunIds.size === 0 ? session.turns : session.turns.filter((t) => !t.runId || !childRunIds.has(t.runId));
4404
5420
  const filteredForRuntime = resumeFilteredTurns && resumed === session.turns ? resumeFilteredTurns : filterUnresolvedToolUses(resumed);
4405
5421
  turns.push(...filteredForRuntime);
@@ -4538,6 +5554,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4538
5554
  ...options.parentRunId ? { parentRunId: options.parentRunId } : {},
4539
5555
  depth: runDepth,
4540
5556
  thinkingBudget,
5557
+ ...modelOptions ? { modelOptions } : {},
4541
5558
  schema,
4542
5559
  cache,
4543
5560
  toolOutputBudget,
@@ -4646,20 +5663,12 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4646
5663
  unregisterTurnSync?.();
4647
5664
  unregisterToolResultsSync?.();
4648
5665
  for (const unregister of perRunUnregisters) unregister();
4649
- running = false;
4650
- abortController = void 0;
4651
5666
  steeringQueue.length = 0;
4652
5667
  followUpQueue.length = 0;
4653
- idleResolve?.();
4654
- idlePromise = void 0;
4655
- idleResolve = void 0;
5668
+ resetRunScope();
4656
5669
  }
4657
5670
  } finally {
4658
- running = false;
4659
- abortController = void 0;
4660
- idleResolve?.();
4661
- idlePromise = void 0;
4662
- idleResolve = void 0;
5671
+ resetRunScope();
4663
5672
  if (externalSignal && externalAbortListener) externalSignal.removeEventListener("abort", externalAbortListener);
4664
5673
  }
4665
5674
  }
@@ -4691,6 +5700,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4691
5700
  conversationTurns = [];
4692
5701
  steeringQueue.length = 0;
4693
5702
  followUpQueue.length = 0;
5703
+ lastContextAssembly = null;
4694
5704
  pendingTaskNotifications.clear();
4695
5705
  const cleared = skillActivationState.clear();
4696
5706
  for (const record of cleared) await hooks.callHook("skills:deactivate", {
@@ -4815,6 +5825,151 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4815
5825
  pendingTaskNotifications.clear();
4816
5826
  skillsCleanup();
4817
5827
  skillsCleanup = () => {};
5828
+ lastContextAssembly = null;
5829
+ }
5830
+ /**
5831
+ * Last parent-run `TurnUsage` from the in-memory conversation — mirrors
5832
+ * `lastContextSizeFromTurns` (chat/store) but reads the live agent turns and
5833
+ * returns the full usage so callers can break out cache read/write/input.
5834
+ * Skips subagent (depth > 0) and zero-usage turns.
5835
+ */
5836
+ function lastTurnUsage() {
5837
+ const childRunIds = childRunIdSet(session);
5838
+ for (let i = conversationTurns.length - 1; i >= 0; i--) {
5839
+ const turn = conversationTurns[i];
5840
+ if (turn.role !== "assistant" || !turn.usage) continue;
5841
+ if (turn.runId && childRunIds.has(turn.runId)) continue;
5842
+ if (effectiveInputFromTurn(turn.usage) === 0) continue;
5843
+ return turn.usage;
5844
+ }
5845
+ return null;
5846
+ }
5847
+ /**
5848
+ * Best-effort context assembly BEFORE the first run, so the breakdown is
5849
+ * available the moment the chat screen opens (no "send a message first").
5850
+ *
5851
+ * Resolves skills + MCP via `warmup()`, then assembles the same static pieces
5852
+ * `run()` captures — system prompt (with skills catalog + MCP instructions),
5853
+ * the wire tool specs, and MCP groupings. The disclosure subtleties don't
5854
+ * matter pre-run (nothing has been unlocked yet); we treat every native +
5855
+ * MCP tool as disclosed, matching the eager seed of a fresh run.
5856
+ */
5857
+ async function buildPreRunAssembly(modelOverride) {
5858
+ if (destroyed) return null;
5859
+ try {
5860
+ await ensureSkillsResolved();
5861
+ } catch {}
5862
+ const model = modelOverride ?? provider.meta.defaultModel;
5863
+ const base = agentSystem || "You are a helpful assistant.";
5864
+ const baseSystem = renderSystemForWire(base);
5865
+ let system = base;
5866
+ if (skillsCatalog) system = appendStaticSection(system, skillsCatalog);
5867
+ const baseTools = mcpConnection ? {
5868
+ ...sourceTools,
5869
+ ...mcpConnection.tools
5870
+ } : sourceTools;
5871
+ const mcpNames = new Set(mcpConnection ? Object.keys(mcpConnection.tools) : []);
5872
+ const disclosedTools = [];
5873
+ const nativeSpecs = [];
5874
+ const mcpSpecs = [];
5875
+ for (const t of Object.values(baseTools)) {
5876
+ const spec = {
5877
+ name: t.spec.name,
5878
+ description: t.spec.description || "",
5879
+ inputSchema: t.spec.inputSchema
5880
+ };
5881
+ if (mcpNames.has(t.spec.name)) mcpSpecs.push(spec);
5882
+ else nativeSpecs.push(spec);
5883
+ }
5884
+ for (const spec of nativeSpecs) disclosedTools.push({
5885
+ name: spec.name,
5886
+ description: spec.description || "",
5887
+ inputSchema: spec.inputSchema
5888
+ });
5889
+ for (const spec of mcpSpecs) disclosedTools.push({
5890
+ name: spec.name,
5891
+ description: spec.description || "",
5892
+ inputSchema: spec.inputSchema,
5893
+ mcpServer: "mcp"
5894
+ });
5895
+ const wireTools = nativeSpecs.length + mcpSpecs.length > 0 ? provider.formatTools([...nativeSpecs, ...mcpSpecs]) : [];
5896
+ const mcpInstructionsText = mcpConnection?.instructions && mcpConnection.instructions.size > 0 ? renderMcpInstructionsSection(mcpConnection.instructions) : void 0;
5897
+ if (mcpInstructionsText) system = appendStaticSection(system, mcpInstructionsText);
5898
+ return assembleContextSnapshot({
5899
+ modelId: model,
5900
+ system: renderSystemForWire(system),
5901
+ baseSystem,
5902
+ disclosedTools,
5903
+ deferredEntries: [],
5904
+ mcpInstructions: mcpInstructionsText,
5905
+ skillsCatalog,
5906
+ wireTools
5907
+ });
5908
+ }
5909
+ async function getContextBreakdown(opts) {
5910
+ if (destroyed) return null;
5911
+ const assembly = lastContextAssembly ?? await buildPreRunAssembly(opts?.model);
5912
+ if (!assembly) return null;
5913
+ const usage = lastTurnUsage();
5914
+ const used = effectiveInputFromTurn(usage);
5915
+ const effectiveWindow = opts?.effectiveWindow ?? used;
5916
+ const autocompactBuffer = opts?.compactThreshold !== void 0 && opts.compactThreshold > 0 && opts.compactThreshold < 1 ? Math.max(0, Math.round((1 - opts.compactThreshold) * effectiveWindow)) : opts?.autocompactBuffer ?? 2e4;
5917
+ let exact;
5918
+ if (typeof provider.countTokens === "function") {
5919
+ const BASELINE = ".";
5920
+ const dummy = [{
5921
+ role: "user",
5922
+ content: [{
5923
+ type: "text",
5924
+ text: "."
5925
+ }]
5926
+ }];
5927
+ const count = (system, tools) => provider.countTokens({
5928
+ model: assembly.modelId,
5929
+ system,
5930
+ tools,
5931
+ messages: dummy
5932
+ }, opts?.signal);
5933
+ try {
5934
+ const [base, sysProbe, sysToolsProbe] = await Promise.all([
5935
+ count(BASELINE, []),
5936
+ count(assembly.system, []),
5937
+ count(assembly.system, assembly.wireTools)
5938
+ ]);
5939
+ if (base !== null && sysProbe !== null) exact = {
5940
+ system: Math.max(0, sysProbe - base),
5941
+ ...sysToolsProbe !== null ? { systemAndTools: Math.max(0, sysToolsProbe - base) } : {}
5942
+ };
5943
+ } catch {
5944
+ exact = void 0;
5945
+ }
5946
+ }
5947
+ const materializedTools = materializeAssemblyTools(assembly);
5948
+ return buildContextBreakdown({
5949
+ modelId: assembly.modelId,
5950
+ system: assembly.system,
5951
+ baseSystem: assembly.baseSystem,
5952
+ ...assembly.rulesBlock ? { rulesBlock: assembly.rulesBlock } : {},
5953
+ ...assembly.rulesFiles?.length ? { rulesFiles: assembly.rulesFiles } : {},
5954
+ toolsJson: materializedTools.toolsJson,
5955
+ deferredToolsJson: materializedTools.deferredToolsJson,
5956
+ mcpGroups: materializedTools.mcpGroups,
5957
+ deferredMcpGroups: materializedTools.deferredMcpGroups,
5958
+ ...assembly.mcpInstructions ? { mcpInstructions: assembly.mcpInstructions } : {},
5959
+ ...assembly.skillsCatalog ? { skillsCatalog: assembly.skillsCatalog } : {},
5960
+ ...assembly.subagentDefs ? { subagentDefs: assembly.subagentDefs } : {},
5961
+ used,
5962
+ effectiveWindow,
5963
+ autocompactBuffer,
5964
+ ...exact ? { exact } : {},
5965
+ ...usage ? { usage: {
5966
+ input: usage.input ?? 0,
5967
+ cacheRead: usage.cacheRead ?? 0,
5968
+ cacheCreation: usage.cacheCreation ?? 0,
5969
+ output: usage.output ?? 0
5970
+ } } : {},
5971
+ ...skillActivationState.active().length > 0 ? { activeSkills: skillActivationState.active().map((s) => s.skill.name) } : {}
5972
+ });
4818
5973
  }
4819
5974
  const eagerHasWork = allMcpServers.length > 0 || !skillsDisabled && !!skillsConfig;
4820
5975
  if (eager && eagerHasWork) warmup().catch(() => {});
@@ -4851,225 +6006,11 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4851
6006
  return skillActivationState.active();
4852
6007
  },
4853
6008
  meta: Object.freeze({ ...provider.meta }),
6009
+ getContextBreakdown,
4854
6010
  [Symbol.asyncDispose]: destroy
4855
6011
  };
4856
6012
  }
4857
6013
  //#endregion
4858
- //#region src/tools/edit-utils.ts
4859
- /**
4860
- * Internal helpers shared between the `edit` and `multi_edit` tools.
4861
- *
4862
- * Not part of the public API — intentionally not re-exported from `tools/index.ts`
4863
- * or the package barrel.
4864
- */
4865
- /**
4866
- * Count exact (non-overlapping) occurrences of `needle` in `haystack`.
4867
- * Returns 0 for an empty needle — both edit tools reject empty `old_string`
4868
- * up front, so this branch is defensive rather than semantic.
4869
- */
4870
- function countExactMatches(haystack, needle) {
4871
- if (needle.length === 0) return 0;
4872
- let count = 0;
4873
- let idx = 0;
4874
- while (true) {
4875
- const next = haystack.indexOf(needle, idx);
4876
- if (next === -1) break;
4877
- count++;
4878
- idx = next + needle.length;
4879
- }
4880
- return count;
4881
- }
4882
- /** Map curly quotes (any of the four) to their straight ASCII equivalents. */
4883
- function normalizeQuotes(str) {
4884
- return str.replaceAll("‘", "'").replaceAll("’", "'").replaceAll("“", "\"").replaceAll("”", "\"");
4885
- }
4886
- /**
4887
- * Substitutions Anthropic's API applies to assistant output before the model
4888
- * sees it. The model emits the sanitized form; the file on disk contains the
4889
- * unsanitized form. We undo the substitutions on `old_string` so the search
4890
- * lands on the actual file contents.
4891
- *
4892
- * Verbatim from `claude-code/tools/FileEditTool/utils.ts`.
4893
- */
4894
- const DESANITIZATIONS = [
4895
- ["<fnr>", "<function_results>"],
4896
- ["<n>", "<name>"],
4897
- ["</n>", "</name>"],
4898
- ["<o>", "<output>"],
4899
- ["</o>", "</output>"],
4900
- ["<e>", "<error>"],
4901
- ["</e>", "</error>"],
4902
- ["<s>", "<system>"],
4903
- ["</s>", "</system>"],
4904
- ["<r>", "<result>"],
4905
- ["</r>", "</result>"],
4906
- ["< META_START >", "<META_START>"],
4907
- ["< META_END >", "<META_END>"],
4908
- ["< EOT >", "<EOT>"],
4909
- ["< META >", "<META>"],
4910
- ["< SOS >", "<SOS>"],
4911
- ["\n\nH:", "\n\nHuman:"],
4912
- ["\n\nA:", "\n\nAssistant:"]
4913
- ];
4914
- /**
4915
- * Apply the SDK desanitization table to a string. Exported so the edit tools
4916
- * can apply it to `new_string` whenever `old_string` matched via a
4917
- * desanitize-class fallback — keeps the file's unsanitized form on disk
4918
- * instead of writing the model's abbreviated form back.
4919
- */
4920
- function desanitize(s) {
4921
- let out = s;
4922
- for (const [from, to] of DESANITIZATIONS) out = out.replaceAll(from, to);
4923
- return out;
4924
- }
4925
- /**
4926
- * Strip line-number prefixes from each line of a needle, used as a recovery
4927
- * fallback when the model pastes a `read_file` chunk verbatim into
4928
- * `old_string` — the on-disk file doesn't carry the metadata prefix.
4929
- *
4930
- * Accepts three separator characters so a model that learned on a different
4931
- * agent stack still works here: `\t` (Claude Code compact, our default),
4932
- * `|`, and `→`. Pattern: optional leading whitespace, 1-9 digits, then one
4933
- * of `\t | →`. The 9-digit ceiling covers files up to ~1B lines without
4934
- * overshooting into legitimate `\d{N}<sep>` content like Markdown table
4935
- * cells with long numeric IDs.
4936
- */
4937
- const LINE_NUMBER_PREFIX_RE = /^[ \t]*\d{1,9}[\t|\u2192]/gm;
4938
- function stripLineNumberPrefixes(s) {
4939
- return s.replace(LINE_NUMBER_PREFIX_RE, "");
4940
- }
4941
- /**
4942
- * Search `target` in `normFile` and slice the matching span out of the
4943
- * original `haystack`, counting all non-overlapping occurrences. `normFile`
4944
- * is the haystack with whatever transform (quotes / desanitize / combined)
4945
- * was applied to make the indices align — slicing the original haystack
4946
- * preserves the file's actual typography so `replace_all` writes back the
4947
- * file's form, not the model's.
4948
- *
4949
- * Pre-condition: `normFile.length === haystack.length` (every transform
4950
- * we use is one-to-one). Returns null on miss.
4951
- */
4952
- function locateAndCount(haystack, normFile, target, via) {
4953
- const idx = normFile.indexOf(target);
4954
- if (idx === -1) return null;
4955
- const actual = haystack.slice(idx, idx + target.length);
4956
- let occ = 0;
4957
- let cursor = 0;
4958
- while (true) {
4959
- const next = normFile.indexOf(target, cursor);
4960
- if (next === -1) break;
4961
- occ++;
4962
- cursor = next + target.length;
4963
- }
4964
- return {
4965
- actual,
4966
- occurrences: occ,
4967
- via
4968
- };
4969
- }
4970
- function resolveOldString(haystack, needle) {
4971
- const exact = countExactMatches(haystack, needle);
4972
- if (exact > 0) return {
4973
- actual: needle,
4974
- occurrences: exact,
4975
- via: "exact"
4976
- };
4977
- const normNeedle = normalizeQuotes(needle);
4978
- const normFile = normalizeQuotes(haystack);
4979
- if (normNeedle !== needle || normFile !== haystack) {
4980
- const m = locateAndCount(haystack, normFile, normNeedle, "quotes");
4981
- if (m) return m;
4982
- }
4983
- const desan = desanitize(needle);
4984
- if (desan !== needle) {
4985
- const desanCount = countExactMatches(haystack, desan);
4986
- if (desanCount > 0) return {
4987
- actual: desan,
4988
- occurrences: desanCount,
4989
- via: "desanitize"
4990
- };
4991
- }
4992
- const combo = desanitize(normNeedle);
4993
- if (combo !== needle) {
4994
- const m = locateAndCount(haystack, normFile, combo, "quotes+desanitize");
4995
- if (m) return m;
4996
- }
4997
- const stripped = stripLineNumberPrefixes(needle);
4998
- if (stripped !== needle && stripped.trim().length > 0) {
4999
- const count = countExactMatches(haystack, stripped);
5000
- if (count > 0) return {
5001
- actual: stripped,
5002
- occurrences: count,
5003
- via: "line-numbers"
5004
- };
5005
- const strippedNorm = normalizeQuotes(stripped);
5006
- if (strippedNorm !== stripped || normFile !== haystack) {
5007
- const m = locateAndCount(haystack, normFile, strippedNorm, "quotes+line-numbers");
5008
- if (m) return m;
5009
- }
5010
- }
5011
- return null;
5012
- }
5013
- /**
5014
- * Apply the same recovery transforms used to find `old_string` to
5015
- * `new_string`, so the file gets back its native form: desanitize when
5016
- * the model emitted `<n>` for `<name>`, strip line-number prefixes when
5017
- * the match required them, then re-curlify when the match required
5018
- * quote normalization. Shared between `edit` and `multi_edit`.
5019
- */
5020
- function styleReplacementForVia(replacement, via, actual) {
5021
- let out = replacement;
5022
- if (via === "desanitize" || via === "quotes+desanitize") out = desanitize(out);
5023
- if (via === "line-numbers" || via === "quotes+line-numbers") out = stripLineNumberPrefixes(out);
5024
- if (via === "quotes" || via === "quotes+desanitize" || via === "quotes+line-numbers") out = preserveQuoteStyle(actual, out);
5025
- return out;
5026
- }
5027
- /**
5028
- * When `old_string` matched via curly-quote normalization, re-style
5029
- * `new_string` so the file's typography is preserved across the edit.
5030
- * Detects whether the matched file region had curly singles, doubles, or
5031
- * both, and applies the matching curlification to the replacement.
5032
- *
5033
- * Apostrophes in contractions (`don't`, `it's`) get the right-single curly
5034
- * quote regardless of opening context — that's the canonical typographer's
5035
- * convention for English. Other quotes use a simple
5036
- * preceded-by-whitespace-or-opening-punctuation heuristic.
5037
- */
5038
- function preserveQuoteStyle(actual, replacement) {
5039
- const hasDouble = actual.includes("“") || actual.includes("”");
5040
- const hasSingle = actual.includes("‘") || actual.includes("’");
5041
- if (!hasDouble && !hasSingle) return replacement;
5042
- let out = replacement;
5043
- if (hasDouble) out = applyCurly(out, "\"", "“", "”", false);
5044
- if (hasSingle) out = applyCurly(out, "'", "‘", "’", true);
5045
- return out;
5046
- }
5047
- function applyCurly(s, straight, left, right, contractionAware) {
5048
- const chars = [...s];
5049
- const result = [];
5050
- for (let i = 0; i < chars.length; i++) {
5051
- if (chars[i] !== straight) {
5052
- result.push(chars[i]);
5053
- continue;
5054
- }
5055
- if (contractionAware) {
5056
- const prev = i > 0 ? chars[i - 1] : "";
5057
- const next = i < chars.length - 1 ? chars[i + 1] : "";
5058
- if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
5059
- result.push(right);
5060
- continue;
5061
- }
5062
- }
5063
- result.push(isOpeningContext(chars, i) ? left : right);
5064
- }
5065
- return result.join("");
5066
- }
5067
- function isOpeningContext(chars, i) {
5068
- if (i === 0) return true;
5069
- const prev = chars[i - 1];
5070
- return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "—" || prev === "–";
5071
- }
5072
- //#endregion
5073
6014
  //#region src/tools/path-suggest.ts
5074
6015
  /**
5075
6016
  * Find a sibling file in the same directory sharing `path`'s basename
@@ -6008,6 +6949,8 @@ const BUBBLED_EVENTS = [
6008
6949
  "stream:thinking",
6009
6950
  "stream:end",
6010
6951
  "stream:error",
6952
+ "stream:server_tool_use",
6953
+ "stream:server_tool_result",
6011
6954
  "tool:dispatched",
6012
6955
  "tool:before",
6013
6956
  "tool:after",
@@ -6028,6 +6971,8 @@ const CHILD_EVENT_NAME = {
6028
6971
  "stream:thinking": "child:stream:thinking",
6029
6972
  "stream:end": "child:stream:end",
6030
6973
  "stream:error": "child:stream:error",
6974
+ "stream:server_tool_use": "child:stream:server_tool_use",
6975
+ "stream:server_tool_result": "child:stream:server_tool_result",
6031
6976
  "tool:dispatched": "child:tool:dispatched",
6032
6977
  "tool:before": "child:tool:before",
6033
6978
  "tool:after": "child:tool:after",
@@ -6524,6 +7469,6 @@ const writeFile$1 = {
6524
7469
  }
6525
7470
  };
6526
7471
  //#endregion
6527
- export { resolvePersistDir as A, formatTaskStatus as B, TOOL_USE_SKIPPED_MESSAGE as C, buildPersistedStub as D, PERSISTENCE_PREVIEW_BYTES as E, resolveReadStateMap as F, previewLine as H, ageString as I, compactPath as L, getReadState as M, hashContent as N, cleanupPersistedSession as O, readStateKey as P, fmtTokens as R, TOOL_USE_CANCELLED_MESSAGE as S, PERSISTED_STUB_PREFIX as T, shortId as U, formatTaskSummary as V, createSkillsReadTool as _, multiEdit as a, INTERRUPT_MESSAGE_FOR_TOOL_USE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, resolveTasksDir as j, maybePersistToolResult as k, glob$1 as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, createShellTool as v, validateToolArgs as w, SHELL_CASCADE_CANCEL_MESSAGE as x, shell as y, formatDuration as z };
7472
+ export { getReadState as A, enabledModelOptions as B, PERSISTED_STUB_PREFIX as C, maybePersistToolResult as D, cleanupPersistedSession as E, OUTPUT_RESERVE_TOKENS as F, modelSupportsReasoning as G, getModelInfo as H, anthropicDescriptor as I, openrouterDescriptor as J, modelsForDescriptor as K, cerebrasDescriptor as L, readStateKey as M, resolveReadStateMap as N, resolvePersistDir as O, BUILTIN_PROVIDERS as P, credKeyOf as R, validateToolArgs as S, buildPersistedStub as T, localDescriptor as U, getContextWindow as V, modelOptionsFor as W, restoreModelOptions as X, piIdOf as Y, shell as _, multiEdit as a, TOOL_USE_CANCELLED_MESSAGE as b, grep as c, createAgent as d, createToolSearchTool as f, createShellTool as g, createSkillsReadTool as h, readFile$1 as i, hashContent as j, resolveTasksDir as k, glob$1 as l, createSkillsRunScriptTool as m, createSpawnTool as n, listFiles as o, createSkillsUseTool as p, openaiDescriptor as q, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, INTERRUPT_MESSAGE_FOR_TOOL_USE as v, PERSISTENCE_PREVIEW_BYTES as w, TOOL_USE_SKIPPED_MESSAGE as x, SHELL_CASCADE_CANCEL_MESSAGE as y, effectiveContextWindow as z };
6528
7473
 
6529
- //# sourceMappingURL=tools-0Kolu2bY.js.map
7474
+ //# sourceMappingURL=tools-BGtJK0vo.js.map