autopilot-ai 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/index.js +2888 -1772
- package/dist/index.js.map +1 -1
- package/dist/sdk/index.d.ts +9 -0
- package/dist/sdk/index.js +692 -123
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
package/dist/sdk/index.js
CHANGED
|
@@ -325,9 +325,9 @@ var require_node_gyp_build = __commonJS({
|
|
|
325
325
|
var debug = getFirst(path.join(dir, "build/Debug"), matchBuild);
|
|
326
326
|
if (debug) return debug;
|
|
327
327
|
}
|
|
328
|
-
var prebuild =
|
|
328
|
+
var prebuild = resolve6(dir);
|
|
329
329
|
if (prebuild) return prebuild;
|
|
330
|
-
var nearby =
|
|
330
|
+
var nearby = resolve6(path.dirname(process.execPath));
|
|
331
331
|
if (nearby) return nearby;
|
|
332
332
|
var target = [
|
|
333
333
|
"platform=" + platform3,
|
|
@@ -343,7 +343,7 @@ var require_node_gyp_build = __commonJS({
|
|
|
343
343
|
// eslint-disable-line
|
|
344
344
|
].filter(Boolean).join(" ");
|
|
345
345
|
throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
|
|
346
|
-
function
|
|
346
|
+
function resolve6(dir2) {
|
|
347
347
|
var tuples = readdirSync2(path.join(dir2, "prebuilds")).map(parseTuple);
|
|
348
348
|
var tuple = tuples.filter(matchTuple(platform3, arch)).sort(compareTuples)[0];
|
|
349
349
|
if (!tuple) return;
|
|
@@ -1169,7 +1169,7 @@ var init_types = __esm({
|
|
|
1169
1169
|
// src/hooks/settings.ts
|
|
1170
1170
|
import { existsSync, readFileSync as readFileSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1171
1171
|
import { homedir as homedir10 } from "os";
|
|
1172
|
-
import { join as join17, dirname as
|
|
1172
|
+
import { join as join17, dirname as dirname8 } from "path";
|
|
1173
1173
|
import { createHash } from "crypto";
|
|
1174
1174
|
function globalSettingsPath() {
|
|
1175
1175
|
const xdg = process.env.XDG_CONFIG_HOME || join17(homedir10(), ".config");
|
|
@@ -1196,12 +1196,12 @@ function normalizeHook(event, raw, source) {
|
|
|
1196
1196
|
if (!raw || typeof raw !== "object") return null;
|
|
1197
1197
|
const r = raw;
|
|
1198
1198
|
if (typeof r.command !== "string" || !r.command.trim()) return null;
|
|
1199
|
-
const
|
|
1199
|
+
const enabled2 = typeof r.enabled === "boolean" ? r.enabled : true;
|
|
1200
1200
|
const matcher = typeof r.matcher === "string" ? r.matcher : void 0;
|
|
1201
1201
|
const timeoutMs = typeof r.timeoutMs === "number" && r.timeoutMs > 0 ? r.timeoutMs : void 0;
|
|
1202
1202
|
const description = typeof r.description === "string" ? r.description : void 0;
|
|
1203
1203
|
const id = typeof r.id === "string" && r.id.length > 0 ? r.id : deriveHookId(event, r.command);
|
|
1204
|
-
return { id, matcher, command: r.command, timeoutMs, enabled, description, source };
|
|
1204
|
+
return { id, matcher, command: r.command, timeoutMs, enabled: enabled2, description, source };
|
|
1205
1205
|
}
|
|
1206
1206
|
function mergeHookMaps(a, b) {
|
|
1207
1207
|
const out = {};
|
|
@@ -1237,7 +1237,7 @@ var init_settings = __esm({
|
|
|
1237
1237
|
});
|
|
1238
1238
|
|
|
1239
1239
|
// src/hooks/runner.ts
|
|
1240
|
-
import { spawn as
|
|
1240
|
+
import { spawn as spawn4 } from "child_process";
|
|
1241
1241
|
function isVetoEvent(event) {
|
|
1242
1242
|
return event === "PreToolUse" || event === "UserPromptSubmit";
|
|
1243
1243
|
}
|
|
@@ -1354,8 +1354,8 @@ var init_runner = __esm({
|
|
|
1354
1354
|
init_logger();
|
|
1355
1355
|
DEFAULT_TIMEOUT_MS = 3e4;
|
|
1356
1356
|
STREAM_CAP_BYTES = 4 * 1024;
|
|
1357
|
-
defaultSpawn = (command, payloadJson, env, cwd, timeoutMs, signal) => new Promise((
|
|
1358
|
-
const child =
|
|
1357
|
+
defaultSpawn = (command, payloadJson, env, cwd, timeoutMs, signal) => new Promise((resolve6) => {
|
|
1358
|
+
const child = spawn4(command, {
|
|
1359
1359
|
shell: true,
|
|
1360
1360
|
cwd,
|
|
1361
1361
|
env,
|
|
@@ -1369,7 +1369,7 @@ var init_runner = __esm({
|
|
|
1369
1369
|
settled = true;
|
|
1370
1370
|
clearTimeout(timer2);
|
|
1371
1371
|
signal?.removeEventListener("abort", onAbort);
|
|
1372
|
-
|
|
1372
|
+
resolve6({ exitCode, stdout, stderr, timedOut });
|
|
1373
1373
|
};
|
|
1374
1374
|
const onAbort = () => {
|
|
1375
1375
|
child.kill("SIGTERM");
|
|
@@ -1442,7 +1442,7 @@ var init_manager = __esm({
|
|
|
1442
1442
|
});
|
|
1443
1443
|
|
|
1444
1444
|
// src/sdk/session.ts
|
|
1445
|
-
import { resolve as
|
|
1445
|
+
import { resolve as resolve5 } from "path";
|
|
1446
1446
|
import { homedir as homedir11 } from "os";
|
|
1447
1447
|
import { join as join18 } from "path";
|
|
1448
1448
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -1490,10 +1490,10 @@ async function* readSSE(stream, signal, idleTimeoutMs, postFirstByteIdleTimeoutM
|
|
|
1490
1490
|
gotFirstByte = true;
|
|
1491
1491
|
buffer += decoder.decode(value, { stream: true });
|
|
1492
1492
|
buffer = buffer.replace(/\r\n/g, "\n");
|
|
1493
|
-
let
|
|
1494
|
-
while ((
|
|
1495
|
-
const event = buffer.slice(0,
|
|
1496
|
-
buffer = buffer.slice(
|
|
1493
|
+
let sep3;
|
|
1494
|
+
while ((sep3 = buffer.indexOf("\n\n")) !== -1) {
|
|
1495
|
+
const event = buffer.slice(0, sep3);
|
|
1496
|
+
buffer = buffer.slice(sep3 + 2);
|
|
1497
1497
|
const data = extractData(event);
|
|
1498
1498
|
if (data !== null) yield data;
|
|
1499
1499
|
}
|
|
@@ -2296,11 +2296,11 @@ function extractApiError(parsed, rawText) {
|
|
|
2296
2296
|
return null;
|
|
2297
2297
|
}
|
|
2298
2298
|
function sleep(ms, signal) {
|
|
2299
|
-
return new Promise((
|
|
2299
|
+
return new Promise((resolve6, reject) => {
|
|
2300
2300
|
if (signal?.aborted) return reject(new DOMException("aborted", "AbortError"));
|
|
2301
2301
|
const t = setTimeout(() => {
|
|
2302
2302
|
signal?.removeEventListener("abort", onAbort);
|
|
2303
|
-
|
|
2303
|
+
resolve6();
|
|
2304
2304
|
}, ms);
|
|
2305
2305
|
const onAbort = () => {
|
|
2306
2306
|
clearTimeout(t);
|
|
@@ -2385,7 +2385,7 @@ var RETENTION = {
|
|
|
2385
2385
|
memoryMaxEntries: 1e3
|
|
2386
2386
|
};
|
|
2387
2387
|
async function rotateJsonl(path, maxBytes, rotations) {
|
|
2388
|
-
const { rename } = await import("fs/promises");
|
|
2388
|
+
const { rename: rename3 } = await import("fs/promises");
|
|
2389
2389
|
let s;
|
|
2390
2390
|
try {
|
|
2391
2391
|
s = await stat(path);
|
|
@@ -2397,7 +2397,7 @@ async function rotateJsonl(path, maxBytes, rotations) {
|
|
|
2397
2397
|
const src = i === 1 ? path : `${path}.${i - 1}`;
|
|
2398
2398
|
const dst = `${path}.${i}`;
|
|
2399
2399
|
try {
|
|
2400
|
-
await
|
|
2400
|
+
await rename3(src, dst);
|
|
2401
2401
|
} catch {
|
|
2402
2402
|
}
|
|
2403
2403
|
}
|
|
@@ -3148,7 +3148,7 @@ function truncateForEmbedding(text) {
|
|
|
3148
3148
|
return text.slice(0, MAX_EMBED_CHARS);
|
|
3149
3149
|
}
|
|
3150
3150
|
async function sleep2(ms) {
|
|
3151
|
-
return new Promise((
|
|
3151
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
3152
3152
|
}
|
|
3153
3153
|
async function fetchWithRetry(url, init, retries = 3) {
|
|
3154
3154
|
let lastError;
|
|
@@ -3347,6 +3347,56 @@ init_mode();
|
|
|
3347
3347
|
import { platform, release, homedir as homedir4 } from "os";
|
|
3348
3348
|
import { basename, join as join7 } from "path";
|
|
3349
3349
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
3350
|
+
|
|
3351
|
+
// src/agent/user-shell.ts
|
|
3352
|
+
var enabled = false;
|
|
3353
|
+
function userShellCommandsEnabled() {
|
|
3354
|
+
return enabled;
|
|
3355
|
+
}
|
|
3356
|
+
var USER_SHELL_PROMPT_SECTION = `## Commands the user runs with \`!\`
|
|
3357
|
+
|
|
3358
|
+
The user can run a shell command directly in this session by typing \`!\` followed by the command in the prompt (for example \`! gh auth login\`). It runs in their real terminal with full keyboard input, and the output is added to this conversation for you to read.
|
|
3359
|
+
|
|
3360
|
+
Your \`bash\` tool has no terminal: stdin is closed and there is no TTY, so anything that waits for typed input fails or hangs. Instead of running the command yourself, suggest \`! <exact command>\` when:
|
|
3361
|
+
- it needs a login or interactive auth (\`gh auth login\`, \`npm login\`, \`docker login\`, \`gcloud auth login\`, \`aws sso login\`, \`az login\`, \`wrangler login\`, \`vercel login\`, \u2026)
|
|
3362
|
+
- it prompts for a password, passphrase, one-time code, or confirmation (\`sudo \u2026\`, \`ssh\` with a passphrase or unknown host key, \`git push\` over HTTPS asking for credentials)
|
|
3363
|
+
- it is an interactive program (REPLs, setup wizards without non-interactive flags, \`git rebase -i\`, editors, TUIs)
|
|
3364
|
+
- the user would otherwise have to paste a secret (token, password) into the chat
|
|
3365
|
+
- one of your bash calls failed with a "not logged in", "authentication required", "not a tty", or "terminal required" error, or timed out waiting for input
|
|
3366
|
+
- the user declined permission for a command but still wants it run
|
|
3367
|
+
|
|
3368
|
+
Prefer non-interactive flags when they exist (\`--yes\`, \`-m\`, \`--with-token\` reading an env var, \`CI=1\`) and run the command yourself. Don't suggest \`!\` for commands your bash tool can run. When you do suggest it, give the exact line to type (e.g. "run \`! gh auth login\` in the prompt, then tell me when you're done") and stop; the output will appear in the conversation.`;
|
|
3369
|
+
var AUTH_OR_TTY_OUTPUT = [
|
|
3370
|
+
/not logged in(?:to)?\b/i,
|
|
3371
|
+
/you are not logged in/i,
|
|
3372
|
+
/\b(?:authentication|login|sign[- ]?in) (?:is )?required\b/i,
|
|
3373
|
+
/\bplease (?:run|use) [`'"]?[\w.-]+ (?:auth )?log ?in\b/i,
|
|
3374
|
+
/\bto (?:authenticate|get started with [\w ]+CLI), (?:please )?run\b/i,
|
|
3375
|
+
/\bgh auth login\b/i,
|
|
3376
|
+
/\bnot a tty\b/i,
|
|
3377
|
+
/\binappropriate ioctl for device\b/i,
|
|
3378
|
+
/\b(?:stdin|input) is not a (?:tty|terminal)\b/i,
|
|
3379
|
+
/\b(?:a|no) (?:terminal|tty) (?:is )?(?:required|present)\b/i,
|
|
3380
|
+
/\bmust be run (?:from|in) an? (?:terminal|interactive)/i,
|
|
3381
|
+
/\brequires an interactive (?:terminal|shell|session)\b/i,
|
|
3382
|
+
/\bcannot prompt\b|\bunable to prompt\b|\bprompts? (?:are )?disabled\b/i,
|
|
3383
|
+
/\bcould not read (?:username|password) for\b/i,
|
|
3384
|
+
/\bterminal prompts disabled\b/i,
|
|
3385
|
+
/\bhost key verification failed\b/i
|
|
3386
|
+
];
|
|
3387
|
+
var INTERACTIVE_COMMAND = /^(?:sudo\b|ssh\b|passwd\b|su\b|gh auth login\b|npm (?:login|adduser)\b|yarn (?:npm )?login\b|pnpm login\b|docker login\b|gcloud auth\b|aws (?:sso )?(?:login|configure)\b|az login\b|firebase login\b|vercel login\b|wrangler login\b|netlify login\b|heroku login\b|fly(?:ctl)? auth login\b|op signin\b|gpg\b)/i;
|
|
3388
|
+
var TRAILING_PROMPT = /(?:password|passphrase|username|token|code|otp)[^\n]{0,40}:\s*$|\[(?:y\/n|Y\/n|y\/N)\]\s*$|\(y(?:es)?\/n(?:o)?\)\s*$|\?\s*$/i;
|
|
3389
|
+
function interactiveCommandHint(o) {
|
|
3390
|
+
if (!o.timedOut && o.exitCode === 0) return null;
|
|
3391
|
+
const cmd = o.command.trim().replace(/^cd\s+[^\s&;]+\s*(?:&&|;)\s*/, "");
|
|
3392
|
+
const tail = o.output.slice(-4e3);
|
|
3393
|
+
const looksInteractive = AUTH_OR_TTY_OUTPUT.some((re) => re.test(tail)) || INTERACTIVE_COMMAND.test(cmd) || o.timedOut && TRAILING_PROMPT.test(tail.trimEnd());
|
|
3394
|
+
if (!looksInteractive) return null;
|
|
3395
|
+
const suggestion = cmd.length <= 200 && !cmd.includes("\n") ? `\`! ${cmd}\`` : "`! <command>`";
|
|
3396
|
+
return `[autopilot] This command appears to need a login, a password, or an interactive terminal, which your bash tool cannot provide. Don't retry it as-is. If a non-interactive alternative exists, use it; otherwise ask the user to run ${suggestion} in the prompt and wait for its output to appear in the conversation.`;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// src/agent/system-prompt.ts
|
|
3350
3400
|
var CONTEXT_FILENAMES = ["KIMI.md", "KIMIFLARE.md", "AGENT.md"];
|
|
3351
3401
|
var MAX_CONTEXT_BYTES = 20 * 1024;
|
|
3352
3402
|
function loadContextFile(cwd) {
|
|
@@ -3409,6 +3459,9 @@ If the user asks what model you are, answer with exactly: \`${opts.model}\`. Thi
|
|
|
3409
3459
|
const lspBlock = hasLsp ? "\n\nLSP tools are available for semantic code intelligence. Prefer `lsp_definition` over `grep` when looking for the source of a symbol. Prefer `lsp_references` over `grep` when finding usages. Use `lsp_hover` to confirm types before refactoring." : "";
|
|
3410
3460
|
const tools = `Tools available:
|
|
3411
3461
|
${toolsBlock}`;
|
|
3462
|
+
const userShellBlock = userShellCommandsEnabled() ? `
|
|
3463
|
+
|
|
3464
|
+
${USER_SHELL_PROMPT_SECTION}` : "";
|
|
3412
3465
|
const ctx = loadContextFile(opts.cwd);
|
|
3413
3466
|
const contextBlock = ctx ? `
|
|
3414
3467
|
|
|
@@ -3445,13 +3498,19 @@ ${opts.lspContext}` : "";
|
|
|
3445
3498
|
The following MCP server information was pre-computed by the coordinator.
|
|
3446
3499
|
|
|
3447
3500
|
${opts.mcpContext}` : "";
|
|
3448
|
-
|
|
3501
|
+
const delegationBlock = opts.delegationDirective ? `
|
|
3502
|
+
|
|
3503
|
+
## Subagent policy for this turn
|
|
3504
|
+
|
|
3505
|
+
${opts.delegationDirective}` : "";
|
|
3506
|
+
return identity + "\n\n" + env + "\n\n" + tools + lspBlock + userShellBlock + contextBlock + modeBlock + skillsBlock + memoryBlock + lspContextBlock + mcpContextBlock + delegationBlock;
|
|
3449
3507
|
}
|
|
3450
3508
|
function buildSystemPrompt(opts) {
|
|
3451
3509
|
return buildStaticPrefix(opts) + "\n\n" + buildSessionPrefix(opts);
|
|
3452
3510
|
}
|
|
3453
3511
|
|
|
3454
3512
|
// src/agent/loop.ts
|
|
3513
|
+
var MAX_LOOP_RECOVERIES = 1;
|
|
3455
3514
|
var BudgetExhaustedError = class extends Error {
|
|
3456
3515
|
constructor(message = "Cumulative input token budget exhausted") {
|
|
3457
3516
|
super(message);
|
|
@@ -3459,7 +3518,7 @@ var BudgetExhaustedError = class extends Error {
|
|
|
3459
3518
|
}
|
|
3460
3519
|
};
|
|
3461
3520
|
var AgentLoopError = class extends Error {
|
|
3462
|
-
constructor(message = "Agent got stuck repeating the same tool calls") {
|
|
3521
|
+
constructor(message = "Agent got stuck repeating the same tool calls and was stopped after a recovery attempt") {
|
|
3463
3522
|
super(message);
|
|
3464
3523
|
this.name = "AgentLoopError";
|
|
3465
3524
|
}
|
|
@@ -3516,6 +3575,12 @@ async function runAgentTurn(opts) {
|
|
|
3516
3575
|
const turnStart = performance.now();
|
|
3517
3576
|
logger.info("turn:start", { sessionId: opts.sessionId, codeMode: opts.codeMode ?? false });
|
|
3518
3577
|
const max = opts.maxToolIterations ?? 200;
|
|
3578
|
+
const limitBehavior = opts.toolLimitBehavior ?? (opts.continueOnLimit ? "continue" : "throw");
|
|
3579
|
+
const requestedCeiling = opts.maxTotalToolIterations;
|
|
3580
|
+
const hardCeiling = Math.max(
|
|
3581
|
+
max,
|
|
3582
|
+
requestedCeiling !== void 0 && Number.isFinite(requestedCeiling) && requestedCeiling > 0 ? Math.floor(requestedCeiling) : max * 5
|
|
3583
|
+
);
|
|
3519
3584
|
const codeMode = opts.codeMode ?? false;
|
|
3520
3585
|
const fireStopHook = async () => {
|
|
3521
3586
|
if (opts.signal.aborted) return;
|
|
@@ -3582,7 +3647,8 @@ async function runAgentTurn(opts) {
|
|
|
3582
3647
|
model: opts.model,
|
|
3583
3648
|
mode: opts.mode,
|
|
3584
3649
|
skillContext: skillResult.skillContext,
|
|
3585
|
-
preferPullRequests: opts.preferPullRequests
|
|
3650
|
+
preferPullRequests: opts.preferPullRequests,
|
|
3651
|
+
delegationDirective: opts.delegationDirective
|
|
3586
3652
|
})
|
|
3587
3653
|
};
|
|
3588
3654
|
} else {
|
|
@@ -3594,7 +3660,8 @@ async function runAgentTurn(opts) {
|
|
|
3594
3660
|
model: opts.model,
|
|
3595
3661
|
mode: opts.mode,
|
|
3596
3662
|
skillContext: skillResult.skillContext,
|
|
3597
|
-
preferPullRequests: opts.preferPullRequests
|
|
3663
|
+
preferPullRequests: opts.preferPullRequests,
|
|
3664
|
+
delegationDirective: opts.delegationDirective
|
|
3598
3665
|
})
|
|
3599
3666
|
};
|
|
3600
3667
|
}
|
|
@@ -3665,8 +3732,11 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
|
|
|
3665
3732
|
const WEB_FETCH_DOMAIN_THRESHOLD = 2;
|
|
3666
3733
|
let cumulativePromptTokens = 0;
|
|
3667
3734
|
let iter = 0;
|
|
3735
|
+
let totalIter = 0;
|
|
3668
3736
|
let budgetExhausted = false;
|
|
3669
3737
|
let loopExhausted = false;
|
|
3738
|
+
let loopRecoveries = 0;
|
|
3739
|
+
let finalizeReason = null;
|
|
3670
3740
|
let currentTasks = [];
|
|
3671
3741
|
let mutatingToolsSinceLastTasksSet = 0;
|
|
3672
3742
|
const MUTATING_TOOLS2 = /* @__PURE__ */ new Set(["write", "edit", "bash"]);
|
|
@@ -3684,36 +3754,43 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
|
|
|
3684
3754
|
content: "You have reached the cumulative input token budget for this session. Please synthesize your findings and provide a final summary of what was accomplished."
|
|
3685
3755
|
});
|
|
3686
3756
|
}
|
|
3687
|
-
if (
|
|
3757
|
+
if (finalizeReason === null && totalIter >= hardCeiling) {
|
|
3758
|
+
finalizeReason = "limit_ceiling";
|
|
3688
3759
|
opts.messages.push({
|
|
3689
3760
|
role: "system",
|
|
3690
|
-
content:
|
|
3761
|
+
content: `You have reached the hard safety ceiling of ${hardCeiling} tool iterations for this turn. Do not call any more tools. Summarize what you accomplished, what remains unfinished, and the concrete next steps needed to complete the task.`
|
|
3691
3762
|
});
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
content: "You have reached the tool-call limit for this session. The counter has been reset so you can continue working. Please proceed with your task."
|
|
3700
|
-
});
|
|
3701
|
-
iter = 0;
|
|
3702
|
-
} else {
|
|
3703
|
-
await fireStopHook();
|
|
3704
|
-
return;
|
|
3705
|
-
}
|
|
3706
|
-
} else if (opts.continueOnLimit) {
|
|
3763
|
+
opts.callbacks.onGuardrail?.({
|
|
3764
|
+
kind: "limit_ceiling",
|
|
3765
|
+
message: `Tool-iteration ceiling (${hardCeiling}) reached \u2014 wrapping up the turn with a summary.`,
|
|
3766
|
+
totalIterations: totalIter
|
|
3767
|
+
});
|
|
3768
|
+
} else if (finalizeReason === null && iter >= max) {
|
|
3769
|
+
if (limitBehavior === "continue") {
|
|
3707
3770
|
opts.messages.push({
|
|
3708
3771
|
role: "system",
|
|
3709
3772
|
content: "You have reached the tool-call limit for this session. The counter has been reset so you can continue working. Please proceed with your task."
|
|
3710
3773
|
});
|
|
3711
3774
|
iter = 0;
|
|
3775
|
+
opts.callbacks.onGuardrail?.({
|
|
3776
|
+
kind: "limit_reset",
|
|
3777
|
+
message: `Tool-call limit (${max}) reached \u2014 counter reset automatically (${totalIter}/${hardCeiling} total).`,
|
|
3778
|
+
totalIterations: totalIter
|
|
3779
|
+
});
|
|
3780
|
+
} else if (limitBehavior === "stop") {
|
|
3781
|
+
opts.callbacks.onGuardrail?.({
|
|
3782
|
+
kind: "limit_stopped",
|
|
3783
|
+
message: `Tool-call limit (${max}) reached \u2014 turn stopped.`,
|
|
3784
|
+
totalIterations: totalIter
|
|
3785
|
+
});
|
|
3786
|
+
await fireStopHook();
|
|
3787
|
+
return;
|
|
3712
3788
|
} else {
|
|
3713
3789
|
throw new Error(`kimiflare: tool iteration limit reached (${max})`);
|
|
3714
3790
|
}
|
|
3715
3791
|
}
|
|
3716
3792
|
iter++;
|
|
3793
|
+
totalIter++;
|
|
3717
3794
|
turn++;
|
|
3718
3795
|
const previousMessages = opts.messages.slice();
|
|
3719
3796
|
const toolCalls = [];
|
|
@@ -3852,8 +3929,21 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
|
|
|
3852
3929
|
}))
|
|
3853
3930
|
} : {}
|
|
3854
3931
|
};
|
|
3932
|
+
if (finalizeReason !== null) {
|
|
3933
|
+
delete assistantMsg.tool_calls;
|
|
3934
|
+
if (!assistantMsg.content) {
|
|
3935
|
+
assistantMsg.content = finalizeReason === "loop" ? "Stopped: I kept repeating blocked tool calls and could not make further progress on this task." : `Stopped: reached the ${hardCeiling}-iteration safety ceiling for this turn before finishing.`;
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3855
3938
|
opts.messages.push(assistantMsg);
|
|
3856
3939
|
opts.callbacks.onAssistantFinal?.(assistantMsg);
|
|
3940
|
+
if (finalizeReason !== null) {
|
|
3941
|
+
logger.info("turn:finalized", { sessionId: opts.sessionId, reason: finalizeReason, totalIter });
|
|
3942
|
+
if (budgetExhausted) throw new BudgetExhaustedError();
|
|
3943
|
+
await fireStopHook();
|
|
3944
|
+
if (finalizeReason === "loop") throw new AgentLoopError();
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3857
3947
|
if (toolCalls.length === 0) {
|
|
3858
3948
|
if (opts.sessionId && lastUsage) {
|
|
3859
3949
|
void logTurnDebug({
|
|
@@ -3881,9 +3971,11 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
|
|
|
3881
3971
|
return;
|
|
3882
3972
|
}
|
|
3883
3973
|
let blockedCount = 0;
|
|
3974
|
+
const availableToolNames = new Set(opts.tools.map((tool) => tool.name));
|
|
3975
|
+
if (codeMode) availableToolNames.add("execute_code");
|
|
3884
3976
|
const allReadOnly = toolCalls.length > 1 && toolCalls.every((tc) => {
|
|
3885
3977
|
const tool = opts.executor.list().find((t) => t.name === tc.function.name);
|
|
3886
|
-
return tool?.isReadOnly === true;
|
|
3978
|
+
return availableToolNames.has(tc.function.name) && tool?.isReadOnly === true;
|
|
3887
3979
|
});
|
|
3888
3980
|
if (allReadOnly) {
|
|
3889
3981
|
opts.callbacks.onInfo?.(`${toolCalls.length} read-only tools running in parallel`);
|
|
@@ -4123,6 +4215,26 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
|
|
|
4123
4215
|
for (const [i, tc] of toolCalls.entries()) {
|
|
4124
4216
|
if (opts.signal.aborted) throw new DOMException("aborted", "AbortError");
|
|
4125
4217
|
const loopSignature = `${tc.function.name}:${stableStringify(tc.function.arguments)}`;
|
|
4218
|
+
if (!availableToolNames.has(tc.function.name)) {
|
|
4219
|
+
const unavailableResult = {
|
|
4220
|
+
tool_call_id: tc.id,
|
|
4221
|
+
name: tc.function.name,
|
|
4222
|
+
content: `Tool ${tc.function.name} is not available under this turn's policy. Continue without it or ask the user to enable it.`,
|
|
4223
|
+
ok: false
|
|
4224
|
+
};
|
|
4225
|
+
toolResults.push(unavailableResult);
|
|
4226
|
+
opts.messages.push({
|
|
4227
|
+
role: "tool",
|
|
4228
|
+
tool_call_id: tc.id,
|
|
4229
|
+
content: sanitizeString(unavailableResult.content),
|
|
4230
|
+
name: tc.function.name
|
|
4231
|
+
});
|
|
4232
|
+
opts.callbacks.onToolResult?.(unavailableResult);
|
|
4233
|
+
recentToolCalls.push(loopSignature);
|
|
4234
|
+
if (recentToolCalls.length > LOOP_WINDOW) recentToolCalls.shift();
|
|
4235
|
+
blockedCount++;
|
|
4236
|
+
continue;
|
|
4237
|
+
}
|
|
4126
4238
|
const loopCount = recentToolCalls.filter((s) => s === loopSignature).length;
|
|
4127
4239
|
if (loopCount >= LOOP_THRESHOLD) {
|
|
4128
4240
|
const warning = `Loop detected: you have called ${tc.function.name} with the same arguments multiple times in a row. Consider a different approach.`;
|
|
@@ -4485,30 +4597,30 @@ ${sandboxResult.output}` : sandboxResult.output;
|
|
|
4485
4597
|
throw new BudgetExhaustedError();
|
|
4486
4598
|
}
|
|
4487
4599
|
if (loopExhausted) {
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
role: "system",
|
|
4502
|
-
content: "You were stuck calling the same tools with identical arguments. Please synthesize and conclude your findings so far. Do not call any more tools."
|
|
4503
|
-
});
|
|
4504
|
-
loopExhausted = false;
|
|
4505
|
-
recentToolCalls.length = 0;
|
|
4506
|
-
continue;
|
|
4507
|
-
}
|
|
4508
|
-
await fireStopHook();
|
|
4509
|
-
return;
|
|
4600
|
+
loopExhausted = false;
|
|
4601
|
+
recentToolCalls.length = 0;
|
|
4602
|
+
if (loopRecoveries < MAX_LOOP_RECOVERIES) {
|
|
4603
|
+
loopRecoveries++;
|
|
4604
|
+
opts.messages.push({
|
|
4605
|
+
role: "system",
|
|
4606
|
+
content: "Every tool call in your last step was blocked because you kept repeating the same calls with identical arguments. Do not repeat them. Either take a genuinely different approach (different tools, arguments, or files), or, if you already have enough information, stop calling tools and give your final answer."
|
|
4607
|
+
});
|
|
4608
|
+
opts.callbacks.onGuardrail?.({
|
|
4609
|
+
kind: "loop_recovery",
|
|
4610
|
+
message: "Loop guardrail: repeated tool calls were blocked \u2014 asked the agent to change approach."
|
|
4611
|
+
});
|
|
4612
|
+
continue;
|
|
4510
4613
|
}
|
|
4511
|
-
|
|
4614
|
+
finalizeReason = "loop";
|
|
4615
|
+
opts.messages.push({
|
|
4616
|
+
role: "system",
|
|
4617
|
+
content: "You are still repeating blocked tool calls after being asked to change approach, so this turn is ending. Do not call any more tools. Summarize what you accomplished, what is blocking you, and what the user could do next."
|
|
4618
|
+
});
|
|
4619
|
+
opts.callbacks.onGuardrail?.({
|
|
4620
|
+
kind: "loop_stopped",
|
|
4621
|
+
message: "Loop guardrail: the agent kept repeating blocked tool calls \u2014 ending the turn with a summary."
|
|
4622
|
+
});
|
|
4623
|
+
continue;
|
|
4512
4624
|
}
|
|
4513
4625
|
}
|
|
4514
4626
|
}
|
|
@@ -4875,15 +4987,15 @@ function makeErrorOutput(message) {
|
|
|
4875
4987
|
return { content: message, rawBytes: bytes, reducedBytes: bytes };
|
|
4876
4988
|
}
|
|
4877
4989
|
async function execOnce(command, cwd) {
|
|
4878
|
-
return new Promise((
|
|
4990
|
+
return new Promise((resolve6) => {
|
|
4879
4991
|
const child = spawn("bash", ["-lc", command], { cwd });
|
|
4880
4992
|
let out = "";
|
|
4881
4993
|
child.stdout.on("data", (d) => {
|
|
4882
4994
|
out += d.toString("utf8");
|
|
4883
4995
|
});
|
|
4884
4996
|
child.on("close", (code) => {
|
|
4885
|
-
if (code !== 0) return
|
|
4886
|
-
|
|
4997
|
+
if (code !== 0) return resolve6(void 0);
|
|
4998
|
+
resolve6(out.trim() || void 0);
|
|
4887
4999
|
});
|
|
4888
5000
|
});
|
|
4889
5001
|
}
|
|
@@ -5021,7 +5133,7 @@ async function runBash(args, ctx) {
|
|
|
5021
5133
|
if (pushGuard) return pushGuard;
|
|
5022
5134
|
const { shell, args: shellArgs, isPosix } = getShellCommand(ctx.shell);
|
|
5023
5135
|
const command = isPosix ? injectCoauthor(args.command, ctx.coauthor) : args.command;
|
|
5024
|
-
return new Promise((
|
|
5136
|
+
return new Promise((resolve6, reject) => {
|
|
5025
5137
|
logger.debug("bash:spawn", { command: args.command.slice(0, 200), cwd: ctx.cwd, shell });
|
|
5026
5138
|
const child = spawn(shell, [...shellArgs, command], {
|
|
5027
5139
|
cwd: ctx.cwd,
|
|
@@ -5030,6 +5142,7 @@ async function runBash(args, ctx) {
|
|
|
5030
5142
|
GIT_EDITOR: "true"
|
|
5031
5143
|
}
|
|
5032
5144
|
});
|
|
5145
|
+
child.stdin?.end();
|
|
5033
5146
|
let stdout = "";
|
|
5034
5147
|
let stderr = "";
|
|
5035
5148
|
let killedByTimeout = false;
|
|
@@ -5073,8 +5186,18 @@ ${stdout.trimEnd()}`);
|
|
|
5073
5186
|
if (stderr) parts.push(`--- stderr ---
|
|
5074
5187
|
${stderr.trimEnd()}`);
|
|
5075
5188
|
if (!stdout && !stderr) parts.push("(no output)");
|
|
5189
|
+
if (userShellCommandsEnabled() && !killedByAbort) {
|
|
5190
|
+
const hint = interactiveCommandHint({
|
|
5191
|
+
command: args.command,
|
|
5192
|
+
exitCode: code,
|
|
5193
|
+
timedOut: killedByTimeout,
|
|
5194
|
+
output: `${stdout}
|
|
5195
|
+
${stderr}`
|
|
5196
|
+
});
|
|
5197
|
+
if (hint) parts.push(hint);
|
|
5198
|
+
}
|
|
5076
5199
|
const raw = parts.join("\n");
|
|
5077
|
-
|
|
5200
|
+
resolve6({
|
|
5078
5201
|
content: raw,
|
|
5079
5202
|
rawBytes: Buffer.byteLength(raw, "utf8"),
|
|
5080
5203
|
reducedBytes: Buffer.byteLength(raw, "utf8")
|
|
@@ -6419,7 +6542,7 @@ var browserFetchTool = {
|
|
|
6419
6542
|
};
|
|
6420
6543
|
async function autoScroll(page) {
|
|
6421
6544
|
await page.evaluate(async () => {
|
|
6422
|
-
await new Promise((
|
|
6545
|
+
await new Promise((resolve6) => {
|
|
6423
6546
|
let totalHeight = 0;
|
|
6424
6547
|
const distance = 300;
|
|
6425
6548
|
const timer2 = setInterval(() => {
|
|
@@ -6428,12 +6551,12 @@ async function autoScroll(page) {
|
|
|
6428
6551
|
totalHeight += distance;
|
|
6429
6552
|
if (totalHeight >= scrollHeight) {
|
|
6430
6553
|
clearInterval(timer2);
|
|
6431
|
-
|
|
6554
|
+
resolve6();
|
|
6432
6555
|
}
|
|
6433
6556
|
}, 100);
|
|
6434
6557
|
setTimeout(() => {
|
|
6435
6558
|
clearInterval(timer2);
|
|
6436
|
-
|
|
6559
|
+
resolve6();
|
|
6437
6560
|
}, 1e4);
|
|
6438
6561
|
});
|
|
6439
6562
|
});
|
|
@@ -6658,9 +6781,13 @@ function readReasoningEffortEnv() {
|
|
|
6658
6781
|
const raw = process.env.KIMI_REASONING_EFFORT?.toLowerCase();
|
|
6659
6782
|
return EFFORTS.includes(raw ?? "") ? raw : void 0;
|
|
6660
6783
|
}
|
|
6784
|
+
function readSubagentPolicyEnv() {
|
|
6785
|
+
const raw = process.env.KIMIFLARE_SUBAGENT_POLICY?.toLowerCase();
|
|
6786
|
+
return raw === "off" || raw === "suggest" || raw === "auto" ? raw : void 0;
|
|
6787
|
+
}
|
|
6661
6788
|
function readCoauthorEnv() {
|
|
6662
|
-
const
|
|
6663
|
-
if (
|
|
6789
|
+
const enabled2 = process.env.KIMIFLARE_COAUTHOR;
|
|
6790
|
+
if (enabled2 === "0" || enabled2 === "false") return void 0;
|
|
6664
6791
|
const name = process.env.KIMIFLARE_COAUTHOR_NAME || "kimiflare";
|
|
6665
6792
|
const email = process.env.KIMIFLARE_COAUTHOR_EMAIL || "kimiflare@proton.me";
|
|
6666
6793
|
return { enabled: true, name, email };
|
|
@@ -6694,6 +6821,7 @@ async function loadConfig() {
|
|
|
6694
6821
|
if (!openrouterApiKey && !baseUrl) return null;
|
|
6695
6822
|
const envModel = process.env.KIMI_MODEL || void 0;
|
|
6696
6823
|
const envEffort = readReasoningEffortEnv();
|
|
6824
|
+
const envSubagentPolicy = readSubagentPolicyEnv();
|
|
6697
6825
|
const envCoauthor = readCoauthorEnv();
|
|
6698
6826
|
const envCacheStable = process.env.KIMIFLARE_CACHE_STABLE_PROMPTS;
|
|
6699
6827
|
const cacheStablePrompts = envCacheStable === "0" || envCacheStable === "false" ? false : true;
|
|
@@ -6714,6 +6842,7 @@ async function loadConfig() {
|
|
|
6714
6842
|
model: m(envModel ?? persisted.model) ?? DEFAULT_MODEL,
|
|
6715
6843
|
openrouterProvider: persisted.openrouterProvider,
|
|
6716
6844
|
reasoningEffort: envEffort ?? persisted.reasoningEffort,
|
|
6845
|
+
subagentPolicy: envSubagentPolicy ?? (persisted.subagentPolicy === "off" || persisted.subagentPolicy === "suggest" || persisted.subagentPolicy === "auto" ? persisted.subagentPolicy : "suggest"),
|
|
6717
6846
|
modesEnabled: persisted.modesEnabled,
|
|
6718
6847
|
coauthor: envCoauthor?.enabled ?? persisted.coauthor ?? true,
|
|
6719
6848
|
coauthorName: envCoauthor?.name ?? persisted.coauthorName,
|
|
@@ -6722,6 +6851,7 @@ async function loadConfig() {
|
|
|
6722
6851
|
cacheStablePrompts: persisted.cacheStablePrompts ?? cacheStablePrompts,
|
|
6723
6852
|
compiledContext: persisted.compiledContext ?? compiledContext,
|
|
6724
6853
|
imageHistoryTurns: imageHistoryTurns === void 0 || Number.isNaN(imageHistoryTurns) ? persisted.imageHistoryTurns : imageHistoryTurns,
|
|
6854
|
+
maxTotalToolIterations: readNumberEnv("KIMIFLARE_MAX_TOTAL_TOOL_ITERATIONS") ?? persisted.maxTotalToolIterations,
|
|
6725
6855
|
memoryEnabled: readBooleanEnv("KIMIFLARE_MEMORY_ENABLED") ?? persisted.memoryEnabled ?? false,
|
|
6726
6856
|
memoryDbPath: process.env.KIMIFLARE_MEMORY_DB_PATH ?? persisted.memoryDbPath,
|
|
6727
6857
|
memoryMaxAgeDays: readNumberEnv("KIMIFLARE_MEMORY_MAX_AGE_DAYS") ?? persisted.memoryMaxAgeDays,
|
|
@@ -6746,6 +6876,7 @@ async function loadConfig() {
|
|
|
6746
6876
|
githubTokenExpiry: persisted.githubTokenExpiry,
|
|
6747
6877
|
githubRepo: persisted.githubRepo,
|
|
6748
6878
|
workerEndpoint: process.env.KIMIFLARE_WORKER_ENDPOINT ?? persisted.workerEndpoint,
|
|
6879
|
+
workerBackend: process.env.KIMIFLARE_WORKER_BACKEND === "hotcell" ? "hotcell" : process.env.KIMIFLARE_WORKER_BACKEND === "remote" ? "remote" : persisted.workerBackend === "hotcell" ? "hotcell" : "remote",
|
|
6749
6880
|
workerBudgetUsd: readNumberEnv("KIMIFLARE_WORKER_BUDGET_USD") ?? persisted.workerBudgetUsd,
|
|
6750
6881
|
workerBudgetMaxUsd: readNumberEnv("KIMIFLARE_WORKER_BUDGET_MAX_USD") ?? persisted.workerBudgetMaxUsd,
|
|
6751
6882
|
workerMaxParallel: readNumberEnv("KIMIFLARE_WORKER_MAX_PARALLEL") ?? persisted.workerMaxParallel,
|
|
@@ -6851,6 +6982,330 @@ async function saveConfig(cfg) {
|
|
|
6851
6982
|
return p;
|
|
6852
6983
|
}
|
|
6853
6984
|
|
|
6985
|
+
// src/tools/hotcell-worker.ts
|
|
6986
|
+
import { spawn as spawn2 } from "child_process";
|
|
6987
|
+
import { promisify as promisify2 } from "util";
|
|
6988
|
+
import { execFile as execFile2 } from "child_process";
|
|
6989
|
+
import { resolve as resolve3 } from "path";
|
|
6990
|
+
var execFileAsync = promisify2(execFile2);
|
|
6991
|
+
var MAX_OUTPUT_BYTES = 1e6;
|
|
6992
|
+
var MAX_PROMPT_CHARS = 4e4;
|
|
6993
|
+
var DEFAULT_CELL_TIMEOUT_MS = 3e5;
|
|
6994
|
+
var MAX_PARALLEL_WORKERS = 3;
|
|
6995
|
+
var runningWorkers = 0;
|
|
6996
|
+
var workerQueue = [];
|
|
6997
|
+
async function runHotcellWorker(options) {
|
|
6998
|
+
const workerId = `hotcell-${crypto.randomUUID().slice(0, 8)}`;
|
|
6999
|
+
const cwd = resolve3(options.cwd ?? process.cwd());
|
|
7000
|
+
const model = options.model.trim();
|
|
7001
|
+
if (!model) throw new Error("Hotcell workers require the coordinator's explicit model ID.");
|
|
7002
|
+
validateModelId(model);
|
|
7003
|
+
if (options.task.length + (options.context?.length ?? 0) > MAX_PROMPT_CHARS) {
|
|
7004
|
+
throw new Error(`Hotcell worker prompt exceeds the ${MAX_PROMPT_CHARS.toLocaleString()} character limit.`);
|
|
7005
|
+
}
|
|
7006
|
+
if (!Number.isFinite(options.budgetUsd) || options.budgetUsd <= 0) {
|
|
7007
|
+
throw new Error("Hotcell worker spend cap must be a positive number.");
|
|
7008
|
+
}
|
|
7009
|
+
if (options.signal?.aborted) return terminalResult(workerId, options.task, "cancelled", "Cancelled before worker setup.");
|
|
7010
|
+
await acquireSlot(options.maxParallel ?? MAX_PARALLEL_WORKERS, options.signal);
|
|
7011
|
+
let cellId;
|
|
7012
|
+
let result;
|
|
7013
|
+
const command = options.hotcellCommand ?? process.env.HOTCELL_BIN ?? "hotcell";
|
|
7014
|
+
const execute = options.processRunner ?? runProcess;
|
|
7015
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CELL_TIMEOUT_MS;
|
|
7016
|
+
try {
|
|
7017
|
+
const repo = await getCleanRepository(cwd);
|
|
7018
|
+
const cellName = "autopilot-" + workerId;
|
|
7019
|
+
const created = await execute(command, [
|
|
7020
|
+
"create",
|
|
7021
|
+
"-n",
|
|
7022
|
+
"1",
|
|
7023
|
+
"--name",
|
|
7024
|
+
cellName,
|
|
7025
|
+
"--repo",
|
|
7026
|
+
repo.url,
|
|
7027
|
+
"--ref",
|
|
7028
|
+
repo.ref,
|
|
7029
|
+
"--egress",
|
|
7030
|
+
"--egress-spend-cap",
|
|
7031
|
+
String(options.budgetUsd),
|
|
7032
|
+
"--memory",
|
|
7033
|
+
"1024",
|
|
7034
|
+
"--cpus",
|
|
7035
|
+
"2"
|
|
7036
|
+
], { cwd, signal: options.signal, timeoutMs: 3e5 });
|
|
7037
|
+
cellId = parseCellId(created.stdout);
|
|
7038
|
+
if (!cellId) {
|
|
7039
|
+
try {
|
|
7040
|
+
const cells = await execute(command, ["ls"], { cwd, timeoutMs: 1e4 });
|
|
7041
|
+
cellId = parseNamedCellId(cells.stdout, cellName);
|
|
7042
|
+
} catch {
|
|
7043
|
+
}
|
|
7044
|
+
}
|
|
7045
|
+
if (created.timedOut || created.aborted) {
|
|
7046
|
+
result = terminalResult(
|
|
7047
|
+
workerId,
|
|
7048
|
+
options.task,
|
|
7049
|
+
options.signal?.aborted ? "cancelled" : "timed_out",
|
|
7050
|
+
options.signal?.aborted ? "Cancelled during Hotcell setup." : "Hotcell setup timed out before the worker started.",
|
|
7051
|
+
model
|
|
7052
|
+
);
|
|
7053
|
+
return result;
|
|
7054
|
+
}
|
|
7055
|
+
if (created.code !== 0) throw new Error(formatSetupFailure(created));
|
|
7056
|
+
if (!cellId) throw new Error("Hotcell created a sandbox but did not return a recognizable cell ID.");
|
|
7057
|
+
if (/no providers configured|providers:\s*\(none\)/i.test(created.stdout)) {
|
|
7058
|
+
result = terminalResult(workerId, options.task, "failed", "Hotcell has no OpenRouter gateway route. Run `hotcell keys add openrouter` on the daemon host, then retry.", model);
|
|
7059
|
+
return result;
|
|
7060
|
+
}
|
|
7061
|
+
const prompt = [
|
|
7062
|
+
"You are an isolated, read-only research worker. You cannot edit files, run shell commands, push, publish, or call write-capable integrations.",
|
|
7063
|
+
"Use the exact requested model and return concise findings, recommendations, and relevant file paths. Do not claim to have changed files.",
|
|
7064
|
+
`Mission:
|
|
7065
|
+
${options.task}`,
|
|
7066
|
+
options.context ? `Coordinator context:
|
|
7067
|
+
${options.context}` : ""
|
|
7068
|
+
].filter(Boolean).join("\n\n");
|
|
7069
|
+
const encodedPrompt = Buffer.from(prompt, "utf8").toString("base64");
|
|
7070
|
+
const shellCommand = [
|
|
7071
|
+
'GIT_DIR="$(find /workspace -mindepth 2 -maxdepth 5 -name .git -print -quit)"',
|
|
7072
|
+
'test -n "$GIT_DIR"',
|
|
7073
|
+
'REPO_ROOT="${GIT_DIR%/.git}"',
|
|
7074
|
+
`git -C "$REPO_ROOT" fetch --quiet origin ${shellQuote(repo.commit)}`,
|
|
7075
|
+
`git -C "$REPO_ROOT" checkout --quiet --detach ${shellQuote(repo.commit)}`,
|
|
7076
|
+
'cd "$REPO_ROOT"',
|
|
7077
|
+
"npm ci --no-audit --no-fund --loglevel=error 1>&2",
|
|
7078
|
+
'export OPENROUTER_BASE_URL="${OPENROUTER_BASE_URL%/}/v1"',
|
|
7079
|
+
`PROMPT="$(printf '%s' '${encodedPrompt}' | base64 -d)"`,
|
|
7080
|
+
`node --import tsx "$REPO_ROOT/src/index.tsx" --format json --max-input-tokens 14000 --model ${shellQuote(model)} --worker-profile research -p "$PROMPT"`
|
|
7081
|
+
].join(" && ");
|
|
7082
|
+
const execution = await execute(command, ["exec", cellId, shellCommand, "--cwd", "/workspace"], {
|
|
7083
|
+
cwd,
|
|
7084
|
+
signal: options.signal,
|
|
7085
|
+
timeoutMs
|
|
7086
|
+
});
|
|
7087
|
+
const stats = await execute(command, ["stats", cellId], { cwd, timeoutMs: 3e4 }).catch(() => void 0);
|
|
7088
|
+
const metrics = stats?.code === 0 ? parseHotcellStats(stats.stdout) : {};
|
|
7089
|
+
const json = parsePrintOutput(execution.stdout);
|
|
7090
|
+
const summary = typeof json?.text === "string" ? json.text.trim() : execution.stdout.trim();
|
|
7091
|
+
const tokensUsed = typeof json?.usage?.totalTokens === "number" ? json.usage.totalTokens : 0;
|
|
7092
|
+
const budgetExhausted = execution.code === 42;
|
|
7093
|
+
const diagnostic = `${execution.stdout}
|
|
7094
|
+
${execution.stderr}`.toLowerCase();
|
|
7095
|
+
const executionDiagnostic = sanitizeHotcellDiagnostic(execution.stderr);
|
|
7096
|
+
const spendExhausted = /(?:http\s*)?402|spend cap|spending limit/.test(diagnostic);
|
|
7097
|
+
const status = execution.timedOut ? "timed_out" : execution.aborted ? options.signal?.aborted ? "cancelled" : "failed" : spendExhausted ? "spend_exhausted" : budgetExhausted ? "budget_exhausted" : execution.code === 0 ? "completed" : "failed";
|
|
7098
|
+
const findings = summary ? [{ topic: "Research findings", summary: summary.slice(0, 4e4), confidence: "medium", sources: [], relevance: "high" }] : [];
|
|
7099
|
+
result = {
|
|
7100
|
+
workerId,
|
|
7101
|
+
status,
|
|
7102
|
+
task: options.task,
|
|
7103
|
+
findings,
|
|
7104
|
+
recommendations: [],
|
|
7105
|
+
filesRead: [],
|
|
7106
|
+
webSources: [],
|
|
7107
|
+
costUsd: metrics.costUsd ?? 0,
|
|
7108
|
+
tokensUsed: metrics.tokensUsed ?? tokensUsed,
|
|
7109
|
+
reasoning: "",
|
|
7110
|
+
model,
|
|
7111
|
+
exitCode: execution.code,
|
|
7112
|
+
...budgetExhausted || spendExhausted ? { budgetExceeded: true, partialResult: findings.length > 0 } : {},
|
|
7113
|
+
...execution.code !== 0 && !budgetExhausted ? {
|
|
7114
|
+
error: status === "timed_out" ? "Worker timed out." : status === "cancelled" ? "Worker cancelled." : spendExhausted ? "Hotcell per-cell spend cap was reached." : execution.outputLimitExceeded ? "Worker output exceeded the 1 MB result limit." : /unknown model|model.{0,30}(?:not found|invalid|unavailable|unsupported)/i.test(diagnostic) ? "The requested model is unavailable in the Hotcell OpenRouter gateway." : /no openrouter api key|openrouter.{0,30}(?:key|gateway).{0,30}(?:missing|not configured)/i.test(diagnostic) ? "Hotcell OpenRouter gateway credentials are not configured. Run `hotcell keys add openrouter` on the daemon host." : `Worker process exited with code ${execution.code}${executionDiagnostic ? `: ${executionDiagnostic}` : "."}`
|
|
7115
|
+
} : {}
|
|
7116
|
+
};
|
|
7117
|
+
} catch (error) {
|
|
7118
|
+
const message = error instanceof Error ? error.message : "Unknown Hotcell worker error.";
|
|
7119
|
+
result = terminalResult(workerId, options.task, options.signal?.aborted ? "cancelled" : "failed", message, model);
|
|
7120
|
+
} finally {
|
|
7121
|
+
if (cellId) {
|
|
7122
|
+
try {
|
|
7123
|
+
const removed = await execute(command, ["rm", cellId], { cwd, timeoutMs: 3e4 });
|
|
7124
|
+
if (removed.code !== 0 && result) {
|
|
7125
|
+
result = {
|
|
7126
|
+
...result,
|
|
7127
|
+
status: result.status === "completed" ? "failed" : result.status,
|
|
7128
|
+
error: [result.error, "Hotcell cleanup failed; remove the cell manually to revoke its gateway token."].filter(Boolean).join(" ")
|
|
7129
|
+
};
|
|
7130
|
+
}
|
|
7131
|
+
} catch {
|
|
7132
|
+
if (result) {
|
|
7133
|
+
result = {
|
|
7134
|
+
...result,
|
|
7135
|
+
status: result.status === "completed" ? "failed" : result.status,
|
|
7136
|
+
error: [result.error, "Hotcell cleanup failed; remove the cell manually to revoke its gateway token."].filter(Boolean).join(" ")
|
|
7137
|
+
};
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
releaseSlot();
|
|
7142
|
+
}
|
|
7143
|
+
return result;
|
|
7144
|
+
}
|
|
7145
|
+
async function getCleanRepository(cwd) {
|
|
7146
|
+
const root = (await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd })).stdout.trim();
|
|
7147
|
+
const status = (await execFileAsync("git", ["status", "--porcelain"], { cwd: root })).stdout;
|
|
7148
|
+
if (status.trim()) throw new Error("Hotcell workers require a clean Git checkout; commit or stash local changes first.");
|
|
7149
|
+
const commit = (await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: root })).stdout.trim();
|
|
7150
|
+
const ref = (await execFileAsync("git", ["branch", "--show-current"], { cwd: root })).stdout.trim();
|
|
7151
|
+
if (!ref) throw new Error("Hotcell workers require a named branch so the daemon can clone before pinning the exact commit.");
|
|
7152
|
+
const url = (await execFileAsync("git", ["config", "--get", "remote.origin.url"], { cwd: root })).stdout.trim();
|
|
7153
|
+
if (!/^(https:\/\/|git@|ssh:\/\/)/i.test(url) || /:\/\/[^/]*@/.test(url)) {
|
|
7154
|
+
throw new Error("Hotcell workers require a credential-free HTTPS or SSH origin URL that the Hotcell daemon can clone.");
|
|
7155
|
+
}
|
|
7156
|
+
return { url, commit, ref };
|
|
7157
|
+
}
|
|
7158
|
+
function parseCellId(output) {
|
|
7159
|
+
const uuid = output.match(/\b[a-f0-9]{8}(?:-[a-f0-9]{4,}){0,4}\b/gi)?.at(-1);
|
|
7160
|
+
if (uuid) return uuid;
|
|
7161
|
+
const labeled = output.match(/(?:sandbox|cell)(?:\s+id)?\s*[:= ]\s*([a-z0-9][a-z0-9_-]{7,79})/i)?.[1];
|
|
7162
|
+
if (labeled) return labeled;
|
|
7163
|
+
return output.split(/\r?\n/).map((line) => line.trim()).reverse().find((line) => /^[a-z0-9][a-z0-9_-]{7,79}$/i.test(line));
|
|
7164
|
+
}
|
|
7165
|
+
function parseNamedCellId(output, name) {
|
|
7166
|
+
for (const line of output.split(/\r?\n/)) {
|
|
7167
|
+
const columns = line.trim().split(/\s+/);
|
|
7168
|
+
if (columns[1] === name && /^[a-z0-9][a-z0-9_-]{7,79}$/i.test(columns[0] ?? "")) return columns[0];
|
|
7169
|
+
}
|
|
7170
|
+
return void 0;
|
|
7171
|
+
}
|
|
7172
|
+
function formatSetupFailure(result) {
|
|
7173
|
+
const detail = sanitizeHotcellDiagnostic(`${result.stderr}
|
|
7174
|
+
${result.stdout}`);
|
|
7175
|
+
return `Hotcell setup failed (exit ${result.code})${detail ? `: ${detail}` : ". Check that the daemon, repository access, and gateway route are configured."}`;
|
|
7176
|
+
}
|
|
7177
|
+
function sanitizeHotcellDiagnostic(text) {
|
|
7178
|
+
return text.replace(/\b(?:sk-or-v1-|sk-ant-|gh[pousr]_|github_pat_)[A-Za-z0-9_-]{8,}\b/gi, "[REDACTED]").replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(/\b[A-Z0-9_]*(?:API[_-]?KEY|ACCESS[_-]?TOKEN|SECRET|PASSWORD)\s*[:=]\s*[^\s,;]+/gi, "[REDACTED]").trim().slice(-1500);
|
|
7179
|
+
}
|
|
7180
|
+
function shellQuote(value) {
|
|
7181
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7182
|
+
}
|
|
7183
|
+
function parseHotcellStats(output) {
|
|
7184
|
+
const llm = output.match(/LLM:\s*([\d,]+) calls,\s*([\d,]+) in \+ ([\d,]+) out tokens,\s*\$([\d.]+)/i);
|
|
7185
|
+
const totalCost = output.match(/Cost:\s*\$?([\d.]+)/i);
|
|
7186
|
+
const tokensUsed = llm ? Number(llm[2].replaceAll(",", "")) + Number(llm[3].replaceAll(",", "")) : void 0;
|
|
7187
|
+
const costUsd = totalCost ? Number(totalCost[1]) : llm ? Number(llm[4]) : void 0;
|
|
7188
|
+
return {
|
|
7189
|
+
...Number.isFinite(tokensUsed) ? { tokensUsed } : {},
|
|
7190
|
+
...Number.isFinite(costUsd) ? { costUsd } : {}
|
|
7191
|
+
};
|
|
7192
|
+
}
|
|
7193
|
+
function parsePrintOutput(stdout) {
|
|
7194
|
+
const candidate = stdout.trim();
|
|
7195
|
+
try {
|
|
7196
|
+
const parsed = JSON.parse(candidate);
|
|
7197
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
7198
|
+
return parsed;
|
|
7199
|
+
} catch {
|
|
7200
|
+
return void 0;
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
async function runProcess(executable, args, options) {
|
|
7204
|
+
if (options.signal?.aborted) return { code: 130, stdout: "", stderr: "", aborted: true };
|
|
7205
|
+
return await new Promise((resolvePromise, reject) => {
|
|
7206
|
+
const child = spawn2(executable, args, { cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
7207
|
+
const stdout = [];
|
|
7208
|
+
const stderr = [];
|
|
7209
|
+
let bytes = 0;
|
|
7210
|
+
let aborted2 = false;
|
|
7211
|
+
let timedOut = false;
|
|
7212
|
+
let outputLimitExceeded = false;
|
|
7213
|
+
let settled = false;
|
|
7214
|
+
const terminate = () => {
|
|
7215
|
+
child.kill("SIGTERM");
|
|
7216
|
+
const forceTimer = setTimeout(() => child.kill("SIGKILL"), 2e3);
|
|
7217
|
+
forceTimer.unref();
|
|
7218
|
+
};
|
|
7219
|
+
const abort = () => {
|
|
7220
|
+
aborted2 = true;
|
|
7221
|
+
terminate();
|
|
7222
|
+
};
|
|
7223
|
+
const timer2 = setTimeout(() => {
|
|
7224
|
+
timedOut = true;
|
|
7225
|
+
terminate();
|
|
7226
|
+
}, options.timeoutMs);
|
|
7227
|
+
timer2.unref();
|
|
7228
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
7229
|
+
const collect = (target, chunk) => {
|
|
7230
|
+
bytes += chunk.length;
|
|
7231
|
+
if (bytes > MAX_OUTPUT_BYTES) {
|
|
7232
|
+
outputLimitExceeded = true;
|
|
7233
|
+
terminate();
|
|
7234
|
+
return;
|
|
7235
|
+
}
|
|
7236
|
+
target.push(chunk);
|
|
7237
|
+
};
|
|
7238
|
+
child.stdout.on("data", (chunk) => collect(stdout, chunk));
|
|
7239
|
+
child.stderr.on("data", (chunk) => collect(stderr, chunk));
|
|
7240
|
+
child.once("error", (error) => {
|
|
7241
|
+
if (settled) return;
|
|
7242
|
+
settled = true;
|
|
7243
|
+
clearTimeout(timer2);
|
|
7244
|
+
options.signal?.removeEventListener("abort", abort);
|
|
7245
|
+
reject(error);
|
|
7246
|
+
});
|
|
7247
|
+
child.once("close", (code) => {
|
|
7248
|
+
if (settled) return;
|
|
7249
|
+
settled = true;
|
|
7250
|
+
clearTimeout(timer2);
|
|
7251
|
+
options.signal?.removeEventListener("abort", abort);
|
|
7252
|
+
resolvePromise({
|
|
7253
|
+
code: code ?? (aborted2 ? 130 : 1),
|
|
7254
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
7255
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
7256
|
+
aborted: aborted2,
|
|
7257
|
+
timedOut,
|
|
7258
|
+
outputLimitExceeded
|
|
7259
|
+
});
|
|
7260
|
+
});
|
|
7261
|
+
});
|
|
7262
|
+
}
|
|
7263
|
+
function terminalResult(workerId, task, status, error, model) {
|
|
7264
|
+
return {
|
|
7265
|
+
workerId,
|
|
7266
|
+
status,
|
|
7267
|
+
task,
|
|
7268
|
+
findings: [],
|
|
7269
|
+
recommendations: [],
|
|
7270
|
+
filesRead: [],
|
|
7271
|
+
webSources: [],
|
|
7272
|
+
costUsd: 0,
|
|
7273
|
+
tokensUsed: 0,
|
|
7274
|
+
reasoning: "",
|
|
7275
|
+
...model ? { model } : {},
|
|
7276
|
+
error
|
|
7277
|
+
};
|
|
7278
|
+
}
|
|
7279
|
+
async function acquireSlot(limit, signal) {
|
|
7280
|
+
const cap = Math.max(1, Math.min(MAX_PARALLEL_WORKERS, Math.floor(limit) || 1));
|
|
7281
|
+
if (runningWorkers < cap) {
|
|
7282
|
+
runningWorkers++;
|
|
7283
|
+
return;
|
|
7284
|
+
}
|
|
7285
|
+
await new Promise((resolvePromise, reject) => {
|
|
7286
|
+
const wake = () => {
|
|
7287
|
+
if (signal?.aborted) {
|
|
7288
|
+
const index = workerQueue.indexOf(wake);
|
|
7289
|
+
if (index >= 0) workerQueue.splice(index, 1);
|
|
7290
|
+
reject(new Error("Hotcell worker cancelled while waiting for a slot."));
|
|
7291
|
+
return;
|
|
7292
|
+
}
|
|
7293
|
+
if (runningWorkers < cap) {
|
|
7294
|
+
const index = workerQueue.indexOf(wake);
|
|
7295
|
+
if (index >= 0) workerQueue.splice(index, 1);
|
|
7296
|
+
runningWorkers++;
|
|
7297
|
+
resolvePromise();
|
|
7298
|
+
}
|
|
7299
|
+
};
|
|
7300
|
+
workerQueue.push(wake);
|
|
7301
|
+
signal?.addEventListener("abort", wake, { once: true });
|
|
7302
|
+
});
|
|
7303
|
+
}
|
|
7304
|
+
function releaseSlot() {
|
|
7305
|
+
runningWorkers = Math.max(0, runningWorkers - 1);
|
|
7306
|
+
for (const wake of workerQueue.slice()) wake();
|
|
7307
|
+
}
|
|
7308
|
+
|
|
6854
7309
|
// src/tools/spawn-worker.ts
|
|
6855
7310
|
var DEFAULT_WORKER_TIMEOUT_MS = 3e5;
|
|
6856
7311
|
async function callWorkerEndpoint(endpoint, apiKey, payload, signal, timeoutMs = DEFAULT_WORKER_TIMEOUT_MS) {
|
|
@@ -6886,10 +7341,9 @@ async function callWorkerEndpoint(endpoint, apiKey, payload, signal, timeoutMs =
|
|
|
6886
7341
|
var spawnWorkerTool = {
|
|
6887
7342
|
name: "spawn_worker",
|
|
6888
7343
|
description: [
|
|
6889
|
-
"Spawn a standalone
|
|
6890
|
-
"
|
|
6891
|
-
"Mode '
|
|
6892
|
-
"Mode 'execute': write-enabled worker that creates a branch, implements changes, and opens a PR.",
|
|
7344
|
+
"Spawn a standalone worker using the configured remote endpoint or an opt-in local Hotcell sandbox.",
|
|
7345
|
+
"Mode 'plan': read-only research. Local Hotcell workers use a clean committed revision and cannot edit files.",
|
|
7346
|
+
"Mode 'execute': remote write + PR only; local Hotcell execute mode is intentionally unavailable.",
|
|
6893
7347
|
"Use for heavy tasks that benefit from parallel research (e.g. 'research OAuth2, testing, and migration')."
|
|
6894
7348
|
].join(" "),
|
|
6895
7349
|
parameters: {
|
|
@@ -6922,11 +7376,11 @@ var spawnWorkerTool = {
|
|
|
6922
7376
|
tools: {
|
|
6923
7377
|
type: "string",
|
|
6924
7378
|
enum: ["all", "read-only"],
|
|
6925
|
-
description: "
|
|
7379
|
+
description: "Requested remote tool set. Local Hotcell research always uses a narrow read-only profile and rejects `all`."
|
|
6926
7380
|
},
|
|
6927
7381
|
model: {
|
|
6928
7382
|
type: "string",
|
|
6929
|
-
description:
|
|
7383
|
+
description: "Model to use for the worker. Defaults to the active session model."
|
|
6930
7384
|
},
|
|
6931
7385
|
branchName: {
|
|
6932
7386
|
type: "string",
|
|
@@ -6954,25 +7408,71 @@ var spawnWorkerTool = {
|
|
|
6954
7408
|
body: args.task.slice(0, 200)
|
|
6955
7409
|
}),
|
|
6956
7410
|
async run(args, ctx) {
|
|
6957
|
-
const
|
|
7411
|
+
const cfg = await loadConfig().catch(() => null);
|
|
7412
|
+
const timeoutMs = cfg?.workerTimeoutMs ?? readNumberEnv2("KIMIFLARE_WORKER_TIMEOUT_MS") ?? DEFAULT_WORKER_TIMEOUT_MS;
|
|
7413
|
+
const budgetCeiling = resolveWorkerBudgetUsd(cfg);
|
|
7414
|
+
if (cfg?.workerBackend === "hotcell") {
|
|
7415
|
+
if (cfg.baseUrl) {
|
|
7416
|
+
return textOutput("Hotcell workers currently require an OpenRouter-backed session; custom model endpoints cannot be routed through the Hotcell gateway.");
|
|
7417
|
+
}
|
|
7418
|
+
if (args.mode !== "plan") {
|
|
7419
|
+
return textOutput("Hotcell workers currently support read-only plan mode only. Execute mode is disabled until reviewed patch artifacts are supported.");
|
|
7420
|
+
}
|
|
7421
|
+
if (args.tools === "all") {
|
|
7422
|
+
return textOutput("Hotcell research workers are restricted to read-only tools; the requested broader toolset is not available.");
|
|
7423
|
+
}
|
|
7424
|
+
const model = args.model ?? ctx.model;
|
|
7425
|
+
if (!model) {
|
|
7426
|
+
return textOutput("Hotcell worker requires the coordinator's active model ID. No model was provided; refusing to fall back to a default.");
|
|
7427
|
+
}
|
|
7428
|
+
const requestedBudget = args.budget?.maxCostUsd ?? budgetCeiling;
|
|
7429
|
+
const budgetUsd = Math.min(requestedBudget, budgetCeiling);
|
|
7430
|
+
if (!Number.isFinite(budgetUsd) || budgetUsd <= 0) {
|
|
7431
|
+
return textOutput("Hotcell worker spend cap must be a positive number.");
|
|
7432
|
+
}
|
|
7433
|
+
try {
|
|
7434
|
+
const result = await runHotcellWorker({
|
|
7435
|
+
task: args.task,
|
|
7436
|
+
context: args.context,
|
|
7437
|
+
model,
|
|
7438
|
+
budgetUsd,
|
|
7439
|
+
timeoutMs,
|
|
7440
|
+
maxParallel: cfg?.workerMaxParallel,
|
|
7441
|
+
cwd: ctx.cwd,
|
|
7442
|
+
signal: ctx.signal
|
|
7443
|
+
});
|
|
7444
|
+
if (result.status !== "completed" && result.status !== "budget_exhausted") {
|
|
7445
|
+
return textOutput(`Hotcell worker ${result.status}: ${result.error ?? "unknown error"}`);
|
|
7446
|
+
}
|
|
7447
|
+
const lines = [
|
|
7448
|
+
`Hotcell worker ${result.status}${result.status === "budget_exhausted" ? " (partial result)" : ""}.`,
|
|
7449
|
+
`Model: ${result.model ?? model} \xB7 Tokens: ${result.tokensUsed.toLocaleString()} \xB7 Cost: ${result.costUsd > 0 ? `${result.costUsd.toFixed(4)} total Hotcell cost` : "unavailable from Hotcell stats"}.`,
|
|
7450
|
+
...result.findings.map((finding) => `
|
|
7451
|
+
## ${finding.topic}
|
|
7452
|
+
${finding.summary}`)
|
|
7453
|
+
];
|
|
7454
|
+
if (result.status === "budget_exhausted") {
|
|
7455
|
+
lines.push("\nWorker input-token budget was exhausted; review partial findings before relying on them.");
|
|
7456
|
+
}
|
|
7457
|
+
return textOutput(lines.join("\n"));
|
|
7458
|
+
} catch (error) {
|
|
7459
|
+
return textOutput(`Failed to spawn Hotcell worker: ${error instanceof Error ? error.message : String(error)}`);
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
const endpoint = process.env.KIMIFLARE_WORKER_ENDPOINT ?? cfg?.workerEndpoint ?? cfg?.remoteWorkerUrl;
|
|
6958
7463
|
if (!endpoint) {
|
|
6959
|
-
|
|
6960
|
-
const bytes = Buffer.byteLength(msg, "utf8");
|
|
6961
|
-
return { content: msg, rawBytes: bytes, reducedBytes: bytes };
|
|
7464
|
+
return textOutput("Worker endpoint not configured. Set KIMIFLARE_WORKER_ENDPOINT or workerEndpoint in config, or set workerBackend to hotcell.");
|
|
6962
7465
|
}
|
|
6963
7466
|
const apiKey = process.env.KIMIFLARE_WORKER_API_KEY;
|
|
6964
|
-
const timeoutMs = readNumberEnv2("KIMIFLARE_WORKER_TIMEOUT_MS") ?? DEFAULT_WORKER_TIMEOUT_MS;
|
|
6965
|
-
const cfg = await loadConfig().catch(() => null);
|
|
6966
|
-
const budgetUsd = resolveWorkerBudgetUsd(cfg);
|
|
6967
7467
|
const defaultModel = cfg?.model ?? DEFAULT_MODEL;
|
|
6968
7468
|
const payload = {
|
|
6969
7469
|
mode: args.mode,
|
|
6970
7470
|
task: args.task,
|
|
6971
7471
|
context: args.context ?? "",
|
|
6972
|
-
budget: { maxCostUsd:
|
|
7472
|
+
budget: { maxCostUsd: budgetCeiling },
|
|
6973
7473
|
outputFormat: args.outputFormat ?? "structured",
|
|
6974
7474
|
tools: args.tools ?? (args.mode === "plan" ? "read-only" : "all"),
|
|
6975
|
-
model: args.model ?? defaultModel,
|
|
7475
|
+
model: args.model ?? ctx.model ?? defaultModel,
|
|
6976
7476
|
...args.mode === "execute" ? {
|
|
6977
7477
|
branchName: args.branchName,
|
|
6978
7478
|
baseBranch: args.baseBranch ?? "main",
|
|
@@ -7026,6 +7526,10 @@ var spawnWorkerTool = {
|
|
|
7026
7526
|
}
|
|
7027
7527
|
}
|
|
7028
7528
|
};
|
|
7529
|
+
function textOutput(content) {
|
|
7530
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
7531
|
+
return { content, rawBytes: bytes, reducedBytes: bytes };
|
|
7532
|
+
}
|
|
7029
7533
|
function readNumberEnv2(name) {
|
|
7030
7534
|
const raw = process.env[name];
|
|
7031
7535
|
if (!raw) return void 0;
|
|
@@ -7639,6 +8143,8 @@ function makeExpandArtifactTool(store) {
|
|
|
7639
8143
|
}
|
|
7640
8144
|
|
|
7641
8145
|
// src/tools/executor.ts
|
|
8146
|
+
import { realpath as realpath2 } from "fs/promises";
|
|
8147
|
+
import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
|
|
7642
8148
|
var ALL_TOOLS = [
|
|
7643
8149
|
{ ...readTool, isReadOnly: true },
|
|
7644
8150
|
writeTool,
|
|
@@ -8522,7 +9028,7 @@ Context: This memory was explicitly provided by the user during a conversation.`
|
|
|
8522
9028
|
};
|
|
8523
9029
|
|
|
8524
9030
|
// src/lsp/connection.ts
|
|
8525
|
-
import { spawn as
|
|
9031
|
+
import { spawn as spawn3 } from "child_process";
|
|
8526
9032
|
import { EventEmitter } from "events";
|
|
8527
9033
|
var LspConnection = class extends EventEmitter {
|
|
8528
9034
|
child = null;
|
|
@@ -8539,7 +9045,7 @@ var LspConnection = class extends EventEmitter {
|
|
|
8539
9045
|
if (this.child) {
|
|
8540
9046
|
throw new Error("LSP connection already started");
|
|
8541
9047
|
}
|
|
8542
|
-
return new Promise((
|
|
9048
|
+
return new Promise((resolve6, reject) => {
|
|
8543
9049
|
const abortController = new AbortController();
|
|
8544
9050
|
const spawnTimer = setTimeout(() => {
|
|
8545
9051
|
abortController.abort();
|
|
@@ -8547,7 +9053,7 @@ var LspConnection = class extends EventEmitter {
|
|
|
8547
9053
|
reject(new Error(`LSP server spawn timed out after ${spawnTimeoutMs}ms`));
|
|
8548
9054
|
}, spawnTimeoutMs);
|
|
8549
9055
|
try {
|
|
8550
|
-
const child =
|
|
9056
|
+
const child = spawn3(command[0], command.slice(1), {
|
|
8551
9057
|
env: { ...process.env, ...env },
|
|
8552
9058
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8553
9059
|
});
|
|
@@ -8578,7 +9084,7 @@ var LspConnection = class extends EventEmitter {
|
|
|
8578
9084
|
if (child.pid) {
|
|
8579
9085
|
clearTimeout(spawnTimer);
|
|
8580
9086
|
this.child = child;
|
|
8581
|
-
|
|
9087
|
+
resolve6();
|
|
8582
9088
|
}
|
|
8583
9089
|
});
|
|
8584
9090
|
} catch (err) {
|
|
@@ -8593,12 +9099,12 @@ var LspConnection = class extends EventEmitter {
|
|
|
8593
9099
|
}
|
|
8594
9100
|
const id = this.nextId++;
|
|
8595
9101
|
const msg = { jsonrpc: "2.0", id, method, params };
|
|
8596
|
-
return new Promise((
|
|
9102
|
+
return new Promise((resolve6, reject) => {
|
|
8597
9103
|
const timer2 = setTimeout(() => {
|
|
8598
9104
|
this.pending.delete(id);
|
|
8599
9105
|
reject(toolTimeoutError(`LSP request '${method}'`, this.requestTimeoutMs));
|
|
8600
9106
|
}, this.requestTimeoutMs);
|
|
8601
|
-
const pending = { resolve:
|
|
9107
|
+
const pending = { resolve: resolve6, reject, signal, timer: timer2 };
|
|
8602
9108
|
this.pending.set(id, pending);
|
|
8603
9109
|
if (signal) {
|
|
8604
9110
|
const onAbort = () => {
|
|
@@ -9139,10 +9645,10 @@ var LspManager = class {
|
|
|
9139
9645
|
};
|
|
9140
9646
|
|
|
9141
9647
|
// src/lsp/adapter.ts
|
|
9142
|
-
import { relative as
|
|
9648
|
+
import { relative as relative4 } from "path";
|
|
9143
9649
|
function formatLocation(loc, cwd) {
|
|
9144
9650
|
const path = fromUri(loc.uri);
|
|
9145
|
-
const rel =
|
|
9651
|
+
const rel = relative4(cwd, path) || path;
|
|
9146
9652
|
return `${rel}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
|
|
9147
9653
|
}
|
|
9148
9654
|
function formatLocations(locs, cwd) {
|
|
@@ -9179,7 +9685,7 @@ function formatWorkspaceSymbols(symbols, cwd) {
|
|
|
9179
9685
|
if (!symbols || symbols.length === 0) return "No symbols found.";
|
|
9180
9686
|
return symbols.map((s) => {
|
|
9181
9687
|
const path = fromUri(s.location.uri);
|
|
9182
|
-
const rel =
|
|
9688
|
+
const rel = relative4(cwd, path) || path;
|
|
9183
9689
|
const container = s.containerName ? ` in ${s.containerName}` : "";
|
|
9184
9690
|
return `${s.name} (${symbolKindName(s.kind)})${container} \u2014 ${rel}:${s.location.range.start.line + 1}:${s.location.range.start.character + 1}`;
|
|
9185
9691
|
}).join("\n");
|
|
@@ -9199,7 +9705,7 @@ function formatWorkspaceEdit(edit, cwd) {
|
|
|
9199
9705
|
if (edit.changes) {
|
|
9200
9706
|
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
9201
9707
|
const path = fromUri(uri);
|
|
9202
|
-
const rel =
|
|
9708
|
+
const rel = relative4(cwd, path) || path;
|
|
9203
9709
|
lines.push(`File: ${rel}`);
|
|
9204
9710
|
for (const e of edits) {
|
|
9205
9711
|
lines.push(` ${e.range.start.line + 1}:${e.range.start.character + 1}-${e.range.end.line + 1}:${e.range.end.character + 1}: ${e.newText}`);
|
|
@@ -9218,7 +9724,7 @@ function formatCodeActions(actions) {
|
|
|
9218
9724
|
}
|
|
9219
9725
|
|
|
9220
9726
|
// src/tools/lsp.ts
|
|
9221
|
-
import { relative as
|
|
9727
|
+
import { relative as relative5 } from "path";
|
|
9222
9728
|
function makeOutput2(content) {
|
|
9223
9729
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
9224
9730
|
return { content, rawBytes: bytes, reducedBytes: bytes };
|
|
@@ -9226,7 +9732,7 @@ function makeOutput2(content) {
|
|
|
9226
9732
|
function resolveLspPath(args, ctx) {
|
|
9227
9733
|
const raw = typeof args.path === "string" ? args.path : "";
|
|
9228
9734
|
const resolved = resolvePath(ctx.cwd, raw);
|
|
9229
|
-
const rel =
|
|
9735
|
+
const rel = relative5(ctx.cwd, resolved);
|
|
9230
9736
|
if (isPathOutside(rel)) {
|
|
9231
9737
|
throw new Error(`Path outside workspace: ${raw}`);
|
|
9232
9738
|
}
|
|
@@ -9505,11 +10011,11 @@ async function loadSession(filePath) {
|
|
|
9505
10011
|
}
|
|
9506
10012
|
|
|
9507
10013
|
// src/usage-tracker.ts
|
|
9508
|
-
import { readFile as readFile8,
|
|
10014
|
+
import { readFile as readFile8, rename as rename2 } from "fs/promises";
|
|
9509
10015
|
import { homedir as homedir8 } from "os";
|
|
9510
10016
|
import { join as join15 } from "path";
|
|
9511
10017
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
9512
|
-
import { randomUUID } from "crypto";
|
|
10018
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
9513
10019
|
|
|
9514
10020
|
// src/pricing.ts
|
|
9515
10021
|
var PRICE_IN_PER_M = 0.95;
|
|
@@ -9530,6 +10036,59 @@ function calculateCost(promptTokens, completionTokens, cachedTokens = 0, model)
|
|
|
9530
10036
|
return { uncachedIn, cachedIn, out, total };
|
|
9531
10037
|
}
|
|
9532
10038
|
|
|
10039
|
+
// src/util/atomic-file.ts
|
|
10040
|
+
import { mkdir as mkdir6, rename, rm, stat as stat4, writeFile as writeFile7 } from "fs/promises";
|
|
10041
|
+
import { dirname as dirname6 } from "path";
|
|
10042
|
+
import { randomUUID } from "crypto";
|
|
10043
|
+
async function writeFileAtomic(path, data) {
|
|
10044
|
+
await mkdir6(dirname6(path), { recursive: true });
|
|
10045
|
+
const tmp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
10046
|
+
try {
|
|
10047
|
+
await writeFile7(tmp, data, "utf8");
|
|
10048
|
+
await rename(tmp, path);
|
|
10049
|
+
} catch (err) {
|
|
10050
|
+
await rm(tmp, { force: true }).catch(() => void 0);
|
|
10051
|
+
throw err;
|
|
10052
|
+
}
|
|
10053
|
+
}
|
|
10054
|
+
var LOCK_STALE_MS = 15e3;
|
|
10055
|
+
var LOCK_WAIT_MS = 5e3;
|
|
10056
|
+
var LOCK_RETRY_MS = 25;
|
|
10057
|
+
async function withFileLock(lockPath, fn) {
|
|
10058
|
+
await mkdir6(dirname6(lockPath), { recursive: true });
|
|
10059
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
10060
|
+
let held = false;
|
|
10061
|
+
while (!held) {
|
|
10062
|
+
try {
|
|
10063
|
+
await mkdir6(lockPath);
|
|
10064
|
+
held = true;
|
|
10065
|
+
} catch (err) {
|
|
10066
|
+
if (err.code !== "EEXIST") break;
|
|
10067
|
+
const age = await stat4(lockPath).then((s) => Date.now() - s.mtimeMs, () => 0);
|
|
10068
|
+
if (age > LOCK_STALE_MS) {
|
|
10069
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
10070
|
+
continue;
|
|
10071
|
+
}
|
|
10072
|
+
if (Date.now() > deadline) break;
|
|
10073
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
10074
|
+
}
|
|
10075
|
+
}
|
|
10076
|
+
try {
|
|
10077
|
+
return await fn();
|
|
10078
|
+
} finally {
|
|
10079
|
+
if (held) await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
function recoverJsonPrefix(raw) {
|
|
10083
|
+
for (let i = raw.lastIndexOf("}"); i > 0; i = raw.lastIndexOf("}", i - 1)) {
|
|
10084
|
+
try {
|
|
10085
|
+
return JSON.parse(raw.slice(0, i + 1));
|
|
10086
|
+
} catch {
|
|
10087
|
+
}
|
|
10088
|
+
}
|
|
10089
|
+
return void 0;
|
|
10090
|
+
}
|
|
10091
|
+
|
|
9533
10092
|
// src/usage-tracker.ts
|
|
9534
10093
|
var LOG_VERSION2 = 1;
|
|
9535
10094
|
var usageEvents = new EventEmitter2();
|
|
@@ -9553,21 +10112,29 @@ function cutoffDate(daysBack) {
|
|
|
9553
10112
|
return d.toISOString().slice(0, 10);
|
|
9554
10113
|
}
|
|
9555
10114
|
async function loadLog() {
|
|
10115
|
+
let raw;
|
|
9556
10116
|
try {
|
|
9557
|
-
|
|
9558
|
-
const parsed = JSON.parse(raw);
|
|
9559
|
-
if (parsed.version === LOG_VERSION2) return parsed;
|
|
10117
|
+
raw = await readFile8(usagePath(), "utf8");
|
|
9560
10118
|
} catch {
|
|
10119
|
+
return { version: LOG_VERSION2, days: [], sessions: [] };
|
|
9561
10120
|
}
|
|
10121
|
+
let parsed;
|
|
10122
|
+
try {
|
|
10123
|
+
parsed = JSON.parse(raw);
|
|
10124
|
+
} catch {
|
|
10125
|
+
await rename2(usagePath(), `${usagePath()}.corrupt-${Date.now()}`).catch(() => void 0);
|
|
10126
|
+
parsed = recoverJsonPrefix(raw);
|
|
10127
|
+
}
|
|
10128
|
+
if (parsed && parsed.version === LOG_VERSION2) return parsed;
|
|
9562
10129
|
return { version: LOG_VERSION2, days: [], sessions: [] };
|
|
9563
10130
|
}
|
|
9564
10131
|
async function saveLog(log2) {
|
|
9565
|
-
await
|
|
9566
|
-
await writeFile7(usagePath(), JSON.stringify(log2, null, 2), "utf8");
|
|
10132
|
+
await writeFileAtomic(usagePath(), JSON.stringify(log2, null, 2));
|
|
9567
10133
|
}
|
|
9568
10134
|
var writeChain = Promise.resolve();
|
|
9569
10135
|
function withLock(fn) {
|
|
9570
|
-
const
|
|
10136
|
+
const locked = () => withFileLock(`${usagePath()}.lock`, fn);
|
|
10137
|
+
const next = writeChain.then(locked, locked);
|
|
9571
10138
|
writeChain = next.catch(() => void 0);
|
|
9572
10139
|
return next;
|
|
9573
10140
|
}
|
|
@@ -9597,8 +10164,7 @@ async function upsertHistoryDay(day) {
|
|
|
9597
10164
|
entries.push(day);
|
|
9598
10165
|
}
|
|
9599
10166
|
const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
9600
|
-
await
|
|
9601
|
-
await writeFile7(historyPath(), lines, "utf8");
|
|
10167
|
+
await writeFileAtomic(historyPath(), lines);
|
|
9602
10168
|
}
|
|
9603
10169
|
function getOrCreateDay(log2, date) {
|
|
9604
10170
|
let day = log2.days.find((d) => d.date === date);
|
|
@@ -9657,7 +10223,7 @@ async function recordUsage(sessionId, usage, lookup, model) {
|
|
|
9657
10223
|
);
|
|
9658
10224
|
const estimatedCost = cost.total;
|
|
9659
10225
|
const cachedTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
9660
|
-
const turnId =
|
|
10226
|
+
const turnId = randomUUID2();
|
|
9661
10227
|
const generationId = lookup?.meta.generationId;
|
|
9662
10228
|
const inlineCost = typeof usage.cost === "number" && Number.isFinite(usage.cost) ? usage.cost : void 0;
|
|
9663
10229
|
await withLock(async () => {
|
|
@@ -9760,7 +10326,7 @@ async function reconcileTurnCost(sessionId, turnId, lookup, fetchImpl2 = fetch,
|
|
|
9760
10326
|
// src/models/openrouter-catalog.ts
|
|
9761
10327
|
import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
|
|
9762
10328
|
import { homedir as homedir9 } from "os";
|
|
9763
|
-
import { join as join16, dirname as
|
|
10329
|
+
import { join as join16, dirname as dirname7 } from "path";
|
|
9764
10330
|
var CACHE_VERSION = 4;
|
|
9765
10331
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
9766
10332
|
function catalogCachePath() {
|
|
@@ -9826,7 +10392,7 @@ async function readCache() {
|
|
|
9826
10392
|
async function writeCache(models) {
|
|
9827
10393
|
const path = catalogCachePath();
|
|
9828
10394
|
try {
|
|
9829
|
-
await mkdir7(
|
|
10395
|
+
await mkdir7(dirname7(path), { recursive: true });
|
|
9830
10396
|
const cache = { version: CACHE_VERSION, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), models };
|
|
9831
10397
|
await writeFile8(path, JSON.stringify(cache, null, 2), "utf8");
|
|
9832
10398
|
} catch {
|
|
@@ -9899,7 +10465,7 @@ function createDefaultPermissionHandler(options) {
|
|
|
9899
10465
|
async function createAgentSession(opts) {
|
|
9900
10466
|
const config2 = await resolveSdkConfig(opts);
|
|
9901
10467
|
if (!resolveCustomEndpoint(config2)) await ensureOpenRouterCatalog();
|
|
9902
|
-
const cwd =
|
|
10468
|
+
const cwd = resolve5(opts.cwd ?? process.cwd());
|
|
9903
10469
|
const tools = opts.tools ?? ALL_TOOLS;
|
|
9904
10470
|
let hooks;
|
|
9905
10471
|
if (opts.enableHooks) {
|
|
@@ -10261,6 +10827,10 @@ var InternalSession = class {
|
|
|
10261
10827
|
onWarning: (msg) => {
|
|
10262
10828
|
this.emit({ type: "warning", message: msg });
|
|
10263
10829
|
},
|
|
10830
|
+
onGuardrail: (ev) => {
|
|
10831
|
+
this.emit({ type: "warning", message: ev.message });
|
|
10832
|
+
if (ev.kind === "limit_stopped") this.emit({ type: "status", status: "error" });
|
|
10833
|
+
},
|
|
10264
10834
|
askPermission: async (req) => {
|
|
10265
10835
|
if (mode === "auto") return "allow";
|
|
10266
10836
|
if (mode === "plan") {
|
|
@@ -10280,12 +10850,12 @@ var InternalSession = class {
|
|
|
10280
10850
|
toolName: req.tool.name,
|
|
10281
10851
|
args: req.args
|
|
10282
10852
|
});
|
|
10283
|
-
const decision = await new Promise((
|
|
10284
|
-
this.permissionResolvers.set(requestId,
|
|
10853
|
+
const decision = await new Promise((resolve6) => {
|
|
10854
|
+
this.permissionResolvers.set(requestId, resolve6);
|
|
10285
10855
|
setTimeout(() => {
|
|
10286
10856
|
if (this.permissionResolvers.has(requestId)) {
|
|
10287
10857
|
this.permissionResolvers.delete(requestId);
|
|
10288
|
-
|
|
10858
|
+
resolve6("deny");
|
|
10289
10859
|
}
|
|
10290
10860
|
}, 3e5);
|
|
10291
10861
|
});
|
|
@@ -10296,10 +10866,6 @@ var InternalSession = class {
|
|
|
10296
10866
|
});
|
|
10297
10867
|
return decision;
|
|
10298
10868
|
},
|
|
10299
|
-
onToolLimitReached: async () => {
|
|
10300
|
-
this.emit({ type: "status", status: "error" });
|
|
10301
|
-
return "stop";
|
|
10302
|
-
},
|
|
10303
10869
|
onKimiMdStale: () => {
|
|
10304
10870
|
this.onKimiMdStale?.();
|
|
10305
10871
|
}
|
|
@@ -10317,6 +10883,9 @@ var InternalSession = class {
|
|
|
10317
10883
|
hooks: this.hooks,
|
|
10318
10884
|
// M6.1: Stop fires at end-of-turn when enabled.
|
|
10319
10885
|
maxToolIterations,
|
|
10886
|
+
// SDK callers bound turns with `maxToolIterations`: stop cleanly at the
|
|
10887
|
+
// limit rather than resetting (never waits on a prompt either way).
|
|
10888
|
+
toolLimitBehavior: "stop",
|
|
10320
10889
|
reasoningEffort: this.reasoningEffort,
|
|
10321
10890
|
coauthor,
|
|
10322
10891
|
sessionId: this.sessionId,
|