zidane 5.6.14 → 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.
- package/README.md +3 -1
- package/dist/{agent-ClkpElCZ.d.ts → agent-BNS2nx_T.d.ts} +535 -15
- package/dist/agent-BNS2nx_T.d.ts.map +1 -0
- package/dist/chat/pure.d.ts +4 -0
- package/dist/chat/pure.js +3 -0
- package/dist/chat.d.ts +31 -661
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +5 -3
- package/dist/chat.js.map +1 -1
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js.map +1 -1
- package/dist/{contexts-BOtMvzli.js → contexts-BD2U_xpi.js} +2 -2
- package/dist/{contexts-BOtMvzli.js.map → contexts-BD2U_xpi.js.map} +1 -1
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/edit-utils-DnfNoj16.js +574 -0
- package/dist/edit-utils-DnfNoj16.js.map +1 -0
- package/dist/{errors-DdZXnyXE.js → errors-CoQnKRf1.js} +32 -2
- package/dist/{errors-DdZXnyXE.js.map → errors-CoQnKRf1.js.map} +1 -1
- package/dist/fetch-url-CPxfiXDa.js +518 -0
- package/dist/fetch-url-CPxfiXDa.js.map +1 -0
- package/dist/image-sniff-B7uFSNO1.js +90 -0
- package/dist/image-sniff-B7uFSNO1.js.map +1 -0
- package/dist/{index-CbS75MD3.d.ts → index-CZOwAJIX.d.ts} +2 -2
- package/dist/index-CZOwAJIX.d.ts.map +1 -0
- package/dist/{index-CTDMMdIy.d.ts → index-Ck_AWt8P.d.ts} +3 -4
- package/dist/index-Ck_AWt8P.d.ts.map +1 -0
- package/dist/{index-v3Tzobqr.d.ts → index-KiS7w0dC.d.ts} +3 -3
- package/dist/index-KiS7w0dC.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-DM1UcKeQ.js → interpolate-TySiqKzc.js} +23 -23
- package/dist/{interpolate-DM1UcKeQ.js.map → interpolate-TySiqKzc.js.map} +1 -1
- package/dist/{login-7tHcckmX.js → login-BDeqENSe.js} +7 -58
- package/dist/login-BDeqENSe.js.map +1 -0
- package/dist/{mcp-DGeB7-3D.js → mcp-Kqzz-Rs_.js} +8 -6
- package/dist/mcp-Kqzz-Rs_.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{messages-Dym8S_YH.js → messages-CvRQTdbR.js} +118 -39
- package/dist/messages-CvRQTdbR.js.map +1 -0
- package/dist/{presets-w9Px_aAm.js → presets-JuOnSI-i.js} +2 -2
- package/dist/{presets-w9Px_aAm.js.map → presets-JuOnSI-i.js.map} +1 -1
- package/dist/presets.d.ts +3 -3
- package/dist/presets.js +1 -1
- package/dist/{providers-beXyD9W9.js → providers-h4HJPbbv.js} +485 -31
- package/dist/providers-h4HJPbbv.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-BRIsmBSY.js → session-BzLou2_-.js} +2 -2
- package/dist/{session-BRIsmBSY.js.map → session-BzLou2_-.js.map} +1 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/{stats-Lc3zL3RM.js → stats-DAKBEKjc.js} +12 -2
- package/dist/stats-DAKBEKjc.js.map +1 -0
- package/dist/{stdio-loader-EVAF5KlU.js → stdio-loader-Ce68wUmM.js} +4 -4
- package/dist/stdio-loader-Ce68wUmM.js.map +1 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts +1471 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +70 -0
- package/dist/tools/fetch-url.d.ts.map +1 -0
- package/dist/tools/fetch-url.js +2 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +190 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/{tools-DhrLrOEr.js → tools-BGtJK0vo.js} +1368 -421
- package/dist/tools-BGtJK0vo.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +1 -1
- package/dist/{turn-operations-UAkOjO-u.js → transcript-anchors-BTSZAPVc.js} +147 -2713
- package/dist/transcript-anchors-BTSZAPVc.js.map +1 -0
- package/dist/{transcript-anchors-D0TR6djV.d.ts → transcript-anchors-DX90kXc4.d.ts} +13 -1299
- package/dist/transcript-anchors-DX90kXc4.d.ts.map +1 -0
- package/dist/tui.d.ts +58 -28
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1349 -422
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-CCHfR9eC.js +1938 -0
- package/dist/turn-operations-CCHfR9eC.js.map +1 -0
- package/dist/turn-operations-DDIl4YVk.d.ts +658 -0
- package/dist/turn-operations-DDIl4YVk.d.ts.map +1 -0
- package/dist/{types-oKPBdCmL.js → types-BPw_i5vb.js} +1 -1
- package/dist/types-BPw_i5vb.js.map +1 -0
- package/dist/{types-KukEp-mi.d.ts → types-CEAMIUXw.d.ts} +1 -1
- package/dist/types-CEAMIUXw.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +3 -3
- package/docs/CHAT.md +53 -6
- package/docs/SKILL.md +3 -0
- package/docs/TUI.md +7 -0
- package/package.json +18 -2
- package/dist/agent-ClkpElCZ.d.ts.map +0 -1
- package/dist/index-CTDMMdIy.d.ts.map +0 -1
- package/dist/index-CbS75MD3.d.ts.map +0 -1
- package/dist/index-v3Tzobqr.d.ts.map +0 -1
- package/dist/login-7tHcckmX.js.map +0 -1
- package/dist/mcp-DGeB7-3D.js.map +0 -1
- package/dist/messages-Dym8S_YH.js.map +0 -1
- package/dist/providers-beXyD9W9.js.map +0 -1
- package/dist/stats-Lc3zL3RM.js.map +0 -1
- package/dist/stdio-loader-EVAF5KlU.js.map +0 -1
- package/dist/tools-DhrLrOEr.js.map +0 -1
- package/dist/transcript-anchors-D0TR6djV.d.ts.map +0 -1
- package/dist/turn-operations-UAkOjO-u.js.map +0 -1
- package/dist/types-KukEp-mi.d.ts.map +0 -1
- package/dist/types-oKPBdCmL.js.map +0 -1
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { c as
|
|
3
|
-
import { n as
|
|
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-
|
|
5
|
-
import { t as
|
|
6
|
-
import {
|
|
7
|
-
import { n as
|
|
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";
|
|
8
11
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
9
12
|
import { createHooks } from "hookable";
|
|
10
|
-
import {
|
|
11
|
-
import { glob, mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
13
|
+
import { getModel, getModels } from "@earendil-works/pi-ai";
|
|
12
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";
|
|
13
19
|
//#region src/aliasing.ts
|
|
14
20
|
/**
|
|
15
21
|
* Build alias lookup maps from a `toolAliases` record.
|
|
@@ -143,126 +149,966 @@ function rewriteMessagesToWire(messages, maps) {
|
|
|
143
149
|
}));
|
|
144
150
|
}
|
|
145
151
|
//#endregion
|
|
146
|
-
//#region src/chat/
|
|
147
|
-
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
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.
|
|
256
748
|
*/
|
|
257
|
-
function
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
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;
|
|
263
754
|
}
|
|
264
|
-
|
|
265
|
-
|
|
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;
|
|
266
1112
|
}
|
|
267
1113
|
//#endregion
|
|
268
1114
|
//#region src/tools/read-state.ts
|
|
@@ -685,7 +1531,7 @@ function buildPersistedStub(input) {
|
|
|
685
1531
|
const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output, PERSISTENCE_PREVIEW_BYTES);
|
|
686
1532
|
const previewMarker = previewSlice.length < input.output.length ? `\n…(${input.originalBytes - previewBytes} more bytes in persisted file)` : "";
|
|
687
1533
|
return [
|
|
688
|
-
`${PERSISTED_STUB_PREFIX}${
|
|
1534
|
+
`${PERSISTED_STUB_PREFIX}${escapeXml(input.toolName)}" bytes="${input.originalBytes}" path="${escapeXml(input.persistedPath)}">`,
|
|
689
1535
|
`${previewSlice}${previewMarker}`,
|
|
690
1536
|
"</persisted-output>"
|
|
691
1537
|
].join("\n");
|
|
@@ -738,14 +1584,6 @@ async function writeAtomic(path, content) {
|
|
|
738
1584
|
}
|
|
739
1585
|
}
|
|
740
1586
|
/**
|
|
741
|
-
* Byte length of a UTF-8 string. `Buffer.byteLength` is the canonical
|
|
742
|
-
* answer; pulling it through a small helper keeps `node:buffer` out of
|
|
743
|
-
* call sites that don't otherwise need it.
|
|
744
|
-
*/
|
|
745
|
-
function byteLength(text) {
|
|
746
|
-
return Buffer.byteLength(text, "utf8");
|
|
747
|
-
}
|
|
748
|
-
/**
|
|
749
1587
|
* Take the first `cap` bytes of `text` without splitting a UTF-8
|
|
750
1588
|
* codepoint. Returns the substring AND its exact UTF-8 byte length so
|
|
751
1589
|
* the caller doesn't repeat the byte walk (the truncation marker in
|
|
@@ -769,7 +1607,7 @@ function sliceFirstBytes(text, cap) {
|
|
|
769
1607
|
slice: "",
|
|
770
1608
|
bytes: 0
|
|
771
1609
|
};
|
|
772
|
-
const total =
|
|
1610
|
+
const total = utf8ByteLength(text);
|
|
773
1611
|
if (total <= cap) return {
|
|
774
1612
|
slice: text,
|
|
775
1613
|
bytes: total
|
|
@@ -777,7 +1615,7 @@ function sliceFirstBytes(text, cap) {
|
|
|
777
1615
|
let bytes = 0;
|
|
778
1616
|
let charIdx = 0;
|
|
779
1617
|
for (const ch of text) {
|
|
780
|
-
const chBytes =
|
|
1618
|
+
const chBytes = utf8ByteLength(ch);
|
|
781
1619
|
if (bytes + chBytes > cap) break;
|
|
782
1620
|
bytes += chBytes;
|
|
783
1621
|
charIdx += ch.length;
|
|
@@ -787,18 +1625,6 @@ function sliceFirstBytes(text, cap) {
|
|
|
787
1625
|
bytes
|
|
788
1626
|
};
|
|
789
1627
|
}
|
|
790
|
-
/**
|
|
791
|
-
* Escape `&`, `"`, and `<` for safe inclusion in an XML attribute. The
|
|
792
|
-
* stub format embeds tool names and filesystem paths verbatim — both
|
|
793
|
-
* theoretically could contain one of these (Windows paths can't, but a
|
|
794
|
-
* tool alias is user-controlled). Keeping the attribute well-formed
|
|
795
|
-
* prevents a malicious or unusual tool name from breaking the wrapper
|
|
796
|
-
* parse. `&` is escaped first so the subsequent replacements don't
|
|
797
|
-
* double-encode the entities we just emitted.
|
|
798
|
-
*/
|
|
799
|
-
function escapeAttr(text) {
|
|
800
|
-
return text.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
801
|
-
}
|
|
802
1628
|
//#endregion
|
|
803
1629
|
//#region src/tools/validation.ts
|
|
804
1630
|
const TRUE_STRINGS = new Set([
|
|
@@ -1856,6 +2682,7 @@ async function executeTurn(ctx, turn, priorUsage) {
|
|
|
1856
2682
|
maxTokens: ctx.maxTokens ?? 16384,
|
|
1857
2683
|
thinking: ctx.thinking,
|
|
1858
2684
|
thinkingBudget: effectiveThinkingBudget,
|
|
2685
|
+
...ctx.modelOptions ? { modelOptions: ctx.modelOptions } : {},
|
|
1859
2686
|
cache: ctx.cache ?? true,
|
|
1860
2687
|
signal: ctx.signal
|
|
1861
2688
|
};
|
|
@@ -1916,6 +2743,22 @@ async function executeTurn(ctx, turn, priorUsage) {
|
|
|
1916
2743
|
turnId
|
|
1917
2744
|
});
|
|
1918
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
|
+
},
|
|
1919
2762
|
onOAuthRefresh(refreshCtx) {
|
|
1920
2763
|
return ctx.hooks.callHook("oauth:refresh", refreshCtx);
|
|
1921
2764
|
}
|
|
@@ -2385,10 +3228,10 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
|
|
|
2385
3228
|
let removeAbortListener;
|
|
2386
3229
|
const cancellationPromise = new Promise((_, reject) => {
|
|
2387
3230
|
if (perCallAbort.signal.aborted) {
|
|
2388
|
-
reject(new Error(CANCELLED_BY_USER_SENTINEL));
|
|
3231
|
+
reject(/* @__PURE__ */ new Error(CANCELLED_BY_USER_SENTINEL));
|
|
2389
3232
|
return;
|
|
2390
3233
|
}
|
|
2391
|
-
const onAbort = () => reject(new Error(CANCELLED_BY_USER_SENTINEL));
|
|
3234
|
+
const onAbort = () => reject(/* @__PURE__ */ new Error(CANCELLED_BY_USER_SENTINEL));
|
|
2392
3235
|
perCallAbort.signal.addEventListener("abort", onAbort, { once: true });
|
|
2393
3236
|
removeAbortListener = () => perCallAbort.signal.removeEventListener("abort", onAbort);
|
|
2394
3237
|
});
|
|
@@ -2644,6 +3487,20 @@ async function executeToolBatch(ctx, toolCalls, turnId) {
|
|
|
2644
3487
|
const drain = async () => {
|
|
2645
3488
|
if (inFlight.size > 0) await Promise.all([...inFlight.values()]);
|
|
2646
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
|
+
};
|
|
2647
3504
|
/** Whether every in-flight call is concurrency-safe. */
|
|
2648
3505
|
const fleetAllSafe = () => {
|
|
2649
3506
|
for (const idx of inFlight.keys()) if (!safe[idx]) return false;
|
|
@@ -2665,7 +3522,8 @@ async function executeToolBatch(ctx, toolCalls, turnId) {
|
|
|
2665
3522
|
};
|
|
2666
3523
|
try {
|
|
2667
3524
|
for (let i = 0; i < N; i++) {
|
|
2668
|
-
if (!safe[i] || !fleetAllSafe()
|
|
3525
|
+
if (!safe[i] || !fleetAllSafe()) await drain();
|
|
3526
|
+
else if (inFlight.size >= maxConcurrent) await waitForSlot();
|
|
2669
3527
|
if (ctx.signal.aborted) {
|
|
2670
3528
|
await drain();
|
|
2671
3529
|
fillUnstarted(i, INTERRUPT_MESSAGE_FOR_TOOL_USE);
|
|
@@ -3757,6 +4615,81 @@ function createToolSearchTool(options) {
|
|
|
3757
4615
|
//#endregion
|
|
3758
4616
|
//#region src/agent.ts
|
|
3759
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
|
+
/**
|
|
3760
4693
|
* Authoritative list of hook event names. Kept in sync with `AgentHooks` at
|
|
3761
4694
|
* compile time: the `satisfies` assertion below rejects any drift.
|
|
3762
4695
|
*/
|
|
@@ -3799,6 +4732,8 @@ const HOOK_EVENT_SET = new Set([
|
|
|
3799
4732
|
"stream:text",
|
|
3800
4733
|
"stream:end",
|
|
3801
4734
|
"stream:thinking",
|
|
4735
|
+
"stream:server_tool_use",
|
|
4736
|
+
"stream:server_tool_result",
|
|
3802
4737
|
"stream:error",
|
|
3803
4738
|
"stream:retry",
|
|
3804
4739
|
"oauth:refresh",
|
|
@@ -3821,6 +4756,8 @@ const HOOK_EVENT_SET = new Set([
|
|
|
3821
4756
|
"child:stream:text",
|
|
3822
4757
|
"child:stream:thinking",
|
|
3823
4758
|
"child:stream:end",
|
|
4759
|
+
"child:stream:server_tool_use",
|
|
4760
|
+
"child:stream:server_tool_result",
|
|
3824
4761
|
"child:stream:error",
|
|
3825
4762
|
"child:tool:gate",
|
|
3826
4763
|
"child:mcp:tool:gate",
|
|
@@ -3938,6 +4875,7 @@ function resolveBehavior(agentBehavior, runBehavior) {
|
|
|
3938
4875
|
maxTotalTokens: runBehavior?.maxTotalTokens ?? agentBehavior?.maxTotalTokens,
|
|
3939
4876
|
maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
|
|
3940
4877
|
thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
|
|
4878
|
+
modelOptions: runBehavior?.modelOptions ?? agentBehavior?.modelOptions,
|
|
3941
4879
|
schema: runBehavior?.schema ?? agentBehavior?.schema,
|
|
3942
4880
|
cache: runBehavior?.cache ?? agentBehavior?.cache ?? true,
|
|
3943
4881
|
toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget,
|
|
@@ -4119,29 +5057,43 @@ function renderMcpInstructionsSection(instructions) {
|
|
|
4119
5057
|
return parts.join("\n");
|
|
4120
5058
|
}
|
|
4121
5059
|
/**
|
|
4122
|
-
*
|
|
4123
|
-
*
|
|
4124
|
-
*
|
|
4125
|
-
* `
|
|
4126
|
-
* shape minted by this module; any caller-supplied custom id schemes
|
|
4127
|
-
* are ignored (they don't conflict with `run_N`).
|
|
4128
|
-
*
|
|
4129
|
-
* Returning 0 for a sessionless / clean session preserves the original
|
|
4130
|
-
* "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.
|
|
4131
5064
|
*/
|
|
4132
|
-
function
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
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;
|
|
4145
5097
|
}
|
|
4146
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 }) {
|
|
4147
5099
|
const hooks = createHooks();
|
|
@@ -4159,16 +5111,43 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4159
5111
|
let running = false;
|
|
4160
5112
|
let idleResolve;
|
|
4161
5113
|
let idlePromise;
|
|
5114
|
+
function resetRunScope() {
|
|
5115
|
+
running = false;
|
|
5116
|
+
abortController = void 0;
|
|
5117
|
+
idleResolve?.();
|
|
5118
|
+
idlePromise = void 0;
|
|
5119
|
+
idleResolve = void 0;
|
|
5120
|
+
}
|
|
4162
5121
|
const pendingTaskNotifications = /* @__PURE__ */ new Map();
|
|
4163
5122
|
const pendingToolCancels = /* @__PURE__ */ new Map();
|
|
4164
5123
|
let executionHandle = null;
|
|
4165
5124
|
let mcpConnection = null;
|
|
5125
|
+
let lastContextAssembly = null;
|
|
4166
5126
|
let mcpWarmupPromise = null;
|
|
4167
5127
|
const allMcpServers = mcpServers ?? [];
|
|
4168
5128
|
const steeringQueue = [];
|
|
4169
5129
|
const followUpQueue = [];
|
|
4170
5130
|
let conversationTurns = session?.turns.slice() ?? [];
|
|
4171
|
-
let runCounter =
|
|
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();
|
|
4172
5151
|
const skillsConfig = agentSkills;
|
|
4173
5152
|
const skillsEnabledValue = skillsConfig?.enabled;
|
|
4174
5153
|
const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
|
|
@@ -4212,24 +5191,14 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4212
5191
|
const skillActivationState = createSkillActivationState({ maxActive: skillsConfig?.maxActive });
|
|
4213
5192
|
async function run(options) {
|
|
4214
5193
|
if (running) throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
|
|
4215
|
-
const
|
|
4216
|
-
if (!options.prompt && !hasSessionTurns) throw new Error("prompt is required when no session with existing turns is provided");
|
|
4217
|
-
let resumeFilteredTurns;
|
|
4218
|
-
if (hasSessionTurns) resumeFilteredTurns = filterUnresolvedToolUses(session.turns);
|
|
4219
|
-
if (!options.prompt && resumeFilteredTurns) {
|
|
4220
|
-
const lastTurn = resumeFilteredTurns.at(-1);
|
|
4221
|
-
if (lastTurn && lastTurn.role !== "user") {
|
|
4222
|
-
const detail = detectTurnInterruption(resumeFilteredTurns) === "completed" ? "last turn is a completed assistant message" : "last turn is mid-stream assistant content";
|
|
4223
|
-
throw new Error(`cannot resume without prompt: ${detail}. Pass a prompt to agent.run({ prompt: … }).`);
|
|
4224
|
-
}
|
|
4225
|
-
}
|
|
5194
|
+
const resumeFilteredTurns = validateAndPrepareResume(session, options.prompt);
|
|
4226
5195
|
const clock = options.clock ?? agentClock ?? DEFAULT_AGENT_CLOCK;
|
|
4227
5196
|
let externalAbortListener;
|
|
4228
5197
|
const externalSignal = options.signal;
|
|
4229
5198
|
running = true;
|
|
4230
5199
|
try {
|
|
4231
5200
|
abortController = new AbortController();
|
|
4232
|
-
|
|
5201
|
+
syncRunCounter();
|
|
4233
5202
|
const runId = `run_${++runCounter}`;
|
|
4234
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") : "";
|
|
4235
5204
|
session?.startRun(runId, promptLabel, {
|
|
@@ -4305,8 +5274,10 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4305
5274
|
const thinking = options.thinking ?? "off";
|
|
4306
5275
|
const model = options.model ?? provider.meta.defaultModel;
|
|
4307
5276
|
const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
|
|
4308
|
-
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;
|
|
4309
5279
|
let system = options.system || agentSystem || "You are a helpful assistant.";
|
|
5280
|
+
const baseSystemForBreakdown = renderSystemForWire(system);
|
|
4310
5281
|
if (skillsCatalog) system = appendStaticSection(system, skillsCatalog);
|
|
4311
5282
|
const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? {
|
|
4312
5283
|
...sourceTools,
|
|
@@ -4367,14 +5338,20 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4367
5338
|
initialUnlocked.add(toolSearchTool.spec.name);
|
|
4368
5339
|
}
|
|
4369
5340
|
const discoveryToolName = shouldInjectToolSearch ? "tool_search" : hostDefinedToolSearch ? toolAliases?.tool_search ?? "tool_search" : null;
|
|
4370
|
-
|
|
5341
|
+
let searchableCatalogText;
|
|
5342
|
+
if (disclosure.lazyEntries.length > 0) {
|
|
5343
|
+
searchableCatalogText = buildSearchableCatalog(disclosure.lazyEntries, { discoveryToolName });
|
|
5344
|
+
system = appendStaticSection(system, searchableCatalogText);
|
|
5345
|
+
}
|
|
4371
5346
|
if (surfaceMcpInstructions && mcpConnection?.instructions && mcpConnection.instructions.size > 0) {
|
|
4372
5347
|
const section = renderMcpInstructionsSection(mcpConnection.instructions);
|
|
4373
5348
|
if (section.length > 0) system = appendStaticSection(system, section);
|
|
4374
5349
|
}
|
|
4375
5350
|
const aliasMaps = buildAliasMaps(toolAliases, Object.keys(tools));
|
|
4376
5351
|
augmentMcpDoubleUnderscoreAliases(aliasMaps, Object.keys(tools));
|
|
5352
|
+
let formattedToolsCache = null;
|
|
4377
5353
|
function buildFormattedTools() {
|
|
5354
|
+
if (formattedToolsCache && formattedToolsCache.tailLen === dynamicUnlockOrder.length) return formattedToolsCache.value;
|
|
4378
5355
|
const specs = [];
|
|
4379
5356
|
for (const t of Object.values(tools)) {
|
|
4380
5357
|
if (!initialUnlocked.has(t.spec.name)) continue;
|
|
@@ -4393,12 +5370,52 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4393
5370
|
inputSchema: t.spec.inputSchema
|
|
4394
5371
|
});
|
|
4395
5372
|
}
|
|
4396
|
-
|
|
5373
|
+
const value = specs.length > 0 ? provider.formatTools(specs) : [];
|
|
5374
|
+
formattedToolsCache = {
|
|
5375
|
+
tailLen: dynamicUnlockOrder.length,
|
|
5376
|
+
value
|
|
5377
|
+
};
|
|
5378
|
+
return value;
|
|
4397
5379
|
}
|
|
4398
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
|
+
}
|
|
4399
5416
|
const turns = [];
|
|
4400
5417
|
if (session && session.turns.length > 0 && (session.runs.length > 0 || !options.prompt) && !options.parentRunId) {
|
|
4401
|
-
const childRunIds =
|
|
5418
|
+
const childRunIds = childRunIdSet(session);
|
|
4402
5419
|
const resumed = childRunIds.size === 0 ? session.turns : session.turns.filter((t) => !t.runId || !childRunIds.has(t.runId));
|
|
4403
5420
|
const filteredForRuntime = resumeFilteredTurns && resumed === session.turns ? resumeFilteredTurns : filterUnresolvedToolUses(resumed);
|
|
4404
5421
|
turns.push(...filteredForRuntime);
|
|
@@ -4537,6 +5554,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4537
5554
|
...options.parentRunId ? { parentRunId: options.parentRunId } : {},
|
|
4538
5555
|
depth: runDepth,
|
|
4539
5556
|
thinkingBudget,
|
|
5557
|
+
...modelOptions ? { modelOptions } : {},
|
|
4540
5558
|
schema,
|
|
4541
5559
|
cache,
|
|
4542
5560
|
toolOutputBudget,
|
|
@@ -4645,20 +5663,12 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4645
5663
|
unregisterTurnSync?.();
|
|
4646
5664
|
unregisterToolResultsSync?.();
|
|
4647
5665
|
for (const unregister of perRunUnregisters) unregister();
|
|
4648
|
-
running = false;
|
|
4649
|
-
abortController = void 0;
|
|
4650
5666
|
steeringQueue.length = 0;
|
|
4651
5667
|
followUpQueue.length = 0;
|
|
4652
|
-
|
|
4653
|
-
idlePromise = void 0;
|
|
4654
|
-
idleResolve = void 0;
|
|
5668
|
+
resetRunScope();
|
|
4655
5669
|
}
|
|
4656
5670
|
} finally {
|
|
4657
|
-
|
|
4658
|
-
abortController = void 0;
|
|
4659
|
-
idleResolve?.();
|
|
4660
|
-
idlePromise = void 0;
|
|
4661
|
-
idleResolve = void 0;
|
|
5671
|
+
resetRunScope();
|
|
4662
5672
|
if (externalSignal && externalAbortListener) externalSignal.removeEventListener("abort", externalAbortListener);
|
|
4663
5673
|
}
|
|
4664
5674
|
}
|
|
@@ -4690,6 +5700,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4690
5700
|
conversationTurns = [];
|
|
4691
5701
|
steeringQueue.length = 0;
|
|
4692
5702
|
followUpQueue.length = 0;
|
|
5703
|
+
lastContextAssembly = null;
|
|
4693
5704
|
pendingTaskNotifications.clear();
|
|
4694
5705
|
const cleared = skillActivationState.clear();
|
|
4695
5706
|
for (const record of cleared) await hooks.callHook("skills:deactivate", {
|
|
@@ -4814,6 +5825,151 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4814
5825
|
pendingTaskNotifications.clear();
|
|
4815
5826
|
skillsCleanup();
|
|
4816
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
|
+
});
|
|
4817
5973
|
}
|
|
4818
5974
|
const eagerHasWork = allMcpServers.length > 0 || !skillsDisabled && !!skillsConfig;
|
|
4819
5975
|
if (eager && eagerHasWork) warmup().catch(() => {});
|
|
@@ -4850,225 +6006,11 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
4850
6006
|
return skillActivationState.active();
|
|
4851
6007
|
},
|
|
4852
6008
|
meta: Object.freeze({ ...provider.meta }),
|
|
6009
|
+
getContextBreakdown,
|
|
4853
6010
|
[Symbol.asyncDispose]: destroy
|
|
4854
6011
|
};
|
|
4855
6012
|
}
|
|
4856
6013
|
//#endregion
|
|
4857
|
-
//#region src/tools/edit-utils.ts
|
|
4858
|
-
/**
|
|
4859
|
-
* Internal helpers shared between the `edit` and `multi_edit` tools.
|
|
4860
|
-
*
|
|
4861
|
-
* Not part of the public API — intentionally not re-exported from `tools/index.ts`
|
|
4862
|
-
* or the package barrel.
|
|
4863
|
-
*/
|
|
4864
|
-
/**
|
|
4865
|
-
* Count exact (non-overlapping) occurrences of `needle` in `haystack`.
|
|
4866
|
-
* Returns 0 for an empty needle — both edit tools reject empty `old_string`
|
|
4867
|
-
* up front, so this branch is defensive rather than semantic.
|
|
4868
|
-
*/
|
|
4869
|
-
function countExactMatches(haystack, needle) {
|
|
4870
|
-
if (needle.length === 0) return 0;
|
|
4871
|
-
let count = 0;
|
|
4872
|
-
let idx = 0;
|
|
4873
|
-
while (true) {
|
|
4874
|
-
const next = haystack.indexOf(needle, idx);
|
|
4875
|
-
if (next === -1) break;
|
|
4876
|
-
count++;
|
|
4877
|
-
idx = next + needle.length;
|
|
4878
|
-
}
|
|
4879
|
-
return count;
|
|
4880
|
-
}
|
|
4881
|
-
/** Map curly quotes (any of the four) to their straight ASCII equivalents. */
|
|
4882
|
-
function normalizeQuotes(str) {
|
|
4883
|
-
return str.replaceAll("‘", "'").replaceAll("’", "'").replaceAll("“", "\"").replaceAll("”", "\"");
|
|
4884
|
-
}
|
|
4885
|
-
/**
|
|
4886
|
-
* Substitutions Anthropic's API applies to assistant output before the model
|
|
4887
|
-
* sees it. The model emits the sanitized form; the file on disk contains the
|
|
4888
|
-
* unsanitized form. We undo the substitutions on `old_string` so the search
|
|
4889
|
-
* lands on the actual file contents.
|
|
4890
|
-
*
|
|
4891
|
-
* Verbatim from `claude-code/tools/FileEditTool/utils.ts`.
|
|
4892
|
-
*/
|
|
4893
|
-
const DESANITIZATIONS = [
|
|
4894
|
-
["<fnr>", "<function_results>"],
|
|
4895
|
-
["<n>", "<name>"],
|
|
4896
|
-
["</n>", "</name>"],
|
|
4897
|
-
["<o>", "<output>"],
|
|
4898
|
-
["</o>", "</output>"],
|
|
4899
|
-
["<e>", "<error>"],
|
|
4900
|
-
["</e>", "</error>"],
|
|
4901
|
-
["<s>", "<system>"],
|
|
4902
|
-
["</s>", "</system>"],
|
|
4903
|
-
["<r>", "<result>"],
|
|
4904
|
-
["</r>", "</result>"],
|
|
4905
|
-
["< META_START >", "<META_START>"],
|
|
4906
|
-
["< META_END >", "<META_END>"],
|
|
4907
|
-
["< EOT >", "<EOT>"],
|
|
4908
|
-
["< META >", "<META>"],
|
|
4909
|
-
["< SOS >", "<SOS>"],
|
|
4910
|
-
["\n\nH:", "\n\nHuman:"],
|
|
4911
|
-
["\n\nA:", "\n\nAssistant:"]
|
|
4912
|
-
];
|
|
4913
|
-
/**
|
|
4914
|
-
* Apply the SDK desanitization table to a string. Exported so the edit tools
|
|
4915
|
-
* can apply it to `new_string` whenever `old_string` matched via a
|
|
4916
|
-
* desanitize-class fallback — keeps the file's unsanitized form on disk
|
|
4917
|
-
* instead of writing the model's abbreviated form back.
|
|
4918
|
-
*/
|
|
4919
|
-
function desanitize(s) {
|
|
4920
|
-
let out = s;
|
|
4921
|
-
for (const [from, to] of DESANITIZATIONS) out = out.replaceAll(from, to);
|
|
4922
|
-
return out;
|
|
4923
|
-
}
|
|
4924
|
-
/**
|
|
4925
|
-
* Strip line-number prefixes from each line of a needle, used as a recovery
|
|
4926
|
-
* fallback when the model pastes a `read_file` chunk verbatim into
|
|
4927
|
-
* `old_string` — the on-disk file doesn't carry the metadata prefix.
|
|
4928
|
-
*
|
|
4929
|
-
* Accepts three separator characters so a model that learned on a different
|
|
4930
|
-
* agent stack still works here: `\t` (Claude Code compact, our default),
|
|
4931
|
-
* `|`, and `→`. Pattern: optional leading whitespace, 1-9 digits, then one
|
|
4932
|
-
* of `\t | →`. The 9-digit ceiling covers files up to ~1B lines without
|
|
4933
|
-
* overshooting into legitimate `\d{N}<sep>` content like Markdown table
|
|
4934
|
-
* cells with long numeric IDs.
|
|
4935
|
-
*/
|
|
4936
|
-
const LINE_NUMBER_PREFIX_RE = /^[ \t]*\d{1,9}[\t|\u2192]/gm;
|
|
4937
|
-
function stripLineNumberPrefixes(s) {
|
|
4938
|
-
return s.replace(LINE_NUMBER_PREFIX_RE, "");
|
|
4939
|
-
}
|
|
4940
|
-
/**
|
|
4941
|
-
* Search `target` in `normFile` and slice the matching span out of the
|
|
4942
|
-
* original `haystack`, counting all non-overlapping occurrences. `normFile`
|
|
4943
|
-
* is the haystack with whatever transform (quotes / desanitize / combined)
|
|
4944
|
-
* was applied to make the indices align — slicing the original haystack
|
|
4945
|
-
* preserves the file's actual typography so `replace_all` writes back the
|
|
4946
|
-
* file's form, not the model's.
|
|
4947
|
-
*
|
|
4948
|
-
* Pre-condition: `normFile.length === haystack.length` (every transform
|
|
4949
|
-
* we use is one-to-one). Returns null on miss.
|
|
4950
|
-
*/
|
|
4951
|
-
function locateAndCount(haystack, normFile, target, via) {
|
|
4952
|
-
const idx = normFile.indexOf(target);
|
|
4953
|
-
if (idx === -1) return null;
|
|
4954
|
-
const actual = haystack.slice(idx, idx + target.length);
|
|
4955
|
-
let occ = 0;
|
|
4956
|
-
let cursor = 0;
|
|
4957
|
-
while (true) {
|
|
4958
|
-
const next = normFile.indexOf(target, cursor);
|
|
4959
|
-
if (next === -1) break;
|
|
4960
|
-
occ++;
|
|
4961
|
-
cursor = next + target.length;
|
|
4962
|
-
}
|
|
4963
|
-
return {
|
|
4964
|
-
actual,
|
|
4965
|
-
occurrences: occ,
|
|
4966
|
-
via
|
|
4967
|
-
};
|
|
4968
|
-
}
|
|
4969
|
-
function resolveOldString(haystack, needle) {
|
|
4970
|
-
const exact = countExactMatches(haystack, needle);
|
|
4971
|
-
if (exact > 0) return {
|
|
4972
|
-
actual: needle,
|
|
4973
|
-
occurrences: exact,
|
|
4974
|
-
via: "exact"
|
|
4975
|
-
};
|
|
4976
|
-
const normNeedle = normalizeQuotes(needle);
|
|
4977
|
-
const normFile = normalizeQuotes(haystack);
|
|
4978
|
-
if (normNeedle !== needle || normFile !== haystack) {
|
|
4979
|
-
const m = locateAndCount(haystack, normFile, normNeedle, "quotes");
|
|
4980
|
-
if (m) return m;
|
|
4981
|
-
}
|
|
4982
|
-
const desan = desanitize(needle);
|
|
4983
|
-
if (desan !== needle) {
|
|
4984
|
-
const desanCount = countExactMatches(haystack, desan);
|
|
4985
|
-
if (desanCount > 0) return {
|
|
4986
|
-
actual: desan,
|
|
4987
|
-
occurrences: desanCount,
|
|
4988
|
-
via: "desanitize"
|
|
4989
|
-
};
|
|
4990
|
-
}
|
|
4991
|
-
const combo = desanitize(normNeedle);
|
|
4992
|
-
if (combo !== needle) {
|
|
4993
|
-
const m = locateAndCount(haystack, normFile, combo, "quotes+desanitize");
|
|
4994
|
-
if (m) return m;
|
|
4995
|
-
}
|
|
4996
|
-
const stripped = stripLineNumberPrefixes(needle);
|
|
4997
|
-
if (stripped !== needle && stripped.trim().length > 0) {
|
|
4998
|
-
const count = countExactMatches(haystack, stripped);
|
|
4999
|
-
if (count > 0) return {
|
|
5000
|
-
actual: stripped,
|
|
5001
|
-
occurrences: count,
|
|
5002
|
-
via: "line-numbers"
|
|
5003
|
-
};
|
|
5004
|
-
const strippedNorm = normalizeQuotes(stripped);
|
|
5005
|
-
if (strippedNorm !== stripped || normFile !== haystack) {
|
|
5006
|
-
const m = locateAndCount(haystack, normFile, strippedNorm, "quotes+line-numbers");
|
|
5007
|
-
if (m) return m;
|
|
5008
|
-
}
|
|
5009
|
-
}
|
|
5010
|
-
return null;
|
|
5011
|
-
}
|
|
5012
|
-
/**
|
|
5013
|
-
* Apply the same recovery transforms used to find `old_string` to
|
|
5014
|
-
* `new_string`, so the file gets back its native form: desanitize when
|
|
5015
|
-
* the model emitted `<n>` for `<name>`, strip line-number prefixes when
|
|
5016
|
-
* the match required them, then re-curlify when the match required
|
|
5017
|
-
* quote normalization. Shared between `edit` and `multi_edit`.
|
|
5018
|
-
*/
|
|
5019
|
-
function styleReplacementForVia(replacement, via, actual) {
|
|
5020
|
-
let out = replacement;
|
|
5021
|
-
if (via === "desanitize" || via === "quotes+desanitize") out = desanitize(out);
|
|
5022
|
-
if (via === "line-numbers" || via === "quotes+line-numbers") out = stripLineNumberPrefixes(out);
|
|
5023
|
-
if (via === "quotes" || via === "quotes+desanitize" || via === "quotes+line-numbers") out = preserveQuoteStyle(actual, out);
|
|
5024
|
-
return out;
|
|
5025
|
-
}
|
|
5026
|
-
/**
|
|
5027
|
-
* When `old_string` matched via curly-quote normalization, re-style
|
|
5028
|
-
* `new_string` so the file's typography is preserved across the edit.
|
|
5029
|
-
* Detects whether the matched file region had curly singles, doubles, or
|
|
5030
|
-
* both, and applies the matching curlification to the replacement.
|
|
5031
|
-
*
|
|
5032
|
-
* Apostrophes in contractions (`don't`, `it's`) get the right-single curly
|
|
5033
|
-
* quote regardless of opening context — that's the canonical typographer's
|
|
5034
|
-
* convention for English. Other quotes use a simple
|
|
5035
|
-
* preceded-by-whitespace-or-opening-punctuation heuristic.
|
|
5036
|
-
*/
|
|
5037
|
-
function preserveQuoteStyle(actual, replacement) {
|
|
5038
|
-
const hasDouble = actual.includes("“") || actual.includes("”");
|
|
5039
|
-
const hasSingle = actual.includes("‘") || actual.includes("’");
|
|
5040
|
-
if (!hasDouble && !hasSingle) return replacement;
|
|
5041
|
-
let out = replacement;
|
|
5042
|
-
if (hasDouble) out = applyCurly(out, "\"", "“", "”", false);
|
|
5043
|
-
if (hasSingle) out = applyCurly(out, "'", "‘", "’", true);
|
|
5044
|
-
return out;
|
|
5045
|
-
}
|
|
5046
|
-
function applyCurly(s, straight, left, right, contractionAware) {
|
|
5047
|
-
const chars = [...s];
|
|
5048
|
-
const result = [];
|
|
5049
|
-
for (let i = 0; i < chars.length; i++) {
|
|
5050
|
-
if (chars[i] !== straight) {
|
|
5051
|
-
result.push(chars[i]);
|
|
5052
|
-
continue;
|
|
5053
|
-
}
|
|
5054
|
-
if (contractionAware) {
|
|
5055
|
-
const prev = i > 0 ? chars[i - 1] : "";
|
|
5056
|
-
const next = i < chars.length - 1 ? chars[i + 1] : "";
|
|
5057
|
-
if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
|
|
5058
|
-
result.push(right);
|
|
5059
|
-
continue;
|
|
5060
|
-
}
|
|
5061
|
-
}
|
|
5062
|
-
result.push(isOpeningContext(chars, i) ? left : right);
|
|
5063
|
-
}
|
|
5064
|
-
return result.join("");
|
|
5065
|
-
}
|
|
5066
|
-
function isOpeningContext(chars, i) {
|
|
5067
|
-
if (i === 0) return true;
|
|
5068
|
-
const prev = chars[i - 1];
|
|
5069
|
-
return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "—" || prev === "–";
|
|
5070
|
-
}
|
|
5071
|
-
//#endregion
|
|
5072
6014
|
//#region src/tools/path-suggest.ts
|
|
5073
6015
|
/**
|
|
5074
6016
|
* Find a sibling file in the same directory sharing `path`'s basename
|
|
@@ -5864,12 +6806,13 @@ const readFile$1 = {
|
|
|
5864
6806
|
}
|
|
5865
6807
|
},
|
|
5866
6808
|
async execute({ path, offset, limit, maxBytes, lineNumbers }, ctx) {
|
|
5867
|
-
const
|
|
5868
|
-
if (
|
|
6809
|
+
const extMedia = imageMediaTypeFor(path);
|
|
6810
|
+
if (extMedia) {
|
|
5869
6811
|
const sizeCap = maxBytes !== void 0 ? normalizeInteger(maxBytes, DEFAULT_IMAGE_BYTE_CAP) : DEFAULT_IMAGE_BYTE_CAP;
|
|
5870
6812
|
try {
|
|
5871
6813
|
const { base64, byteLength } = await readFileAsBase64(ctx.execution, ctx.handle, path);
|
|
5872
6814
|
if (sizeCap > 0 && byteLength > sizeCap) return `[image too large to inline: ${path}, ${byteLength} bytes (cap ${sizeCap}). Raise maxBytes, or use shell to inspect.]`;
|
|
6815
|
+
const imgMedia = reconcileImageMediaType(extMedia, base64);
|
|
5873
6816
|
return [{
|
|
5874
6817
|
type: "text",
|
|
5875
6818
|
text: `Image: ${path} (${byteLength} bytes, ${imgMedia})`
|
|
@@ -6006,6 +6949,8 @@ const BUBBLED_EVENTS = [
|
|
|
6006
6949
|
"stream:thinking",
|
|
6007
6950
|
"stream:end",
|
|
6008
6951
|
"stream:error",
|
|
6952
|
+
"stream:server_tool_use",
|
|
6953
|
+
"stream:server_tool_result",
|
|
6009
6954
|
"tool:dispatched",
|
|
6010
6955
|
"tool:before",
|
|
6011
6956
|
"tool:after",
|
|
@@ -6026,6 +6971,8 @@ const CHILD_EVENT_NAME = {
|
|
|
6026
6971
|
"stream:thinking": "child:stream:thinking",
|
|
6027
6972
|
"stream:end": "child:stream:end",
|
|
6028
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",
|
|
6029
6976
|
"tool:dispatched": "child:tool:dispatched",
|
|
6030
6977
|
"tool:before": "child:tool:before",
|
|
6031
6978
|
"tool:after": "child:tool:after",
|
|
@@ -6522,6 +7469,6 @@ const writeFile$1 = {
|
|
|
6522
7469
|
}
|
|
6523
7470
|
};
|
|
6524
7471
|
//#endregion
|
|
6525
|
-
export {
|
|
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 };
|
|
6526
7473
|
|
|
6527
|
-
//# sourceMappingURL=tools-
|
|
7474
|
+
//# sourceMappingURL=tools-BGtJK0vo.js.map
|