@livx.cc/agentx 0.95.1 → 0.95.2
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/dist/{Agent-1DRfsYaK.d.ts → Agent-DRe91tAy.d.ts} +5 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +578 -421
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +387 -232
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1588,7 +1588,7 @@ function makeRealShellTool(options) {
|
|
|
1588
1588
|
proc.stderr?.on("data", collect);
|
|
1589
1589
|
proc.on("error", (err) => {
|
|
1590
1590
|
if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
1591
|
-
|
|
1591
|
+
log5.debug("shell spawn error", err);
|
|
1592
1592
|
finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
|
|
1593
1593
|
});
|
|
1594
1594
|
proc.on("close", (code) => {
|
|
@@ -1650,7 +1650,7 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
1650
1650
|
}
|
|
1651
1651
|
];
|
|
1652
1652
|
}
|
|
1653
|
-
var
|
|
1653
|
+
var log5, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
|
|
1654
1654
|
var init_tools_shell = __esm({
|
|
1655
1655
|
"src/tools.shell.ts"() {
|
|
1656
1656
|
"use strict";
|
|
@@ -1658,7 +1658,7 @@ var init_tools_shell = __esm({
|
|
|
1658
1658
|
init_redact();
|
|
1659
1659
|
init_logging();
|
|
1660
1660
|
init_shell_sandbox();
|
|
1661
|
-
|
|
1661
|
+
log5 = forComponent("shell");
|
|
1662
1662
|
clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
1663
1663
|
DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
|
|
1664
1664
|
SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
|
|
@@ -2435,7 +2435,163 @@ ${sections.join("\n\n---\n\n")}`;
|
|
|
2435
2435
|
|
|
2436
2436
|
// src/subagent.ts
|
|
2437
2437
|
init_tools();
|
|
2438
|
+
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
2439
|
+
import { join as join3 } from "path";
|
|
2438
2440
|
init_OverlayFilesystem();
|
|
2441
|
+
|
|
2442
|
+
// src/worktree.ts
|
|
2443
|
+
import { spawnSync } from "child_process";
|
|
2444
|
+
import { symlinkSync, mkdirSync, statSync, readFileSync, rmSync } from "fs";
|
|
2445
|
+
import { join } from "path";
|
|
2446
|
+
var VALID_SEGMENT = /^[a-zA-Z0-9._-]+$/;
|
|
2447
|
+
var MAX_SLUG_LENGTH = 64;
|
|
2448
|
+
var GIT_NO_PROMPT_ENV = { GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "" };
|
|
2449
|
+
function validateWorktreeSlug(slug2) {
|
|
2450
|
+
if (!slug2) throw new Error("worktree slug must not be empty");
|
|
2451
|
+
if (slug2.length > MAX_SLUG_LENGTH) throw new Error(`worktree slug must be \u2264${MAX_SLUG_LENGTH} chars (got ${slug2.length})`);
|
|
2452
|
+
for (const seg of slug2.split("/")) {
|
|
2453
|
+
if (seg === "." || seg === "..") throw new Error(`worktree slug must not contain "." or ".." segments`);
|
|
2454
|
+
if (!VALID_SEGMENT.test(seg)) throw new Error(`worktree slug segment "${seg}" contains invalid characters (allowed: a-z A-Z 0-9 . _ -)`);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
function flattenSlug(slug2) {
|
|
2458
|
+
return slug2.replaceAll("/", "+");
|
|
2459
|
+
}
|
|
2460
|
+
function worktreeBranchName(slug2) {
|
|
2461
|
+
return `worktree-${flattenSlug(slug2)}`;
|
|
2462
|
+
}
|
|
2463
|
+
function worktreesDir(repoRoot) {
|
|
2464
|
+
return join(repoRoot, ".claude", "worktrees");
|
|
2465
|
+
}
|
|
2466
|
+
function worktreePathFor(repoRoot, slug2) {
|
|
2467
|
+
return join(worktreesDir(repoRoot), flattenSlug(slug2));
|
|
2468
|
+
}
|
|
2469
|
+
function git(args, cwd) {
|
|
2470
|
+
const r = spawnSync("git", args, {
|
|
2471
|
+
cwd,
|
|
2472
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2473
|
+
env: { ...process.env, ...GIT_NO_PROMPT_ENV }
|
|
2474
|
+
});
|
|
2475
|
+
return { ok: r.status === 0, stdout: (r.stdout ?? "").toString().trim(), stderr: (r.stderr ?? "").toString().trim() };
|
|
2476
|
+
}
|
|
2477
|
+
function findGitRoot(from) {
|
|
2478
|
+
const r = git(["rev-parse", "--show-toplevel"], from);
|
|
2479
|
+
return r.ok ? r.stdout : null;
|
|
2480
|
+
}
|
|
2481
|
+
function readWorktreeHead(worktreePath) {
|
|
2482
|
+
try {
|
|
2483
|
+
const gitFile = readFileSync(join(worktreePath, ".git"), "utf-8").trim();
|
|
2484
|
+
const m = gitFile.match(/^gitdir:\s*(.+)$/);
|
|
2485
|
+
if (!m) return null;
|
|
2486
|
+
const headPath = join(m[1], "HEAD");
|
|
2487
|
+
const head = readFileSync(headPath, "utf-8").trim();
|
|
2488
|
+
const refMatch = head.match(/^ref:\s*(.+)$/);
|
|
2489
|
+
if (!refMatch) return head.length === 40 ? head : null;
|
|
2490
|
+
const refPath = join(m[1], "..", refMatch[1]);
|
|
2491
|
+
try {
|
|
2492
|
+
return readFileSync(refPath, "utf-8").trim();
|
|
2493
|
+
} catch {
|
|
2494
|
+
}
|
|
2495
|
+
const r = git(["rev-parse", "HEAD"], worktreePath);
|
|
2496
|
+
return r.ok ? r.stdout : null;
|
|
2497
|
+
} catch {
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
function getOrCreateWorktree(repoRoot, slug2) {
|
|
2502
|
+
validateWorktreeSlug(slug2);
|
|
2503
|
+
const worktreePath = worktreePathFor(repoRoot, slug2);
|
|
2504
|
+
const branch = worktreeBranchName(slug2);
|
|
2505
|
+
const existingHead = readWorktreeHead(worktreePath);
|
|
2506
|
+
if (existingHead) return { worktreePath, branch, headCommit: existingHead, existed: true };
|
|
2507
|
+
mkdirSync(worktreesDir(repoRoot), { recursive: true });
|
|
2508
|
+
const defaultBranch = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoRoot);
|
|
2509
|
+
let baseBranch = defaultBranch.ok ? defaultBranch.stdout.replace(/^origin\//, "") : "HEAD";
|
|
2510
|
+
const originRef = `origin/${baseBranch}`;
|
|
2511
|
+
const hasOrigin = git(["rev-parse", "--verify", originRef], repoRoot);
|
|
2512
|
+
if (!hasOrigin.ok) {
|
|
2513
|
+
const fetched = git(["fetch", "origin", baseBranch], repoRoot);
|
|
2514
|
+
if (!fetched.ok) baseBranch = "HEAD";
|
|
2515
|
+
}
|
|
2516
|
+
const base = baseBranch === "HEAD" ? "HEAD" : originRef;
|
|
2517
|
+
const r = git(["worktree", "add", "-B", branch, worktreePath, base], repoRoot);
|
|
2518
|
+
if (!r.ok) throw new Error(`failed to create worktree: ${r.stderr}`);
|
|
2519
|
+
const headSha = git(["rev-parse", "HEAD"], worktreePath);
|
|
2520
|
+
symlinkDirs(repoRoot, worktreePath, ["node_modules", ".bun"]);
|
|
2521
|
+
return { worktreePath, branch, headCommit: headSha.stdout, existed: false, baseBranch };
|
|
2522
|
+
}
|
|
2523
|
+
function symlinkDirs(repoRoot, worktreePath, dirs) {
|
|
2524
|
+
for (const dir of dirs) {
|
|
2525
|
+
const src = join(repoRoot, dir);
|
|
2526
|
+
const dst = join(worktreePath, dir);
|
|
2527
|
+
try {
|
|
2528
|
+
statSync(src);
|
|
2529
|
+
} catch {
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
try {
|
|
2533
|
+
symlinkSync(src, dst, "dir");
|
|
2534
|
+
} catch (e) {
|
|
2535
|
+
if (e?.code !== "EEXIST") console.error(`[worktree] symlink ${dir}: ${e?.message ?? e}`);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
function cleanupWorktree(repoRoot, slug2, opts) {
|
|
2540
|
+
validateWorktreeSlug(slug2);
|
|
2541
|
+
const worktreePath = worktreePathFor(repoRoot, slug2);
|
|
2542
|
+
const branch = worktreeBranchName(slug2);
|
|
2543
|
+
try {
|
|
2544
|
+
statSync(worktreePath);
|
|
2545
|
+
} catch {
|
|
2546
|
+
return { removed: false, reason: "not found" };
|
|
2547
|
+
}
|
|
2548
|
+
if (!opts?.force) {
|
|
2549
|
+
const status = git(["status", "--porcelain", "-uno"], worktreePath);
|
|
2550
|
+
if (status.ok && status.stdout) return { removed: false, reason: "uncommitted changes" };
|
|
2551
|
+
}
|
|
2552
|
+
for (const dir of ["node_modules", ".bun"]) {
|
|
2553
|
+
try {
|
|
2554
|
+
rmSync(join(worktreePath, dir));
|
|
2555
|
+
} catch {
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
const rm = git(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
2559
|
+
if (!rm.ok) return { removed: false, reason: rm.stderr };
|
|
2560
|
+
git(["branch", "-D", branch], repoRoot);
|
|
2561
|
+
return { removed: true };
|
|
2562
|
+
}
|
|
2563
|
+
var _session = null;
|
|
2564
|
+
function getWorktreeSession() {
|
|
2565
|
+
return _session;
|
|
2566
|
+
}
|
|
2567
|
+
function enterWorktree(slug2, cwd) {
|
|
2568
|
+
const repoRoot = findGitRoot(cwd);
|
|
2569
|
+
if (!repoRoot) throw new Error("--worktree requires a git repository");
|
|
2570
|
+
const info = getOrCreateWorktree(repoRoot, slug2);
|
|
2571
|
+
process.chdir(info.worktreePath);
|
|
2572
|
+
_session = {
|
|
2573
|
+
originalCwd: cwd,
|
|
2574
|
+
worktreePath: info.worktreePath,
|
|
2575
|
+
slug: slug2,
|
|
2576
|
+
branch: info.branch,
|
|
2577
|
+
headCommit: info.headCommit
|
|
2578
|
+
};
|
|
2579
|
+
return _session;
|
|
2580
|
+
}
|
|
2581
|
+
function exitWorktree() {
|
|
2582
|
+
if (!_session) return null;
|
|
2583
|
+
const repoRoot = findGitRoot(_session.originalCwd);
|
|
2584
|
+
if (!repoRoot) return null;
|
|
2585
|
+
process.chdir(_session.originalCwd);
|
|
2586
|
+
const result = cleanupWorktree(repoRoot, _session.slug);
|
|
2587
|
+
_session = null;
|
|
2588
|
+
return result;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// src/subagent.ts
|
|
2592
|
+
init_NodeDiskFilesystem();
|
|
2593
|
+
init_logging();
|
|
2594
|
+
var log3 = forComponent("subagent");
|
|
2439
2595
|
async function boundedPool(items, limit, fn) {
|
|
2440
2596
|
const out = new Array(items.length);
|
|
2441
2597
|
let next = 0;
|
|
@@ -2448,13 +2604,86 @@ async function boundedPool(items, limit, fn) {
|
|
|
2448
2604
|
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
2449
2605
|
return out;
|
|
2450
2606
|
}
|
|
2607
|
+
function sanitizeSlug(raw) {
|
|
2608
|
+
return raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").slice(0, 40);
|
|
2609
|
+
}
|
|
2610
|
+
function createWorktreeFs(cwd, label, index) {
|
|
2611
|
+
const repoRoot = findGitRoot(cwd);
|
|
2612
|
+
if (!repoRoot) return 'isolation: "worktree" requires a git repository';
|
|
2613
|
+
const slug2 = `sub-${index}-${sanitizeSlug(label)}`;
|
|
2614
|
+
try {
|
|
2615
|
+
const info = getOrCreateWorktree(repoRoot, slug2);
|
|
2616
|
+
return { repoRoot, slug: slug2, worktreePath: info.worktreePath, branch: info.branch };
|
|
2617
|
+
} catch (e) {
|
|
2618
|
+
return `worktree creation failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
function releaseWorktree(handle) {
|
|
2622
|
+
return cleanupWorktree(handle.repoRoot, handle.slug, { force: true });
|
|
2623
|
+
}
|
|
2624
|
+
function worktreePromptPrefix(branch) {
|
|
2625
|
+
return `You are running in an isolated git worktree on branch "${branch}". When you finish making changes, commit them with a clear message before replying with your summary. Your worktree will be removed after you finish \u2014 only committed work survives (on the branch).
|
|
2626
|
+
|
|
2627
|
+
`;
|
|
2628
|
+
}
|
|
2629
|
+
var traceSeq = 0;
|
|
2630
|
+
function traceFileFor(dir, label, depth) {
|
|
2631
|
+
try {
|
|
2632
|
+
mkdirSync2(dir, { recursive: true });
|
|
2633
|
+
const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
|
|
2634
|
+
return join3(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
|
|
2635
|
+
} catch (e) {
|
|
2636
|
+
log3.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
|
|
2637
|
+
return void 0;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
function flushTrace(file, meta, messages, status) {
|
|
2641
|
+
try {
|
|
2642
|
+
writeFileSync(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
|
|
2643
|
+
} catch (e) {
|
|
2644
|
+
log3.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
function installTrace(child, file, meta) {
|
|
2648
|
+
const prev = child.options.host;
|
|
2649
|
+
child.options.host = {
|
|
2650
|
+
...prev,
|
|
2651
|
+
notify: (e) => {
|
|
2652
|
+
if (e.kind === "turn_start") flushTrace(file, meta, child.transcript, "running");
|
|
2653
|
+
prev?.notify?.(e);
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
function abortHint(label, res, file) {
|
|
2658
|
+
const last = (res.text || "").trim().slice(0, 500);
|
|
2659
|
+
const lines = [
|
|
2660
|
+
`[sub-task '${label}' did NOT finish \u2014 ${res.finishReason} after ${res.steps} step(s). Treat its work as INCOMPLETE.]`,
|
|
2661
|
+
last ? `Progress so far: ${last}` : "It produced no summary before stopping."
|
|
2662
|
+
];
|
|
2663
|
+
if (file) lines.push(`Full transcript (every step it took): ${file} \u2014 Read it to see what was actually done.`);
|
|
2664
|
+
lines.push("Decide: ignore it, re-delegate a fresh Task that continues from the trace, or start over. Do NOT assume the sub-task completed.");
|
|
2665
|
+
return lines.join("\n");
|
|
2666
|
+
}
|
|
2667
|
+
async function runChildTraced(args) {
|
|
2668
|
+
const { opts, label, agentType, prompt } = args;
|
|
2669
|
+
if (args.signal) args.childOpts.signal = args.signal;
|
|
2670
|
+
const file = opts.traceDir ? traceFileFor(opts.traceDir, label, args.childDepth) : void 0;
|
|
2671
|
+
const meta = { label, agentType, prompt };
|
|
2672
|
+
const child = new Agent(args.childOpts);
|
|
2673
|
+
if (file) installTrace(child, file, meta);
|
|
2674
|
+
const res = await child.run(prompt);
|
|
2675
|
+
if (file) flushTrace(file, meta, child.transcript, res.finishReason);
|
|
2676
|
+
const result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
|
|
2677
|
+
await opts.hooks?.onSubagentStop?.(result, { label, agentType });
|
|
2678
|
+
return { res, result };
|
|
2679
|
+
}
|
|
2451
2680
|
function childOptionsFor(opts, fs, depth, maxDepth, agentType) {
|
|
2452
2681
|
let def;
|
|
2453
2682
|
if (agentType) {
|
|
2454
2683
|
def = (opts.agents ?? []).find((a) => a.name === agentType);
|
|
2455
2684
|
if (!def) return `no subagent type '${agentType}'. Available: ${(opts.agents ?? []).map((a) => a.name).join(", ") || "(none defined)"}`;
|
|
2456
2685
|
}
|
|
2457
|
-
const childOpts = { ai: opts.ai, fs, model: def?.model ?? opts.model, subagents: true, depth: depth + 1, maxDepth };
|
|
2686
|
+
const childOpts = { ai: opts.ai, fs, model: def?.model ?? opts.model, subagents: true, depth: depth + 1, maxDepth, traceDir: opts.traceDir };
|
|
2458
2687
|
if (opts.maxSteps != null) childOpts.maxSteps = opts.maxSteps;
|
|
2459
2688
|
if (def?.systemPrompt) childOpts.systemPrompt = def.systemPrompt;
|
|
2460
2689
|
if (def?.tools?.length) {
|
|
@@ -2471,7 +2700,7 @@ function makeTaskTool(opts) {
|
|
|
2471
2700
|
const maxDepth = opts.maxDepth ?? 2;
|
|
2472
2701
|
return {
|
|
2473
2702
|
name: "Task",
|
|
2474
|
-
description:
|
|
2703
|
+
description: 'Delegate a self-contained sub-task to a child agent over the same filesystem. It runs autonomously with its own step budget and returns a concise summary \u2014 use to isolate context-heavy work (broad search, a scoped refactor). Provide a short `description` and a full `prompt`. If it is interrupted or fails, the result is a STATUS HINT with a pointer to its full transcript (Read it to recover what it did, then decide whether to continue or restart) \u2014 do not assume an incomplete sub-task succeeded. Set `background: true` to run it detached (returns a job id to poll with JobOutput while you keep working); its file edits are overlay-isolated and commit when it finishes. Set `isolation: "worktree"` to run in a real git worktree (needed when the child uses real shell commands that must see/modify actual files) \u2014 slower (~200ms setup), auto-cleaned if no changes.',
|
|
2475
2704
|
parameters: {
|
|
2476
2705
|
type: "object",
|
|
2477
2706
|
required: ["description", "prompt"],
|
|
@@ -2479,36 +2708,83 @@ function makeTaskTool(opts) {
|
|
|
2479
2708
|
description: { type: "string", description: "a short (3-5 word) label for the sub-task" },
|
|
2480
2709
|
prompt: { type: "string", description: "the full instructions the child agent should carry out" },
|
|
2481
2710
|
agentType: { type: "string", description: "optional named subagent type (its persona, model, and scoped tools) \u2014 see the catalog" },
|
|
2482
|
-
background: { type: "boolean", description: "run detached (overlay-isolated, commits on finish); returns a job id to poll with JobOutput instead of blocking" }
|
|
2711
|
+
background: { type: "boolean", description: "run detached (overlay-isolated, commits on finish); returns a job id to poll with JobOutput instead of blocking" },
|
|
2712
|
+
isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode: "overlay" (default, VFS layer) or "worktree" (real git worktree for shell-heavy work)' }
|
|
2483
2713
|
}
|
|
2484
2714
|
},
|
|
2485
|
-
async run({ description, prompt, agentType, background }, ctx) {
|
|
2715
|
+
async run({ description, prompt, agentType, background, isolation }, ctx) {
|
|
2486
2716
|
if (depth >= maxDepth) {
|
|
2487
2717
|
return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn another child agent \u2014 do this work directly instead.`;
|
|
2488
2718
|
}
|
|
2489
2719
|
const label = String(description ?? agentType ?? "sub-task");
|
|
2720
|
+
const useWorktree = isolation === "worktree";
|
|
2490
2721
|
if (background && ctx.jobs) {
|
|
2491
2722
|
const id = ctx.jobs.start(async ({ signal }) => {
|
|
2492
|
-
|
|
2493
|
-
|
|
2723
|
+
let fs2;
|
|
2724
|
+
let commit;
|
|
2725
|
+
let wtHandle2;
|
|
2726
|
+
if (useWorktree) {
|
|
2727
|
+
const h = createWorktreeFs(process.cwd(), label, 0);
|
|
2728
|
+
if (typeof h === "string") throw new Error(h);
|
|
2729
|
+
wtHandle2 = h;
|
|
2730
|
+
fs2 = new NodeDiskFilesystem(h.worktreePath);
|
|
2731
|
+
commit = async () => {
|
|
2732
|
+
};
|
|
2733
|
+
} else {
|
|
2734
|
+
const overlay = new OverlayFilesystem(opts.fs);
|
|
2735
|
+
fs2 = overlay;
|
|
2736
|
+
commit = () => overlay.commit();
|
|
2737
|
+
}
|
|
2738
|
+
const childOpts2 = childOptionsFor(opts, fs2, depth, maxDepth, agentType);
|
|
2494
2739
|
if (typeof childOpts2 === "string") throw new Error(childOpts2);
|
|
2495
|
-
|
|
2496
|
-
const
|
|
2497
|
-
if (
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2740
|
+
const childPrompt = wtHandle2 ? worktreePromptPrefix(wtHandle2.branch) + String(prompt ?? "") : String(prompt ?? "");
|
|
2741
|
+
const { res, result } = await runChildTraced({ opts, childOpts: childOpts2, prompt: childPrompt, label, agentType, childDepth: depth + 1, signal });
|
|
2742
|
+
if (res.finishReason === "aborted" || signal.aborted) {
|
|
2743
|
+
wtHandle2 && releaseWorktree(wtHandle2);
|
|
2744
|
+
return result;
|
|
2745
|
+
}
|
|
2746
|
+
await commit();
|
|
2747
|
+
let summary = result;
|
|
2748
|
+
if (wtHandle2) {
|
|
2749
|
+
const r = releaseWorktree(wtHandle2);
|
|
2750
|
+
if (!r.removed) summary += `
|
|
2751
|
+
|
|
2752
|
+
[worktree removal failed: ${r.reason}]`;
|
|
2753
|
+
}
|
|
2754
|
+
return summary;
|
|
2502
2755
|
}, { kind: "agent", label });
|
|
2503
2756
|
return `Started background sub-task ${id} ('${label}') \u2014 poll with JobOutput({id:"${id}"}).`;
|
|
2504
2757
|
}
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2758
|
+
let fs;
|
|
2759
|
+
let wtHandle;
|
|
2760
|
+
if (useWorktree) {
|
|
2761
|
+
const h = createWorktreeFs(process.cwd(), label, 0);
|
|
2762
|
+
if (typeof h === "string") return `Error: ${h}`;
|
|
2763
|
+
wtHandle = h;
|
|
2764
|
+
fs = new NodeDiskFilesystem(h.worktreePath);
|
|
2765
|
+
} else {
|
|
2766
|
+
fs = opts.fs;
|
|
2767
|
+
}
|
|
2768
|
+
const childOpts = childOptionsFor(opts, fs, depth, maxDepth, agentType);
|
|
2769
|
+
if (typeof childOpts === "string") {
|
|
2770
|
+
wtHandle && releaseWorktree(wtHandle);
|
|
2771
|
+
return `Error: ${childOpts}`;
|
|
2772
|
+
}
|
|
2773
|
+
try {
|
|
2774
|
+
const childPrompt = wtHandle ? worktreePromptPrefix(wtHandle.branch) + String(prompt ?? "") : String(prompt ?? "");
|
|
2775
|
+
const { result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType, childDepth: depth + 1, signal: ctx.signal });
|
|
2776
|
+
let summary = result;
|
|
2777
|
+
if (wtHandle) {
|
|
2778
|
+
const r = releaseWorktree(wtHandle);
|
|
2779
|
+
if (!r.removed) summary += `
|
|
2780
|
+
|
|
2781
|
+
[worktree removal failed: ${r.reason}]`;
|
|
2782
|
+
}
|
|
2783
|
+
return summary;
|
|
2784
|
+
} catch (e) {
|
|
2785
|
+
if (wtHandle) releaseWorktree(wtHandle);
|
|
2786
|
+
throw e;
|
|
2787
|
+
}
|
|
2512
2788
|
}
|
|
2513
2789
|
};
|
|
2514
2790
|
}
|
|
@@ -2518,7 +2794,7 @@ function makeTaskBatchTool(opts) {
|
|
|
2518
2794
|
const maxParallel = opts.maxParallel ?? 4;
|
|
2519
2795
|
return {
|
|
2520
2796
|
name: "TaskBatch",
|
|
2521
|
-
description:
|
|
2797
|
+
description: 'Delegate SEVERAL independent sub-tasks to child agents that run concurrently; returns all their summaries. Each child is write-isolated (its file edits are merged back in array order). Use for parallel fan-out (review/search/scoped refactors across files). Provide `tasks: [{ description, prompt, agentType? }]`. Set `isolation: "worktree"` to run each child in its own git worktree (needed for real shell commands) \u2014 slower, auto-cleaned if no changes.',
|
|
2522
2798
|
parameters: {
|
|
2523
2799
|
type: "object",
|
|
2524
2800
|
required: ["tasks"],
|
|
@@ -2535,30 +2811,53 @@ function makeTaskBatchTool(opts) {
|
|
|
2535
2811
|
agentType: { type: "string", description: "optional named subagent type" }
|
|
2536
2812
|
}
|
|
2537
2813
|
}
|
|
2538
|
-
}
|
|
2814
|
+
},
|
|
2815
|
+
isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode for ALL children: "overlay" (default) or "worktree" (real git worktree each)' }
|
|
2539
2816
|
}
|
|
2540
2817
|
},
|
|
2541
|
-
async run({ tasks },
|
|
2818
|
+
async run({ tasks, isolation }, ctx) {
|
|
2542
2819
|
if (depth >= maxDepth) return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn child agents \u2014 do this work directly instead.`;
|
|
2543
2820
|
const list = Array.isArray(tasks) ? tasks : [];
|
|
2544
2821
|
if (!list.length) return "Error: TaskBatch needs a non-empty `tasks` array.";
|
|
2822
|
+
const useWorktree = isolation === "worktree";
|
|
2545
2823
|
const results = await boundedPool(list, maxParallel, async (t, i) => {
|
|
2546
2824
|
const label = String(t?.description ?? t?.agentType ?? `task ${i + 1}`);
|
|
2547
|
-
|
|
2548
|
-
|
|
2825
|
+
let fs;
|
|
2826
|
+
let overlay;
|
|
2827
|
+
let wtHandle;
|
|
2828
|
+
if (useWorktree) {
|
|
2829
|
+
const h = createWorktreeFs(process.cwd(), label, i);
|
|
2830
|
+
if (typeof h === "string") return { label, error: h, ok: false };
|
|
2831
|
+
wtHandle = h;
|
|
2832
|
+
fs = new NodeDiskFilesystem(h.worktreePath);
|
|
2833
|
+
} else {
|
|
2834
|
+
overlay = new OverlayFilesystem(opts.fs);
|
|
2835
|
+
fs = overlay;
|
|
2836
|
+
}
|
|
2837
|
+
const childOpts = childOptionsFor(opts, fs, depth, maxDepth, t?.agentType);
|
|
2549
2838
|
if (typeof childOpts === "string") return { label, error: childOpts, ok: false };
|
|
2550
2839
|
try {
|
|
2551
|
-
const
|
|
2552
|
-
|
|
2553
|
-
return { label, text:
|
|
2840
|
+
const childPrompt = wtHandle ? worktreePromptPrefix(wtHandle.branch) + String(t?.prompt ?? "") : String(t?.prompt ?? "");
|
|
2841
|
+
const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal: ctx.signal });
|
|
2842
|
+
return { label, text: result, overlay, wtHandle, ok: res.finishReason !== "error" && res.finishReason !== "aborted" };
|
|
2554
2843
|
} catch (e) {
|
|
2555
|
-
return { label, error: e instanceof Error ? e.message : String(e), ok: false };
|
|
2844
|
+
return { label, error: e instanceof Error ? e.message : String(e), wtHandle, ok: false };
|
|
2556
2845
|
}
|
|
2557
2846
|
});
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2847
|
+
const lines = [];
|
|
2848
|
+
for (const r of results) {
|
|
2849
|
+
if (r.ok && r.overlay) await r.overlay.commit().catch(() => {
|
|
2850
|
+
});
|
|
2851
|
+
let extra = "";
|
|
2852
|
+
if (r.wtHandle) {
|
|
2853
|
+
const wr = releaseWorktree(r.wtHandle);
|
|
2854
|
+
if (!wr.removed) extra = `
|
|
2855
|
+
[worktree removal failed: ${wr.reason}]`;
|
|
2856
|
+
}
|
|
2857
|
+
lines.push(`### ${lines.length + 1}. ${r.label}
|
|
2858
|
+
${r.error ? `ERROR: ${r.error}` : r.text || "(no summary)"}${extra}`);
|
|
2859
|
+
}
|
|
2860
|
+
return lines.join("\n\n");
|
|
2562
2861
|
}
|
|
2563
2862
|
};
|
|
2564
2863
|
}
|
|
@@ -2803,7 +3102,7 @@ function reasoningToChatFragment(model, effort) {
|
|
|
2803
3102
|
}
|
|
2804
3103
|
|
|
2805
3104
|
// src/Agent.ts
|
|
2806
|
-
var
|
|
3105
|
+
var log4 = forComponent("Agent");
|
|
2807
3106
|
function isAbortError(err) {
|
|
2808
3107
|
const e = err;
|
|
2809
3108
|
const blob = `${e?.message ?? ""} ${e?.name ?? ""} ${e?.code ?? ""} ${e?.cause?.name ?? ""}`;
|
|
@@ -2879,6 +3178,11 @@ var AgentOptions = class {
|
|
|
2879
3178
|
depth = 0;
|
|
2880
3179
|
/** Hard ceiling on subagent nesting (beyond it the `Task` tool refuses to spawn). */
|
|
2881
3180
|
maxDepth = 2;
|
|
3181
|
+
/** Real-disk dir where child agents stream their transcript as they run (origin-anchored — survives
|
|
3182
|
+
* worktree teardown / a child's own cwd). When set, an interrupted/failed child commits a STATUS HINT
|
|
3183
|
+
* + a pointer to its trace instead of a dangling tool_call, so the parent can read it and decide
|
|
3184
|
+
* whether to ignore, pick it up, or restart. Unset (library/sandbox default) = no tracing. */
|
|
3185
|
+
traceDir;
|
|
2882
3186
|
/** Stream tokens from the model. Takes effect only with a `host.notify`; off => current (non-stream) behavior. */
|
|
2883
3187
|
stream = false;
|
|
2884
3188
|
/** Fold the dropped middle of an over-long transcript into a synthetic summary (edge-safe, no LLM). Off => drop-oldest. */
|
|
@@ -2999,7 +3303,7 @@ var Agent = class _Agent {
|
|
|
2999
3303
|
const disk = new NodeDiskFilesystem2(process.cwd());
|
|
3000
3304
|
await disk.init();
|
|
3001
3305
|
this.options.fs = new JailedFilesystem2(disk);
|
|
3002
|
-
|
|
3306
|
+
log4.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
|
|
3003
3307
|
}
|
|
3004
3308
|
this.buildCtx();
|
|
3005
3309
|
}
|
|
@@ -3053,7 +3357,7 @@ var Agent = class _Agent {
|
|
|
3053
3357
|
agents = loaded.agents;
|
|
3054
3358
|
if (loaded.catalog) systemPrompt += "\n\n" + loaded.catalog;
|
|
3055
3359
|
}
|
|
3056
|
-
const taskOpts = { ai: o.ai, model: o.model, fs, depth: o.depth, maxDepth: o.maxDepth, agents, hooks: o.hooks };
|
|
3360
|
+
const taskOpts = { ai: o.ai, model: o.model, fs, depth: o.depth, maxDepth: o.maxDepth, agents, hooks: o.hooks, traceDir: o.traceDir };
|
|
3057
3361
|
tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
|
|
3058
3362
|
}
|
|
3059
3363
|
if (o.checkpoints) tools = [...tools, ...checkpointTools()];
|
|
@@ -3142,7 +3446,7 @@ var Agent = class _Agent {
|
|
|
3142
3446
|
let lastFp = "";
|
|
3143
3447
|
let repeats = 0;
|
|
3144
3448
|
const kill = (finishReason) => {
|
|
3145
|
-
|
|
3449
|
+
log4.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
|
|
3146
3450
|
this.ctx.jobs?.killAll();
|
|
3147
3451
|
return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated };
|
|
3148
3452
|
};
|
|
@@ -3188,7 +3492,7 @@ var Agent = class _Agent {
|
|
|
3188
3492
|
const transient = !o.signal?.aborted && !isAbortError(err) && attempt < 2 && (network || serverSide);
|
|
3189
3493
|
if (!transient) throw err;
|
|
3190
3494
|
const waitMs = 1e3 * (attempt + 1);
|
|
3191
|
-
|
|
3495
|
+
log4.warn(`network drop mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
|
|
3192
3496
|
o.host?.notify?.({ kind: "retry", message: `connection dropped \u2014 retrying step (#${attempt + 1})` });
|
|
3193
3497
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
3194
3498
|
}
|
|
@@ -3204,7 +3508,7 @@ var Agent = class _Agent {
|
|
|
3204
3508
|
bodyStr = void 0;
|
|
3205
3509
|
}
|
|
3206
3510
|
if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
|
|
3207
|
-
|
|
3511
|
+
log4.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, err);
|
|
3208
3512
|
return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
|
|
3209
3513
|
}
|
|
3210
3514
|
if (o.signal?.aborted) return kill("aborted");
|
|
@@ -3231,7 +3535,7 @@ var Agent = class _Agent {
|
|
|
3231
3535
|
});
|
|
3232
3536
|
}
|
|
3233
3537
|
if (toolCalls.length === 0) {
|
|
3234
|
-
|
|
3538
|
+
log4.verbose(`completed in ${steps} step(s)`);
|
|
3235
3539
|
await this.ctx.jobs?.drain();
|
|
3236
3540
|
await this.activeHooks?.onStop?.(res.content ?? "");
|
|
3237
3541
|
return { text: res.content ?? "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated };
|
|
@@ -3297,13 +3601,13 @@ var Agent = class _Agent {
|
|
|
3297
3601
|
const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
|
|
3298
3602
|
if (decision?.block) {
|
|
3299
3603
|
const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
|
|
3300
|
-
|
|
3604
|
+
log4.debug(`${tc.function.name} -> ${blocked}`);
|
|
3301
3605
|
await hooks?.postToolUse?.(call, blocked, meta);
|
|
3302
3606
|
return blocked;
|
|
3303
3607
|
}
|
|
3304
3608
|
this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
|
|
3305
3609
|
if (earlyError) {
|
|
3306
|
-
|
|
3610
|
+
log4.debug(`${tc.function.name} -> ${earlyError}`);
|
|
3307
3611
|
await hooks?.postToolUse?.(call, earlyError, meta);
|
|
3308
3612
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
|
|
3309
3613
|
return earlyError;
|
|
@@ -3312,12 +3616,12 @@ var Agent = class _Agent {
|
|
|
3312
3616
|
let images;
|
|
3313
3617
|
let threw = false;
|
|
3314
3618
|
try {
|
|
3315
|
-
|
|
3619
|
+
log4.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
3316
3620
|
this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
|
|
3317
3621
|
try {
|
|
3318
3622
|
hooks.onToolOutput(call, chunk, meta);
|
|
3319
3623
|
} catch (e) {
|
|
3320
|
-
|
|
3624
|
+
log4.debug(`onToolOutput hook error: ${e}`);
|
|
3321
3625
|
}
|
|
3322
3626
|
} : void 0;
|
|
3323
3627
|
const raw = await tool.run(args, this.ctx);
|
|
@@ -3329,7 +3633,7 @@ var Agent = class _Agent {
|
|
|
3329
3633
|
}
|
|
3330
3634
|
} catch (e) {
|
|
3331
3635
|
const msg = e instanceof Error ? e.message : String(e);
|
|
3332
|
-
|
|
3636
|
+
log4.debug(`${tc.function.name} -> error: ${msg}`);
|
|
3333
3637
|
result = `Error: ${msg}`;
|
|
3334
3638
|
threw = true;
|
|
3335
3639
|
} finally {
|
|
@@ -3455,7 +3759,7 @@ function fitTokenBudget(messages, maxTokens) {
|
|
|
3455
3759
|
const ids = callIdSet(messages.slice(from));
|
|
3456
3760
|
while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
|
|
3457
3761
|
if (total > maxTokens)
|
|
3458
|
-
|
|
3762
|
+
log4.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
|
|
3459
3763
|
return [...head, ...messages.slice(from)];
|
|
3460
3764
|
}
|
|
3461
3765
|
function compact(m, max, focus) {
|
|
@@ -4116,7 +4420,7 @@ init_tools_web();
|
|
|
4116
4420
|
init_tools();
|
|
4117
4421
|
init_tools_structured();
|
|
4118
4422
|
init_logging();
|
|
4119
|
-
var
|
|
4423
|
+
var log6 = forComponent("scratch");
|
|
4120
4424
|
var SCRATCH_DIR = "/scratch";
|
|
4121
4425
|
function shortArgs(args) {
|
|
4122
4426
|
try {
|
|
@@ -4156,7 +4460,7 @@ var Scratch = class {
|
|
|
4156
4460
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
4157
4461
|
await this.fs.writeFile(path, header + raw);
|
|
4158
4462
|
} catch (e) {
|
|
4159
|
-
|
|
4463
|
+
log6.debug("scratch write failed; returning raw", e);
|
|
4160
4464
|
return raw;
|
|
4161
4465
|
}
|
|
4162
4466
|
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
@@ -4186,7 +4490,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
|
|
|
4186
4490
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
4187
4491
|
await this.fs.writeFile(path, header + full);
|
|
4188
4492
|
} catch (e) {
|
|
4189
|
-
|
|
4493
|
+
log6.debug("scratch spill failed; cropping lossy", e);
|
|
4190
4494
|
return full.slice(0, pageBytes) + `
|
|
4191
4495
|
|
|
4192
4496
|
[output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
|
|
@@ -4232,7 +4536,7 @@ Question: ${q}`);
|
|
|
4232
4536
|
const answer = (res.text ?? "").trim();
|
|
4233
4537
|
return answer || "(no answer found in scratch)";
|
|
4234
4538
|
} catch (e) {
|
|
4235
|
-
|
|
4539
|
+
log6.debug("Ask peek failed", e);
|
|
4236
4540
|
return `Error querying scratch: ${e?.message ?? e}`;
|
|
4237
4541
|
}
|
|
4238
4542
|
}
|
|
@@ -4241,7 +4545,7 @@ Question: ${q}`);
|
|
|
4241
4545
|
|
|
4242
4546
|
// src/lessons.ts
|
|
4243
4547
|
init_logging();
|
|
4244
|
-
var
|
|
4548
|
+
var log7 = forComponent("Lessons");
|
|
4245
4549
|
var LessonOptionsDefaults = class {
|
|
4246
4550
|
minRepeats = 2;
|
|
4247
4551
|
};
|
|
@@ -4266,15 +4570,15 @@ function lessonCapture(options) {
|
|
|
4266
4570
|
counts.set(lesson.slug, n);
|
|
4267
4571
|
if (n < o.minRepeats) return;
|
|
4268
4572
|
written.add(lesson.slug);
|
|
4269
|
-
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) =>
|
|
4270
|
-
|
|
4573
|
+
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log7.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
|
|
4574
|
+
log7.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
|
|
4271
4575
|
}
|
|
4272
4576
|
};
|
|
4273
4577
|
}
|
|
4274
4578
|
|
|
4275
4579
|
// src/reflect.ts
|
|
4276
4580
|
init_logging();
|
|
4277
|
-
var
|
|
4581
|
+
var log8 = forComponent("Reflect");
|
|
4278
4582
|
async function reflectOnRun(o) {
|
|
4279
4583
|
const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
|
|
4280
4584
|
if (!digest.trim()) return null;
|
|
@@ -4290,7 +4594,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
4290
4594
|
const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
|
|
4291
4595
|
text = r?.content ?? "";
|
|
4292
4596
|
} catch (e) {
|
|
4293
|
-
|
|
4597
|
+
log8.warn(`reflection call failed: ${e?.message ?? e}`);
|
|
4294
4598
|
return null;
|
|
4295
4599
|
}
|
|
4296
4600
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
@@ -4300,10 +4604,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
4300
4604
|
try {
|
|
4301
4605
|
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
4302
4606
|
} catch (e) {
|
|
4303
|
-
|
|
4607
|
+
log8.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
4304
4608
|
return null;
|
|
4305
4609
|
}
|
|
4306
|
-
|
|
4610
|
+
log8.debug(`reflection persisted ${slug2}`);
|
|
4307
4611
|
return slug2;
|
|
4308
4612
|
}
|
|
4309
4613
|
function digestRun(messages, maxChars) {
|
|
@@ -4320,7 +4624,7 @@ function digestRun(messages, maxChars) {
|
|
|
4320
4624
|
// src/duplex.ts
|
|
4321
4625
|
import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
|
|
4322
4626
|
init_logging();
|
|
4323
|
-
var
|
|
4627
|
+
var log9 = forComponent("DuplexAgent");
|
|
4324
4628
|
function describeCall(call) {
|
|
4325
4629
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
4326
4630
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -4460,7 +4764,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4460
4764
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER);
|
|
4461
4765
|
if (m) {
|
|
4462
4766
|
this.fabricationCut = true;
|
|
4463
|
-
|
|
4767
|
+
log9.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
4464
4768
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
4465
4769
|
if (!safe) return;
|
|
4466
4770
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -4539,7 +4843,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4539
4843
|
try {
|
|
4540
4844
|
await this.voice.send("[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools.");
|
|
4541
4845
|
} catch (e) {
|
|
4542
|
-
|
|
4846
|
+
log9.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
4543
4847
|
} finally {
|
|
4544
4848
|
this.nudging = false;
|
|
4545
4849
|
}
|
|
@@ -4687,7 +4991,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
4687
4991
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
4688
4992
|
const cres = await new Agent(agentOpts).run(checkBrief);
|
|
4689
4993
|
if (cres.finishReason !== "stop") {
|
|
4690
|
-
|
|
4994
|
+
log9.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
4691
4995
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
4692
4996
|
}
|
|
4693
4997
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -4790,7 +5094,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
4790
5094
|
}
|
|
4791
5095
|
rec.status = "done";
|
|
4792
5096
|
rec.result = res.text;
|
|
4793
|
-
|
|
5097
|
+
log9.verbose(`task ${id} done (${res.steps} steps)`);
|
|
4794
5098
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
4795
5099
|
id,
|
|
4796
5100
|
text: res.text,
|
|
@@ -4808,7 +5112,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
4808
5112
|
this.dropAsk(rec.id);
|
|
4809
5113
|
rec.status = "error";
|
|
4810
5114
|
rec.result = msg;
|
|
4811
|
-
|
|
5115
|
+
log9.warn(`task ${rec.id} failed: ${msg}`);
|
|
4812
5116
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
4813
5117
|
this.queueRevoice(`[task ${rec.id} failed] ${msg}`);
|
|
4814
5118
|
}
|
|
@@ -5168,7 +5472,7 @@ init_logging();
|
|
|
5168
5472
|
|
|
5169
5473
|
// src/voice/engine.ts
|
|
5170
5474
|
init_logging();
|
|
5171
|
-
var
|
|
5475
|
+
var log10 = forComponent("VoiceEngine");
|
|
5172
5476
|
var now = () => performance.now();
|
|
5173
5477
|
var forSpeech = (t) => t.replace(/[*_`#]+/g, "").replace(/^[ \t]*[-•]\s+/gm, "").replace(/\s*[\u2013\u2014]\s*/g, ", ").replace(/[\u2010\u2011]/g, "-").replace(/\s*\|\s*/g, ", ").replace(/(\d)\s+%/g, "$1%").replace(/\.{3,}/g, ".");
|
|
5174
5478
|
var VoiceEngineOptions = class {
|
|
@@ -5276,7 +5580,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5276
5580
|
this.stt.onLevel = (rms) => this.handleLevel(rms);
|
|
5277
5581
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
5278
5582
|
this.setState("listening");
|
|
5279
|
-
|
|
5583
|
+
log10.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
5280
5584
|
}
|
|
5281
5585
|
get usingAec() {
|
|
5282
5586
|
return this.stt.usingAec;
|
|
@@ -5328,7 +5632,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5328
5632
|
this.reply += text;
|
|
5329
5633
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
5330
5634
|
this.tts.speak(forSpeech(text), true);
|
|
5331
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
5635
|
+
if (!this.spokeDeltas && this.turnStartAt) log10.debug(`ttft: ${Math.round(now() - this.turnStartAt)}ms`);
|
|
5332
5636
|
this.spokeDeltas = true;
|
|
5333
5637
|
this.setState("speaking");
|
|
5334
5638
|
}
|
|
@@ -5349,7 +5653,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5349
5653
|
}
|
|
5350
5654
|
this.drainTimer = null;
|
|
5351
5655
|
this.speaking = false;
|
|
5352
|
-
if (this.turnStartAt)
|
|
5656
|
+
if (this.turnStartAt) log10.debug(`turn: ${Math.round(now() - this.turnStartAt)}ms (incl. playback)`);
|
|
5353
5657
|
this.echoUntil = now() + 2500;
|
|
5354
5658
|
if (!this.usingAec) this.stt.reset();
|
|
5355
5659
|
this.setState("listening");
|
|
@@ -5490,7 +5794,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5490
5794
|
this.pendingUtt = this.pendingUtt ? `${this.pendingUtt} ${text}` : text;
|
|
5491
5795
|
if (this.pendingTimer) clearTimeout(this.pendingTimer);
|
|
5492
5796
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
5493
|
-
|
|
5797
|
+
log10.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
5494
5798
|
this.options.onHold();
|
|
5495
5799
|
if (this.options.holdFiller && !this.speaking) {
|
|
5496
5800
|
this.beginSpeech();
|
|
@@ -5588,7 +5892,7 @@ async function resolveAuth(auth) {
|
|
|
5588
5892
|
}
|
|
5589
5893
|
|
|
5590
5894
|
// src/voice/soniox.ts
|
|
5591
|
-
var
|
|
5895
|
+
var log11 = forComponent("SonioxSTT");
|
|
5592
5896
|
var now2 = () => performance.now();
|
|
5593
5897
|
var SonioxSTTOptions = class {
|
|
5594
5898
|
auth = "";
|
|
@@ -5645,9 +5949,9 @@ var SonioxSTT = class {
|
|
|
5645
5949
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
5646
5950
|
this.ws.onclose = (ev) => {
|
|
5647
5951
|
if (this.stopped) return;
|
|
5648
|
-
|
|
5952
|
+
log11.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
5649
5953
|
this.reset();
|
|
5650
|
-
this.connectWs().catch((e) =>
|
|
5954
|
+
this.connectWs().catch((e) => log11.error(`soniox reconnect failed: ${e.message}`));
|
|
5651
5955
|
};
|
|
5652
5956
|
}
|
|
5653
5957
|
async start() {
|
|
@@ -5657,7 +5961,7 @@ var SonioxSTT = class {
|
|
|
5657
5961
|
this.endpointTimer = setInterval(() => {
|
|
5658
5962
|
const combined = (this.finalText + this.partialText).trim();
|
|
5659
5963
|
if (!combined || now2() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
5660
|
-
if (this.firstTokenAt)
|
|
5964
|
+
if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
5661
5965
|
this.reset();
|
|
5662
5966
|
this.onUtterance(combined, now2());
|
|
5663
5967
|
}, 120);
|
|
@@ -5674,7 +5978,7 @@ var SonioxSTT = class {
|
|
|
5674
5978
|
});
|
|
5675
5979
|
}
|
|
5676
5980
|
handle(m) {
|
|
5677
|
-
if (m.error_message) return
|
|
5981
|
+
if (m.error_message) return log11.error(`soniox: ${m.error_message}`);
|
|
5678
5982
|
let endpoint = false;
|
|
5679
5983
|
for (const t of m.tokens ?? []) {
|
|
5680
5984
|
if (t.text === "<end>") endpoint = true;
|
|
@@ -5690,7 +5994,7 @@ var SonioxSTT = class {
|
|
|
5690
5994
|
this.onPartial(combined);
|
|
5691
5995
|
if (endpoint && this.finalText.trim()) {
|
|
5692
5996
|
const utterance = this.finalText.trim();
|
|
5693
|
-
if (this.firstTokenAt)
|
|
5997
|
+
if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
5694
5998
|
this.reset();
|
|
5695
5999
|
this.onUtterance(utterance, now2());
|
|
5696
6000
|
}
|
|
@@ -5712,7 +6016,7 @@ var SonioxSTT = class {
|
|
|
5712
6016
|
|
|
5713
6017
|
// src/voice/cartesia.ts
|
|
5714
6018
|
init_logging();
|
|
5715
|
-
var
|
|
6019
|
+
var log12 = forComponent("CartesiaTTS");
|
|
5716
6020
|
var now3 = () => performance.now();
|
|
5717
6021
|
var CartesiaTTSOptions = class {
|
|
5718
6022
|
auth = "";
|
|
@@ -5762,9 +6066,9 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
5762
6066
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
5763
6067
|
});
|
|
5764
6068
|
this.ws.onclose = (ev) => {
|
|
5765
|
-
|
|
6069
|
+
log12.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
5766
6070
|
if (!this.closed) {
|
|
5767
|
-
this.connecting = this.doConnect().catch((e) =>
|
|
6071
|
+
this.connecting = this.doConnect().catch((e) => log12.error(`cartesia reconnect failed: ${e.message}`));
|
|
5768
6072
|
}
|
|
5769
6073
|
};
|
|
5770
6074
|
this.ws.onmessage = (ev) => {
|
|
@@ -5786,11 +6090,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
5786
6090
|
this.down = true;
|
|
5787
6091
|
this.downAt = now3();
|
|
5788
6092
|
this.consecutiveOk = 0;
|
|
5789
|
-
|
|
6093
|
+
log12.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
5790
6094
|
this.onDone();
|
|
5791
6095
|
this.startProbe();
|
|
5792
6096
|
} else if (!this.down) {
|
|
5793
|
-
|
|
6097
|
+
log12.warn(`cartesia: ${JSON.stringify(m)}`);
|
|
5794
6098
|
}
|
|
5795
6099
|
}
|
|
5796
6100
|
};
|
|
@@ -5804,7 +6108,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
5804
6108
|
this.consecutiveOk = 0;
|
|
5805
6109
|
this.stopProbe();
|
|
5806
6110
|
const downMs = this.downAt ? now3() - this.downAt : 0;
|
|
5807
|
-
(downMs < 2e3 ?
|
|
6111
|
+
(downMs < 2e3 ? log12.debug : log12.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
5808
6112
|
}
|
|
5809
6113
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
5810
6114
|
async ensureConnected() {
|
|
@@ -5876,155 +6180,6 @@ function base64ToBytes(b64) {
|
|
|
5876
6180
|
return out;
|
|
5877
6181
|
}
|
|
5878
6182
|
|
|
5879
|
-
// src/worktree.ts
|
|
5880
|
-
import { spawnSync } from "child_process";
|
|
5881
|
-
import { symlinkSync, mkdirSync, statSync, readFileSync, rmSync } from "fs";
|
|
5882
|
-
import { join as join2 } from "path";
|
|
5883
|
-
var VALID_SEGMENT = /^[a-zA-Z0-9._-]+$/;
|
|
5884
|
-
var MAX_SLUG_LENGTH = 64;
|
|
5885
|
-
var GIT_NO_PROMPT_ENV = { GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "" };
|
|
5886
|
-
function validateWorktreeSlug(slug2) {
|
|
5887
|
-
if (!slug2) throw new Error("worktree slug must not be empty");
|
|
5888
|
-
if (slug2.length > MAX_SLUG_LENGTH) throw new Error(`worktree slug must be \u2264${MAX_SLUG_LENGTH} chars (got ${slug2.length})`);
|
|
5889
|
-
for (const seg of slug2.split("/")) {
|
|
5890
|
-
if (seg === "." || seg === "..") throw new Error(`worktree slug must not contain "." or ".." segments`);
|
|
5891
|
-
if (!VALID_SEGMENT.test(seg)) throw new Error(`worktree slug segment "${seg}" contains invalid characters (allowed: a-z A-Z 0-9 . _ -)`);
|
|
5892
|
-
}
|
|
5893
|
-
}
|
|
5894
|
-
function flattenSlug(slug2) {
|
|
5895
|
-
return slug2.replaceAll("/", "+");
|
|
5896
|
-
}
|
|
5897
|
-
function worktreeBranchName(slug2) {
|
|
5898
|
-
return `worktree-${flattenSlug(slug2)}`;
|
|
5899
|
-
}
|
|
5900
|
-
function worktreesDir(repoRoot) {
|
|
5901
|
-
return join2(repoRoot, ".claude", "worktrees");
|
|
5902
|
-
}
|
|
5903
|
-
function worktreePathFor(repoRoot, slug2) {
|
|
5904
|
-
return join2(worktreesDir(repoRoot), flattenSlug(slug2));
|
|
5905
|
-
}
|
|
5906
|
-
function git(args, cwd) {
|
|
5907
|
-
const r = spawnSync("git", args, {
|
|
5908
|
-
cwd,
|
|
5909
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
5910
|
-
env: { ...process.env, ...GIT_NO_PROMPT_ENV }
|
|
5911
|
-
});
|
|
5912
|
-
return { ok: r.status === 0, stdout: (r.stdout ?? "").toString().trim(), stderr: (r.stderr ?? "").toString().trim() };
|
|
5913
|
-
}
|
|
5914
|
-
function findGitRoot(from) {
|
|
5915
|
-
const r = git(["rev-parse", "--show-toplevel"], from);
|
|
5916
|
-
return r.ok ? r.stdout : null;
|
|
5917
|
-
}
|
|
5918
|
-
function readWorktreeHead(worktreePath) {
|
|
5919
|
-
try {
|
|
5920
|
-
const gitFile = readFileSync(join2(worktreePath, ".git"), "utf-8").trim();
|
|
5921
|
-
const m = gitFile.match(/^gitdir:\s*(.+)$/);
|
|
5922
|
-
if (!m) return null;
|
|
5923
|
-
const headPath = join2(m[1], "HEAD");
|
|
5924
|
-
const head = readFileSync(headPath, "utf-8").trim();
|
|
5925
|
-
const refMatch = head.match(/^ref:\s*(.+)$/);
|
|
5926
|
-
if (!refMatch) return head.length === 40 ? head : null;
|
|
5927
|
-
const refPath = join2(m[1], "..", refMatch[1]);
|
|
5928
|
-
try {
|
|
5929
|
-
return readFileSync(refPath, "utf-8").trim();
|
|
5930
|
-
} catch {
|
|
5931
|
-
}
|
|
5932
|
-
const r = git(["rev-parse", "HEAD"], worktreePath);
|
|
5933
|
-
return r.ok ? r.stdout : null;
|
|
5934
|
-
} catch {
|
|
5935
|
-
return null;
|
|
5936
|
-
}
|
|
5937
|
-
}
|
|
5938
|
-
function getOrCreateWorktree(repoRoot, slug2) {
|
|
5939
|
-
validateWorktreeSlug(slug2);
|
|
5940
|
-
const worktreePath = worktreePathFor(repoRoot, slug2);
|
|
5941
|
-
const branch = worktreeBranchName(slug2);
|
|
5942
|
-
const existingHead = readWorktreeHead(worktreePath);
|
|
5943
|
-
if (existingHead) return { worktreePath, branch, headCommit: existingHead, existed: true };
|
|
5944
|
-
mkdirSync(worktreesDir(repoRoot), { recursive: true });
|
|
5945
|
-
const defaultBranch = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoRoot);
|
|
5946
|
-
let baseBranch = defaultBranch.ok ? defaultBranch.stdout.replace(/^origin\//, "") : "HEAD";
|
|
5947
|
-
const originRef = `origin/${baseBranch}`;
|
|
5948
|
-
const hasOrigin = git(["rev-parse", "--verify", originRef], repoRoot);
|
|
5949
|
-
if (!hasOrigin.ok) {
|
|
5950
|
-
const fetched = git(["fetch", "origin", baseBranch], repoRoot);
|
|
5951
|
-
if (!fetched.ok) baseBranch = "HEAD";
|
|
5952
|
-
}
|
|
5953
|
-
const base = baseBranch === "HEAD" ? "HEAD" : originRef;
|
|
5954
|
-
const r = git(["worktree", "add", "-B", branch, worktreePath, base], repoRoot);
|
|
5955
|
-
if (!r.ok) throw new Error(`failed to create worktree: ${r.stderr}`);
|
|
5956
|
-
const headSha = git(["rev-parse", "HEAD"], worktreePath);
|
|
5957
|
-
symlinkDirs(repoRoot, worktreePath, ["node_modules", ".bun"]);
|
|
5958
|
-
return { worktreePath, branch, headCommit: headSha.stdout, existed: false, baseBranch };
|
|
5959
|
-
}
|
|
5960
|
-
function symlinkDirs(repoRoot, worktreePath, dirs) {
|
|
5961
|
-
for (const dir of dirs) {
|
|
5962
|
-
const src = join2(repoRoot, dir);
|
|
5963
|
-
const dst = join2(worktreePath, dir);
|
|
5964
|
-
try {
|
|
5965
|
-
statSync(src);
|
|
5966
|
-
} catch {
|
|
5967
|
-
continue;
|
|
5968
|
-
}
|
|
5969
|
-
try {
|
|
5970
|
-
symlinkSync(src, dst, "dir");
|
|
5971
|
-
} catch (e) {
|
|
5972
|
-
if (e?.code !== "EEXIST") console.error(`[worktree] symlink ${dir}: ${e?.message ?? e}`);
|
|
5973
|
-
}
|
|
5974
|
-
}
|
|
5975
|
-
}
|
|
5976
|
-
function cleanupWorktree(repoRoot, slug2, opts) {
|
|
5977
|
-
validateWorktreeSlug(slug2);
|
|
5978
|
-
const worktreePath = worktreePathFor(repoRoot, slug2);
|
|
5979
|
-
const branch = worktreeBranchName(slug2);
|
|
5980
|
-
try {
|
|
5981
|
-
statSync(worktreePath);
|
|
5982
|
-
} catch {
|
|
5983
|
-
return { removed: false, reason: "not found" };
|
|
5984
|
-
}
|
|
5985
|
-
if (!opts?.force) {
|
|
5986
|
-
const status = git(["status", "--porcelain", "-uno"], worktreePath);
|
|
5987
|
-
if (status.ok && status.stdout) return { removed: false, reason: "uncommitted changes" };
|
|
5988
|
-
}
|
|
5989
|
-
for (const dir of ["node_modules", ".bun"]) {
|
|
5990
|
-
try {
|
|
5991
|
-
rmSync(join2(worktreePath, dir));
|
|
5992
|
-
} catch {
|
|
5993
|
-
}
|
|
5994
|
-
}
|
|
5995
|
-
const rm = git(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
5996
|
-
if (!rm.ok) return { removed: false, reason: rm.stderr };
|
|
5997
|
-
git(["branch", "-D", branch], repoRoot);
|
|
5998
|
-
return { removed: true };
|
|
5999
|
-
}
|
|
6000
|
-
var _session = null;
|
|
6001
|
-
function getWorktreeSession() {
|
|
6002
|
-
return _session;
|
|
6003
|
-
}
|
|
6004
|
-
function enterWorktree(slug2, cwd) {
|
|
6005
|
-
const repoRoot = findGitRoot(cwd);
|
|
6006
|
-
if (!repoRoot) throw new Error("--worktree requires a git repository");
|
|
6007
|
-
const info = getOrCreateWorktree(repoRoot, slug2);
|
|
6008
|
-
process.chdir(info.worktreePath);
|
|
6009
|
-
_session = {
|
|
6010
|
-
originalCwd: cwd,
|
|
6011
|
-
worktreePath: info.worktreePath,
|
|
6012
|
-
slug: slug2,
|
|
6013
|
-
branch: info.branch,
|
|
6014
|
-
headCommit: info.headCommit
|
|
6015
|
-
};
|
|
6016
|
-
return _session;
|
|
6017
|
-
}
|
|
6018
|
-
function exitWorktree() {
|
|
6019
|
-
if (!_session) return null;
|
|
6020
|
-
const repoRoot = findGitRoot(_session.originalCwd);
|
|
6021
|
-
if (!repoRoot) return null;
|
|
6022
|
-
process.chdir(_session.originalCwd);
|
|
6023
|
-
const result = cleanupWorktree(repoRoot, _session.slug);
|
|
6024
|
-
_session = null;
|
|
6025
|
-
return result;
|
|
6026
|
-
}
|
|
6027
|
-
|
|
6028
6183
|
// src/index.ts
|
|
6029
6184
|
import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
|
|
6030
6185
|
export {
|