autopilot-ai 1.2.2 → 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 +2419 -1356
- package/dist/index.js.map +1 -1
- package/dist/sdk/index.d.ts +6 -0
- package/dist/sdk/index.js +537 -62
- 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;
|
|
@@ -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);
|
|
@@ -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,7 +3498,12 @@ ${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);
|
|
@@ -3589,7 +3647,8 @@ async function runAgentTurn(opts) {
|
|
|
3589
3647
|
model: opts.model,
|
|
3590
3648
|
mode: opts.mode,
|
|
3591
3649
|
skillContext: skillResult.skillContext,
|
|
3592
|
-
preferPullRequests: opts.preferPullRequests
|
|
3650
|
+
preferPullRequests: opts.preferPullRequests,
|
|
3651
|
+
delegationDirective: opts.delegationDirective
|
|
3593
3652
|
})
|
|
3594
3653
|
};
|
|
3595
3654
|
} else {
|
|
@@ -3601,7 +3660,8 @@ async function runAgentTurn(opts) {
|
|
|
3601
3660
|
model: opts.model,
|
|
3602
3661
|
mode: opts.mode,
|
|
3603
3662
|
skillContext: skillResult.skillContext,
|
|
3604
|
-
preferPullRequests: opts.preferPullRequests
|
|
3663
|
+
preferPullRequests: opts.preferPullRequests,
|
|
3664
|
+
delegationDirective: opts.delegationDirective
|
|
3605
3665
|
})
|
|
3606
3666
|
};
|
|
3607
3667
|
}
|
|
@@ -3911,9 +3971,11 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
|
|
|
3911
3971
|
return;
|
|
3912
3972
|
}
|
|
3913
3973
|
let blockedCount = 0;
|
|
3974
|
+
const availableToolNames = new Set(opts.tools.map((tool) => tool.name));
|
|
3975
|
+
if (codeMode) availableToolNames.add("execute_code");
|
|
3914
3976
|
const allReadOnly = toolCalls.length > 1 && toolCalls.every((tc) => {
|
|
3915
3977
|
const tool = opts.executor.list().find((t) => t.name === tc.function.name);
|
|
3916
|
-
return tool?.isReadOnly === true;
|
|
3978
|
+
return availableToolNames.has(tc.function.name) && tool?.isReadOnly === true;
|
|
3917
3979
|
});
|
|
3918
3980
|
if (allReadOnly) {
|
|
3919
3981
|
opts.callbacks.onInfo?.(`${toolCalls.length} read-only tools running in parallel`);
|
|
@@ -4153,6 +4215,26 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
|
|
|
4153
4215
|
for (const [i, tc] of toolCalls.entries()) {
|
|
4154
4216
|
if (opts.signal.aborted) throw new DOMException("aborted", "AbortError");
|
|
4155
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
|
+
}
|
|
4156
4238
|
const loopCount = recentToolCalls.filter((s) => s === loopSignature).length;
|
|
4157
4239
|
if (loopCount >= LOOP_THRESHOLD) {
|
|
4158
4240
|
const warning = `Loop detected: you have called ${tc.function.name} with the same arguments multiple times in a row. Consider a different approach.`;
|
|
@@ -4905,15 +4987,15 @@ function makeErrorOutput(message) {
|
|
|
4905
4987
|
return { content: message, rawBytes: bytes, reducedBytes: bytes };
|
|
4906
4988
|
}
|
|
4907
4989
|
async function execOnce(command, cwd) {
|
|
4908
|
-
return new Promise((
|
|
4990
|
+
return new Promise((resolve6) => {
|
|
4909
4991
|
const child = spawn("bash", ["-lc", command], { cwd });
|
|
4910
4992
|
let out = "";
|
|
4911
4993
|
child.stdout.on("data", (d) => {
|
|
4912
4994
|
out += d.toString("utf8");
|
|
4913
4995
|
});
|
|
4914
4996
|
child.on("close", (code) => {
|
|
4915
|
-
if (code !== 0) return
|
|
4916
|
-
|
|
4997
|
+
if (code !== 0) return resolve6(void 0);
|
|
4998
|
+
resolve6(out.trim() || void 0);
|
|
4917
4999
|
});
|
|
4918
5000
|
});
|
|
4919
5001
|
}
|
|
@@ -5051,7 +5133,7 @@ async function runBash(args, ctx) {
|
|
|
5051
5133
|
if (pushGuard) return pushGuard;
|
|
5052
5134
|
const { shell, args: shellArgs, isPosix } = getShellCommand(ctx.shell);
|
|
5053
5135
|
const command = isPosix ? injectCoauthor(args.command, ctx.coauthor) : args.command;
|
|
5054
|
-
return new Promise((
|
|
5136
|
+
return new Promise((resolve6, reject) => {
|
|
5055
5137
|
logger.debug("bash:spawn", { command: args.command.slice(0, 200), cwd: ctx.cwd, shell });
|
|
5056
5138
|
const child = spawn(shell, [...shellArgs, command], {
|
|
5057
5139
|
cwd: ctx.cwd,
|
|
@@ -5060,6 +5142,7 @@ async function runBash(args, ctx) {
|
|
|
5060
5142
|
GIT_EDITOR: "true"
|
|
5061
5143
|
}
|
|
5062
5144
|
});
|
|
5145
|
+
child.stdin?.end();
|
|
5063
5146
|
let stdout = "";
|
|
5064
5147
|
let stderr = "";
|
|
5065
5148
|
let killedByTimeout = false;
|
|
@@ -5103,8 +5186,18 @@ ${stdout.trimEnd()}`);
|
|
|
5103
5186
|
if (stderr) parts.push(`--- stderr ---
|
|
5104
5187
|
${stderr.trimEnd()}`);
|
|
5105
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
|
+
}
|
|
5106
5199
|
const raw = parts.join("\n");
|
|
5107
|
-
|
|
5200
|
+
resolve6({
|
|
5108
5201
|
content: raw,
|
|
5109
5202
|
rawBytes: Buffer.byteLength(raw, "utf8"),
|
|
5110
5203
|
reducedBytes: Buffer.byteLength(raw, "utf8")
|
|
@@ -6449,7 +6542,7 @@ var browserFetchTool = {
|
|
|
6449
6542
|
};
|
|
6450
6543
|
async function autoScroll(page) {
|
|
6451
6544
|
await page.evaluate(async () => {
|
|
6452
|
-
await new Promise((
|
|
6545
|
+
await new Promise((resolve6) => {
|
|
6453
6546
|
let totalHeight = 0;
|
|
6454
6547
|
const distance = 300;
|
|
6455
6548
|
const timer2 = setInterval(() => {
|
|
@@ -6458,12 +6551,12 @@ async function autoScroll(page) {
|
|
|
6458
6551
|
totalHeight += distance;
|
|
6459
6552
|
if (totalHeight >= scrollHeight) {
|
|
6460
6553
|
clearInterval(timer2);
|
|
6461
|
-
|
|
6554
|
+
resolve6();
|
|
6462
6555
|
}
|
|
6463
6556
|
}, 100);
|
|
6464
6557
|
setTimeout(() => {
|
|
6465
6558
|
clearInterval(timer2);
|
|
6466
|
-
|
|
6559
|
+
resolve6();
|
|
6467
6560
|
}, 1e4);
|
|
6468
6561
|
});
|
|
6469
6562
|
});
|
|
@@ -6688,9 +6781,13 @@ function readReasoningEffortEnv() {
|
|
|
6688
6781
|
const raw = process.env.KIMI_REASONING_EFFORT?.toLowerCase();
|
|
6689
6782
|
return EFFORTS.includes(raw ?? "") ? raw : void 0;
|
|
6690
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
|
+
}
|
|
6691
6788
|
function readCoauthorEnv() {
|
|
6692
|
-
const
|
|
6693
|
-
if (
|
|
6789
|
+
const enabled2 = process.env.KIMIFLARE_COAUTHOR;
|
|
6790
|
+
if (enabled2 === "0" || enabled2 === "false") return void 0;
|
|
6694
6791
|
const name = process.env.KIMIFLARE_COAUTHOR_NAME || "kimiflare";
|
|
6695
6792
|
const email = process.env.KIMIFLARE_COAUTHOR_EMAIL || "kimiflare@proton.me";
|
|
6696
6793
|
return { enabled: true, name, email };
|
|
@@ -6724,6 +6821,7 @@ async function loadConfig() {
|
|
|
6724
6821
|
if (!openrouterApiKey && !baseUrl) return null;
|
|
6725
6822
|
const envModel = process.env.KIMI_MODEL || void 0;
|
|
6726
6823
|
const envEffort = readReasoningEffortEnv();
|
|
6824
|
+
const envSubagentPolicy = readSubagentPolicyEnv();
|
|
6727
6825
|
const envCoauthor = readCoauthorEnv();
|
|
6728
6826
|
const envCacheStable = process.env.KIMIFLARE_CACHE_STABLE_PROMPTS;
|
|
6729
6827
|
const cacheStablePrompts = envCacheStable === "0" || envCacheStable === "false" ? false : true;
|
|
@@ -6744,6 +6842,7 @@ async function loadConfig() {
|
|
|
6744
6842
|
model: m(envModel ?? persisted.model) ?? DEFAULT_MODEL,
|
|
6745
6843
|
openrouterProvider: persisted.openrouterProvider,
|
|
6746
6844
|
reasoningEffort: envEffort ?? persisted.reasoningEffort,
|
|
6845
|
+
subagentPolicy: envSubagentPolicy ?? (persisted.subagentPolicy === "off" || persisted.subagentPolicy === "suggest" || persisted.subagentPolicy === "auto" ? persisted.subagentPolicy : "suggest"),
|
|
6747
6846
|
modesEnabled: persisted.modesEnabled,
|
|
6748
6847
|
coauthor: envCoauthor?.enabled ?? persisted.coauthor ?? true,
|
|
6749
6848
|
coauthorName: envCoauthor?.name ?? persisted.coauthorName,
|
|
@@ -6777,6 +6876,7 @@ async function loadConfig() {
|
|
|
6777
6876
|
githubTokenExpiry: persisted.githubTokenExpiry,
|
|
6778
6877
|
githubRepo: persisted.githubRepo,
|
|
6779
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",
|
|
6780
6880
|
workerBudgetUsd: readNumberEnv("KIMIFLARE_WORKER_BUDGET_USD") ?? persisted.workerBudgetUsd,
|
|
6781
6881
|
workerBudgetMaxUsd: readNumberEnv("KIMIFLARE_WORKER_BUDGET_MAX_USD") ?? persisted.workerBudgetMaxUsd,
|
|
6782
6882
|
workerMaxParallel: readNumberEnv("KIMIFLARE_WORKER_MAX_PARALLEL") ?? persisted.workerMaxParallel,
|
|
@@ -6882,6 +6982,330 @@ async function saveConfig(cfg) {
|
|
|
6882
6982
|
return p;
|
|
6883
6983
|
}
|
|
6884
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
|
+
|
|
6885
7309
|
// src/tools/spawn-worker.ts
|
|
6886
7310
|
var DEFAULT_WORKER_TIMEOUT_MS = 3e5;
|
|
6887
7311
|
async function callWorkerEndpoint(endpoint, apiKey, payload, signal, timeoutMs = DEFAULT_WORKER_TIMEOUT_MS) {
|
|
@@ -6917,10 +7341,9 @@ async function callWorkerEndpoint(endpoint, apiKey, payload, signal, timeoutMs =
|
|
|
6917
7341
|
var spawnWorkerTool = {
|
|
6918
7342
|
name: "spawn_worker",
|
|
6919
7343
|
description: [
|
|
6920
|
-
"Spawn a standalone
|
|
6921
|
-
"
|
|
6922
|
-
"Mode '
|
|
6923
|
-
"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.",
|
|
6924
7347
|
"Use for heavy tasks that benefit from parallel research (e.g. 'research OAuth2, testing, and migration')."
|
|
6925
7348
|
].join(" "),
|
|
6926
7349
|
parameters: {
|
|
@@ -6953,11 +7376,11 @@ var spawnWorkerTool = {
|
|
|
6953
7376
|
tools: {
|
|
6954
7377
|
type: "string",
|
|
6955
7378
|
enum: ["all", "read-only"],
|
|
6956
|
-
description: "
|
|
7379
|
+
description: "Requested remote tool set. Local Hotcell research always uses a narrow read-only profile and rejects `all`."
|
|
6957
7380
|
},
|
|
6958
7381
|
model: {
|
|
6959
7382
|
type: "string",
|
|
6960
|
-
description:
|
|
7383
|
+
description: "Model to use for the worker. Defaults to the active session model."
|
|
6961
7384
|
},
|
|
6962
7385
|
branchName: {
|
|
6963
7386
|
type: "string",
|
|
@@ -6985,25 +7408,71 @@ var spawnWorkerTool = {
|
|
|
6985
7408
|
body: args.task.slice(0, 200)
|
|
6986
7409
|
}),
|
|
6987
7410
|
async run(args, ctx) {
|
|
6988
|
-
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;
|
|
6989
7463
|
if (!endpoint) {
|
|
6990
|
-
|
|
6991
|
-
const bytes = Buffer.byteLength(msg, "utf8");
|
|
6992
|
-
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.");
|
|
6993
7465
|
}
|
|
6994
7466
|
const apiKey = process.env.KIMIFLARE_WORKER_API_KEY;
|
|
6995
|
-
const timeoutMs = readNumberEnv2("KIMIFLARE_WORKER_TIMEOUT_MS") ?? DEFAULT_WORKER_TIMEOUT_MS;
|
|
6996
|
-
const cfg = await loadConfig().catch(() => null);
|
|
6997
|
-
const budgetUsd = resolveWorkerBudgetUsd(cfg);
|
|
6998
7467
|
const defaultModel = cfg?.model ?? DEFAULT_MODEL;
|
|
6999
7468
|
const payload = {
|
|
7000
7469
|
mode: args.mode,
|
|
7001
7470
|
task: args.task,
|
|
7002
7471
|
context: args.context ?? "",
|
|
7003
|
-
budget: { maxCostUsd:
|
|
7472
|
+
budget: { maxCostUsd: budgetCeiling },
|
|
7004
7473
|
outputFormat: args.outputFormat ?? "structured",
|
|
7005
7474
|
tools: args.tools ?? (args.mode === "plan" ? "read-only" : "all"),
|
|
7006
|
-
model: args.model ?? defaultModel,
|
|
7475
|
+
model: args.model ?? ctx.model ?? defaultModel,
|
|
7007
7476
|
...args.mode === "execute" ? {
|
|
7008
7477
|
branchName: args.branchName,
|
|
7009
7478
|
baseBranch: args.baseBranch ?? "main",
|
|
@@ -7057,6 +7526,10 @@ var spawnWorkerTool = {
|
|
|
7057
7526
|
}
|
|
7058
7527
|
}
|
|
7059
7528
|
};
|
|
7529
|
+
function textOutput(content) {
|
|
7530
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
7531
|
+
return { content, rawBytes: bytes, reducedBytes: bytes };
|
|
7532
|
+
}
|
|
7060
7533
|
function readNumberEnv2(name) {
|
|
7061
7534
|
const raw = process.env[name];
|
|
7062
7535
|
if (!raw) return void 0;
|
|
@@ -7670,6 +8143,8 @@ function makeExpandArtifactTool(store) {
|
|
|
7670
8143
|
}
|
|
7671
8144
|
|
|
7672
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";
|
|
7673
8148
|
var ALL_TOOLS = [
|
|
7674
8149
|
{ ...readTool, isReadOnly: true },
|
|
7675
8150
|
writeTool,
|
|
@@ -8553,7 +9028,7 @@ Context: This memory was explicitly provided by the user during a conversation.`
|
|
|
8553
9028
|
};
|
|
8554
9029
|
|
|
8555
9030
|
// src/lsp/connection.ts
|
|
8556
|
-
import { spawn as
|
|
9031
|
+
import { spawn as spawn3 } from "child_process";
|
|
8557
9032
|
import { EventEmitter } from "events";
|
|
8558
9033
|
var LspConnection = class extends EventEmitter {
|
|
8559
9034
|
child = null;
|
|
@@ -8570,7 +9045,7 @@ var LspConnection = class extends EventEmitter {
|
|
|
8570
9045
|
if (this.child) {
|
|
8571
9046
|
throw new Error("LSP connection already started");
|
|
8572
9047
|
}
|
|
8573
|
-
return new Promise((
|
|
9048
|
+
return new Promise((resolve6, reject) => {
|
|
8574
9049
|
const abortController = new AbortController();
|
|
8575
9050
|
const spawnTimer = setTimeout(() => {
|
|
8576
9051
|
abortController.abort();
|
|
@@ -8578,7 +9053,7 @@ var LspConnection = class extends EventEmitter {
|
|
|
8578
9053
|
reject(new Error(`LSP server spawn timed out after ${spawnTimeoutMs}ms`));
|
|
8579
9054
|
}, spawnTimeoutMs);
|
|
8580
9055
|
try {
|
|
8581
|
-
const child =
|
|
9056
|
+
const child = spawn3(command[0], command.slice(1), {
|
|
8582
9057
|
env: { ...process.env, ...env },
|
|
8583
9058
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8584
9059
|
});
|
|
@@ -8609,7 +9084,7 @@ var LspConnection = class extends EventEmitter {
|
|
|
8609
9084
|
if (child.pid) {
|
|
8610
9085
|
clearTimeout(spawnTimer);
|
|
8611
9086
|
this.child = child;
|
|
8612
|
-
|
|
9087
|
+
resolve6();
|
|
8613
9088
|
}
|
|
8614
9089
|
});
|
|
8615
9090
|
} catch (err) {
|
|
@@ -8624,12 +9099,12 @@ var LspConnection = class extends EventEmitter {
|
|
|
8624
9099
|
}
|
|
8625
9100
|
const id = this.nextId++;
|
|
8626
9101
|
const msg = { jsonrpc: "2.0", id, method, params };
|
|
8627
|
-
return new Promise((
|
|
9102
|
+
return new Promise((resolve6, reject) => {
|
|
8628
9103
|
const timer2 = setTimeout(() => {
|
|
8629
9104
|
this.pending.delete(id);
|
|
8630
9105
|
reject(toolTimeoutError(`LSP request '${method}'`, this.requestTimeoutMs));
|
|
8631
9106
|
}, this.requestTimeoutMs);
|
|
8632
|
-
const pending = { resolve:
|
|
9107
|
+
const pending = { resolve: resolve6, reject, signal, timer: timer2 };
|
|
8633
9108
|
this.pending.set(id, pending);
|
|
8634
9109
|
if (signal) {
|
|
8635
9110
|
const onAbort = () => {
|
|
@@ -9170,10 +9645,10 @@ var LspManager = class {
|
|
|
9170
9645
|
};
|
|
9171
9646
|
|
|
9172
9647
|
// src/lsp/adapter.ts
|
|
9173
|
-
import { relative as
|
|
9648
|
+
import { relative as relative4 } from "path";
|
|
9174
9649
|
function formatLocation(loc, cwd) {
|
|
9175
9650
|
const path = fromUri(loc.uri);
|
|
9176
|
-
const rel =
|
|
9651
|
+
const rel = relative4(cwd, path) || path;
|
|
9177
9652
|
return `${rel}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
|
|
9178
9653
|
}
|
|
9179
9654
|
function formatLocations(locs, cwd) {
|
|
@@ -9210,7 +9685,7 @@ function formatWorkspaceSymbols(symbols, cwd) {
|
|
|
9210
9685
|
if (!symbols || symbols.length === 0) return "No symbols found.";
|
|
9211
9686
|
return symbols.map((s) => {
|
|
9212
9687
|
const path = fromUri(s.location.uri);
|
|
9213
|
-
const rel =
|
|
9688
|
+
const rel = relative4(cwd, path) || path;
|
|
9214
9689
|
const container = s.containerName ? ` in ${s.containerName}` : "";
|
|
9215
9690
|
return `${s.name} (${symbolKindName(s.kind)})${container} \u2014 ${rel}:${s.location.range.start.line + 1}:${s.location.range.start.character + 1}`;
|
|
9216
9691
|
}).join("\n");
|
|
@@ -9230,7 +9705,7 @@ function formatWorkspaceEdit(edit, cwd) {
|
|
|
9230
9705
|
if (edit.changes) {
|
|
9231
9706
|
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
9232
9707
|
const path = fromUri(uri);
|
|
9233
|
-
const rel =
|
|
9708
|
+
const rel = relative4(cwd, path) || path;
|
|
9234
9709
|
lines.push(`File: ${rel}`);
|
|
9235
9710
|
for (const e of edits) {
|
|
9236
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}`);
|
|
@@ -9249,7 +9724,7 @@ function formatCodeActions(actions) {
|
|
|
9249
9724
|
}
|
|
9250
9725
|
|
|
9251
9726
|
// src/tools/lsp.ts
|
|
9252
|
-
import { relative as
|
|
9727
|
+
import { relative as relative5 } from "path";
|
|
9253
9728
|
function makeOutput2(content) {
|
|
9254
9729
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
9255
9730
|
return { content, rawBytes: bytes, reducedBytes: bytes };
|
|
@@ -9257,7 +9732,7 @@ function makeOutput2(content) {
|
|
|
9257
9732
|
function resolveLspPath(args, ctx) {
|
|
9258
9733
|
const raw = typeof args.path === "string" ? args.path : "";
|
|
9259
9734
|
const resolved = resolvePath(ctx.cwd, raw);
|
|
9260
|
-
const rel =
|
|
9735
|
+
const rel = relative5(ctx.cwd, resolved);
|
|
9261
9736
|
if (isPathOutside(rel)) {
|
|
9262
9737
|
throw new Error(`Path outside workspace: ${raw}`);
|
|
9263
9738
|
}
|
|
@@ -9990,7 +10465,7 @@ function createDefaultPermissionHandler(options) {
|
|
|
9990
10465
|
async function createAgentSession(opts) {
|
|
9991
10466
|
const config2 = await resolveSdkConfig(opts);
|
|
9992
10467
|
if (!resolveCustomEndpoint(config2)) await ensureOpenRouterCatalog();
|
|
9993
|
-
const cwd =
|
|
10468
|
+
const cwd = resolve5(opts.cwd ?? process.cwd());
|
|
9994
10469
|
const tools = opts.tools ?? ALL_TOOLS;
|
|
9995
10470
|
let hooks;
|
|
9996
10471
|
if (opts.enableHooks) {
|
|
@@ -10375,12 +10850,12 @@ var InternalSession = class {
|
|
|
10375
10850
|
toolName: req.tool.name,
|
|
10376
10851
|
args: req.args
|
|
10377
10852
|
});
|
|
10378
|
-
const decision = await new Promise((
|
|
10379
|
-
this.permissionResolvers.set(requestId,
|
|
10853
|
+
const decision = await new Promise((resolve6) => {
|
|
10854
|
+
this.permissionResolvers.set(requestId, resolve6);
|
|
10380
10855
|
setTimeout(() => {
|
|
10381
10856
|
if (this.permissionResolvers.has(requestId)) {
|
|
10382
10857
|
this.permissionResolvers.delete(requestId);
|
|
10383
|
-
|
|
10858
|
+
resolve6("deny");
|
|
10384
10859
|
}
|
|
10385
10860
|
}, 3e5);
|
|
10386
10861
|
});
|