@livx.cc/agentx 0.99.47 → 0.99.48
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 +2 -1
- package/dist/{Agent-BWbQ4Sqp.d.ts → Agent-BeoEFl2T.d.ts} +17 -12
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +888 -428
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +696 -315
- package/dist/index.js.map +1 -1
- package/dist/{mcp-kgG6rtsd.d.ts → mcp-DjYZzGGW.d.ts} +13 -4
- package/dist/mcp.client.d.ts +33 -7
- package/dist/mcp.client.js +147 -40
- package/dist/mcp.client.js.map +1 -1
- package/dist/{tools-BqL8Lk4J.d.ts → tools-MNdqlJIa.d.ts} +180 -40
- package/dist/tools.shell.d.ts +40 -11
- package/dist/tools.shell.js +452 -91
- package/dist/tools.shell.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1509,6 +1509,394 @@ var init_tools = __esm({
|
|
|
1509
1509
|
}
|
|
1510
1510
|
});
|
|
1511
1511
|
|
|
1512
|
+
// src/tools.jobs.ts
|
|
1513
|
+
function formatJobNotice(n) {
|
|
1514
|
+
return `[job ${n.id} ${n.status}${n.reason ? `: ${n.reason}` : ""}] ${n.kind}${n.label ? ` \`${n.label}\`` : ""}
|
|
1515
|
+
` + (n.tail ? `${n.tail.replace(/\n+$/, "")}
|
|
1516
|
+
` : "(no output)\n") + `Read the full output with JobOutput({id:"${n.id}"}).`;
|
|
1517
|
+
}
|
|
1518
|
+
function makeJobTools(registry, opts = {}) {
|
|
1519
|
+
const maxWaitMs = opts.maxWaitMs ?? 12e4;
|
|
1520
|
+
return [
|
|
1521
|
+
{
|
|
1522
|
+
name: "JobOutput",
|
|
1523
|
+
description: "Read a job's output by id. Without `offset`: the buffered tail. With `offset` (the `next` value from a previous read): only what arrived since.",
|
|
1524
|
+
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" }, offset: { type: "number", description: "return output after this position (from a previous read's `next`)" } } },
|
|
1525
|
+
async run({ id, offset }) {
|
|
1526
|
+
const r = registry.read(String(id), Number(offset) || 0);
|
|
1527
|
+
if (!r) return NO_JOB(String(id));
|
|
1528
|
+
const st = registry.status(String(id));
|
|
1529
|
+
return `[${st.status}${st.reason ? `: ${st.reason}` : ""}] next=${r.next}${r.truncated ? " (older output dropped)" : ""}
|
|
1530
|
+
${r.text.replace(/\n+$/, "") || "(no new output)"}`;
|
|
1531
|
+
}
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
name: "JobStatus",
|
|
1535
|
+
description: "Status of a job (running/done/error/killed, age, last activity). Omit `id` to list all jobs.",
|
|
1536
|
+
parameters: { type: "object", properties: { id: { type: "string" } } },
|
|
1537
|
+
async run({ id }) {
|
|
1538
|
+
const now4 = registry.options.now();
|
|
1539
|
+
if (!id) {
|
|
1540
|
+
const all = registry.list();
|
|
1541
|
+
return all.length ? all.map((j2) => describe(j2, now4)).join("\n") : "(no background jobs)";
|
|
1542
|
+
}
|
|
1543
|
+
const j = registry.get(String(id));
|
|
1544
|
+
return j ? describe(j, now4) : NO_JOB(String(id));
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
name: "JobKill",
|
|
1549
|
+
description: "Stop a running job by id (sandbox jobs are cancelled without committing; processes get SIGTERM; MCP calls are cancelled).",
|
|
1550
|
+
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
1551
|
+
async run({ id }) {
|
|
1552
|
+
return registry.kill(String(id), "killed by JobKill") ? `Killed job ${id}.` : NO_JOB(String(id));
|
|
1553
|
+
}
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
name: "JobWait",
|
|
1557
|
+
description: `Block until a job finishes or \`timeoutMs\` passes (max ${maxWaitMs}ms), then return its status and output tail. Prefer this over sleeping.`,
|
|
1558
|
+
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" }, timeoutMs: { type: "number" } } },
|
|
1559
|
+
async run({ id, timeoutMs }) {
|
|
1560
|
+
const j = await registry.wait(String(id), Math.min(Number(timeoutMs) || maxWaitMs, maxWaitMs));
|
|
1561
|
+
if (!j) return NO_JOB(String(id));
|
|
1562
|
+
const tail = (registry.output(j.id) ?? "").slice(-4e3).replace(/\n+$/, "");
|
|
1563
|
+
return `${describe(j, registry.options.now())}
|
|
1564
|
+
${tail || "(no output)"}`;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
];
|
|
1568
|
+
}
|
|
1569
|
+
var log5, JobRegistryOptions, secs, JobRegistry, NO_JOB, describe;
|
|
1570
|
+
var init_tools_jobs = __esm({
|
|
1571
|
+
"src/tools.jobs.ts"() {
|
|
1572
|
+
"use strict";
|
|
1573
|
+
init_logging();
|
|
1574
|
+
log5 = forComponent("jobs");
|
|
1575
|
+
JobRegistryOptions = class {
|
|
1576
|
+
/** Tail buffer cap per job (chars). */
|
|
1577
|
+
maxBuffer = 256 * 1024;
|
|
1578
|
+
/** Idle window per kind: no output/progress/CPU for this long ⇒ stuck. Kinds without an activity channel
|
|
1579
|
+
* (host, native) are absent on purpose — silence is all they can ever show, so it proves nothing; their
|
|
1580
|
+
* bound is the hard cap. Explicit `background:true` shell jobs opt out per job (a quiet server is healthy). */
|
|
1581
|
+
idleMs = { shell: 10 * 6e4, mcp: 10 * 6e4 };
|
|
1582
|
+
/** Absolute ceiling per kind (0/absent = none). */
|
|
1583
|
+
hardCapMs = { mcp: 30 * 6e4 };
|
|
1584
|
+
/** Soft foreground timeout per kind: past it the caller stops waiting and gets a job id instead. */
|
|
1585
|
+
foregroundMs = { mcp: 3e4 };
|
|
1586
|
+
/** How often the reaper runs while reapable jobs exist. */
|
|
1587
|
+
sweepMs = 15e3;
|
|
1588
|
+
/** Grace between the kill signal and a forced kill (producers that escalate read this). */
|
|
1589
|
+
killGraceMs = 5e3;
|
|
1590
|
+
/** Finished jobs kept for inspection; older finished ones are pruned (every delegated tool call is a job). */
|
|
1591
|
+
keepFinished = 200;
|
|
1592
|
+
/** Clock (injectable for tests). Late-bound so a patched Date.now is honoured. */
|
|
1593
|
+
now = () => Date.now();
|
|
1594
|
+
/** Terminal-state notification for jobs with `notify` (the Agent wires this to `inject`). */
|
|
1595
|
+
onExit;
|
|
1596
|
+
};
|
|
1597
|
+
secs = (ms) => `${Math.round(ms / 1e3)}s`;
|
|
1598
|
+
JobRegistry = class {
|
|
1599
|
+
options;
|
|
1600
|
+
jobs = /* @__PURE__ */ new Map();
|
|
1601
|
+
seq = 0;
|
|
1602
|
+
timer;
|
|
1603
|
+
constructor(options) {
|
|
1604
|
+
this.options = { ...new JobRegistryOptions(), ...options };
|
|
1605
|
+
}
|
|
1606
|
+
/** Register externally-driven work (a process, a request, a native tool) and get a handle to feed it. */
|
|
1607
|
+
track(opts = {}) {
|
|
1608
|
+
const o = this.options;
|
|
1609
|
+
const kind = opts.kind ?? "task";
|
|
1610
|
+
const now4 = o.now();
|
|
1611
|
+
let settle;
|
|
1612
|
+
const done = new Promise((r) => {
|
|
1613
|
+
settle = r;
|
|
1614
|
+
});
|
|
1615
|
+
const job = {
|
|
1616
|
+
id: `job-${++this.seq}`,
|
|
1617
|
+
kind,
|
|
1618
|
+
label: opts.label ?? "",
|
|
1619
|
+
status: "running",
|
|
1620
|
+
startedAt: now4,
|
|
1621
|
+
lastActivityAt: now4,
|
|
1622
|
+
bytes: 0,
|
|
1623
|
+
holds: !!opts.holds,
|
|
1624
|
+
buf: "",
|
|
1625
|
+
dropped: 0,
|
|
1626
|
+
idleMs: opts.idleMs ?? o.idleMs[kind] ?? 0,
|
|
1627
|
+
hardCapMs: opts.hardCapMs ?? o.hardCapMs[kind] ?? 0,
|
|
1628
|
+
drain: !!opts.drain,
|
|
1629
|
+
notify: !!opts.notify,
|
|
1630
|
+
idleAfterActivity: !!opts.idleAfterActivity,
|
|
1631
|
+
active: false,
|
|
1632
|
+
cpuTime: opts.cpuTime,
|
|
1633
|
+
onKill: opts.onKill,
|
|
1634
|
+
onExit: opts.onExit,
|
|
1635
|
+
done,
|
|
1636
|
+
settle
|
|
1637
|
+
};
|
|
1638
|
+
this.jobs.set(job.id, job);
|
|
1639
|
+
if (opts.seed) this.append(job, opts.seed);
|
|
1640
|
+
this.arm();
|
|
1641
|
+
return {
|
|
1642
|
+
id: job.id,
|
|
1643
|
+
chunk: (s) => {
|
|
1644
|
+
if (s) {
|
|
1645
|
+
this.append(job, s);
|
|
1646
|
+
job.lastActivityAt = o.now();
|
|
1647
|
+
job.active = true;
|
|
1648
|
+
}
|
|
1649
|
+
},
|
|
1650
|
+
touch: () => {
|
|
1651
|
+
job.lastActivityAt = o.now();
|
|
1652
|
+
job.active = true;
|
|
1653
|
+
},
|
|
1654
|
+
finish: (status, result, reason) => this.end(job, status, result, reason),
|
|
1655
|
+
detach: () => {
|
|
1656
|
+
job.holds = false;
|
|
1657
|
+
job.notify = true;
|
|
1658
|
+
job.drain = true;
|
|
1659
|
+
},
|
|
1660
|
+
get status() {
|
|
1661
|
+
return job.status;
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
/** Start `fn` in the background and return a job id immediately (sandbox/subagent semantics: drained at turn end). */
|
|
1666
|
+
start(fn, opts = {}) {
|
|
1667
|
+
return this.run(fn, { drain: true, ...opts }).handle.id;
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Run `fn` in the FOREGROUND with a soft timeout: resolves `{done:true,value}` if it settles in time, else
|
|
1671
|
+
* `{done:false,id}` — the work keeps going as a detached job whose completion is reported via `onExit`.
|
|
1672
|
+
* A rejection before the timeout is rethrown (the caller's normal error path). `softTimeoutMs` ≤ 0 waits forever.
|
|
1673
|
+
*/
|
|
1674
|
+
async runForeground(fn, opts = {}) {
|
|
1675
|
+
const { handle, promise } = this.run(fn, opts);
|
|
1676
|
+
const soft = opts.softTimeoutMs ?? this.options.foregroundMs[opts.kind ?? "task"] ?? 0;
|
|
1677
|
+
let t;
|
|
1678
|
+
const late = soft > 0 ? new Promise((r) => {
|
|
1679
|
+
t = setTimeout(() => r("late"), soft);
|
|
1680
|
+
}) : new Promise(() => {
|
|
1681
|
+
});
|
|
1682
|
+
try {
|
|
1683
|
+
const r = await Promise.race([promise.then((v) => ({ v }), (e) => ({ e })), late]);
|
|
1684
|
+
if (r !== "late" && handle.status !== "running") this.jobs.delete(handle.id);
|
|
1685
|
+
if (r === "late") {
|
|
1686
|
+
if (handle.status === "running") {
|
|
1687
|
+
handle.detach();
|
|
1688
|
+
return { done: false, id: handle.id };
|
|
1689
|
+
}
|
|
1690
|
+
const settled = await promise.then((v) => ({ v }), (e) => ({ e }));
|
|
1691
|
+
if ("e" in settled) throw settled.e;
|
|
1692
|
+
return { done: true, value: settled.v };
|
|
1693
|
+
}
|
|
1694
|
+
if ("e" in r) throw r.e;
|
|
1695
|
+
return { done: true, value: r.v };
|
|
1696
|
+
} finally {
|
|
1697
|
+
if (t) clearTimeout(t);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
run(fn, opts) {
|
|
1701
|
+
const ctl = new AbortController();
|
|
1702
|
+
const handle = this.track({ ...opts, onKill: (reason) => {
|
|
1703
|
+
ctl.abort(Object.assign(new Error(reason), { name: "AbortError", code: "job_killed" }));
|
|
1704
|
+
opts.onKill?.(reason);
|
|
1705
|
+
} });
|
|
1706
|
+
let promise;
|
|
1707
|
+
try {
|
|
1708
|
+
promise = fn({ signal: ctl.signal, onChunk: handle.chunk, touch: handle.touch });
|
|
1709
|
+
} catch (e) {
|
|
1710
|
+
promise = Promise.reject(e);
|
|
1711
|
+
}
|
|
1712
|
+
promise.then(
|
|
1713
|
+
(res) => {
|
|
1714
|
+
if (handle.status === "running" && typeof res === "string" && res) handle.chunk(res);
|
|
1715
|
+
handle.finish("done", res);
|
|
1716
|
+
},
|
|
1717
|
+
(e) => {
|
|
1718
|
+
if (handle.status === "running") handle.chunk(`
|
|
1719
|
+
[error] ${e?.message ?? e}`);
|
|
1720
|
+
handle.finish("error", void 0, e?.message ?? String(e));
|
|
1721
|
+
}
|
|
1722
|
+
);
|
|
1723
|
+
return { handle, promise };
|
|
1724
|
+
}
|
|
1725
|
+
append(job, s) {
|
|
1726
|
+
job.buf += s;
|
|
1727
|
+
const over = job.buf.length - this.options.maxBuffer;
|
|
1728
|
+
if (over > 0) {
|
|
1729
|
+
job.buf = job.buf.slice(over);
|
|
1730
|
+
job.dropped += over;
|
|
1731
|
+
}
|
|
1732
|
+
job.bytes = job.buf.length;
|
|
1733
|
+
}
|
|
1734
|
+
end(job, status, result, reason) {
|
|
1735
|
+
if (job.status !== "running") return;
|
|
1736
|
+
job.status = status;
|
|
1737
|
+
job.endedAt = this.options.now();
|
|
1738
|
+
job.result = result;
|
|
1739
|
+
if (reason && status !== "done") job.reason = reason;
|
|
1740
|
+
job.holds = false;
|
|
1741
|
+
job.settle();
|
|
1742
|
+
this.prune();
|
|
1743
|
+
const onExit = job.onExit ?? this.options.onExit;
|
|
1744
|
+
if (job.notify && onExit) {
|
|
1745
|
+
try {
|
|
1746
|
+
onExit({ id: job.id, kind: job.kind, label: job.label, status, reason: job.reason, tail: job.buf.slice(-4e3), result });
|
|
1747
|
+
} catch (e) {
|
|
1748
|
+
log5.warn(`job onExit handler threw for ${job.id}: ${e}`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
prune() {
|
|
1753
|
+
const finished = [...this.jobs.values()].filter((j) => j.status !== "running");
|
|
1754
|
+
for (const j of finished.slice(0, Math.max(0, finished.length - this.options.keepFinished))) this.jobs.delete(j.id);
|
|
1755
|
+
}
|
|
1756
|
+
/** Current tail output (null = no such job). */
|
|
1757
|
+
output(id) {
|
|
1758
|
+
return this.jobs.get(id)?.buf ?? null;
|
|
1759
|
+
}
|
|
1760
|
+
/** Incremental read: output from absolute char `offset` (clamped to what the ring still holds). */
|
|
1761
|
+
read(id, offset = 0) {
|
|
1762
|
+
const j = this.jobs.get(id);
|
|
1763
|
+
if (!j) return null;
|
|
1764
|
+
const start = Math.max(0, offset - j.dropped);
|
|
1765
|
+
return { text: j.buf.slice(start), next: j.dropped + j.buf.length, truncated: offset < j.dropped };
|
|
1766
|
+
}
|
|
1767
|
+
get(id) {
|
|
1768
|
+
const j = this.jobs.get(id);
|
|
1769
|
+
return j ? this.info(j) : null;
|
|
1770
|
+
}
|
|
1771
|
+
status(id) {
|
|
1772
|
+
const j = this.jobs.get(id);
|
|
1773
|
+
return j ? { status: j.status, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} } : null;
|
|
1774
|
+
}
|
|
1775
|
+
list() {
|
|
1776
|
+
return [...this.jobs.values()].map((j) => this.info(j));
|
|
1777
|
+
}
|
|
1778
|
+
info(j) {
|
|
1779
|
+
const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result } = j;
|
|
1780
|
+
return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result };
|
|
1781
|
+
}
|
|
1782
|
+
/** Wait (bounded) for a job to settle. Resolves the job's info either way; null for an unknown id. */
|
|
1783
|
+
async wait(id, timeoutMs) {
|
|
1784
|
+
const j = this.jobs.get(id);
|
|
1785
|
+
if (!j) return null;
|
|
1786
|
+
let t;
|
|
1787
|
+
await Promise.race([j.done, new Promise((r) => {
|
|
1788
|
+
t = setTimeout(r, Math.max(0, timeoutMs));
|
|
1789
|
+
})]);
|
|
1790
|
+
if (t) clearTimeout(t);
|
|
1791
|
+
return this.info(j);
|
|
1792
|
+
}
|
|
1793
|
+
/** Stop a job: runs its `onKill` (abort/SIGTERM) and marks it killed with `reason`. */
|
|
1794
|
+
kill(id, reason = "killed by request") {
|
|
1795
|
+
const j = this.jobs.get(id);
|
|
1796
|
+
if (!j) return false;
|
|
1797
|
+
if (j.status === "running") {
|
|
1798
|
+
this.end(j, "killed", void 0, reason);
|
|
1799
|
+
try {
|
|
1800
|
+
j.onKill?.(reason);
|
|
1801
|
+
} catch (e) {
|
|
1802
|
+
log5.warn(`onKill for ${id} threw: ${e}`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
return true;
|
|
1806
|
+
}
|
|
1807
|
+
/** Kill every running job — or, with `onlyDrainable`, only turn-scoped ones (sandbox/sub-agent/handed-off MCP),
|
|
1808
|
+
* leaving jobs meant to outlive a run (a background dev server) alone. */
|
|
1809
|
+
killAll(reason = "run ended", onlyDrainable = false) {
|
|
1810
|
+
for (const j of this.jobs.values()) if (!onlyDrainable || j.drain) this.kill(j.id, reason);
|
|
1811
|
+
}
|
|
1812
|
+
/** Jobs something is synchronously waiting on (see `JobTrackOptions.holds`) that are still running. */
|
|
1813
|
+
holding() {
|
|
1814
|
+
return [...this.jobs.values()].filter((j) => j.status === "running" && j.holds).map((j) => this.info(j));
|
|
1815
|
+
}
|
|
1816
|
+
/** Await every drainable running job (turn end). Returns the ids that were still running when drain began. */
|
|
1817
|
+
async drain(signal) {
|
|
1818
|
+
const pending = [...this.jobs.values()].filter((j) => j.status === "running" && j.drain);
|
|
1819
|
+
const all = Promise.all(pending.map((j) => j.done));
|
|
1820
|
+
await (signal ? Promise.race([all, new Promise((r) => signal.aborted ? r() : signal.addEventListener("abort", () => r(), { once: true }))]) : all);
|
|
1821
|
+
return pending.map((j) => j.id);
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* The stuck-job decision, synchronous part: kill every running job past its hard cap, or past its idle
|
|
1825
|
+
* window when it has no CPU probe to argue otherwise. Cheap — the stall watchdog calls it on every tick.
|
|
1826
|
+
* Returns the killed ids. Jobs WITH a probe are left to `sweep()`.
|
|
1827
|
+
*/
|
|
1828
|
+
reap() {
|
|
1829
|
+
const now4 = this.options.now();
|
|
1830
|
+
const killed = [];
|
|
1831
|
+
for (const j of this.jobs.values()) {
|
|
1832
|
+
if (j.status !== "running") continue;
|
|
1833
|
+
const verdict = this.verdict(j, now4);
|
|
1834
|
+
if (!verdict || verdict.idle && j.cpuTime) continue;
|
|
1835
|
+
this.stuck(j, verdict.reason, now4);
|
|
1836
|
+
killed.push(j.id);
|
|
1837
|
+
}
|
|
1838
|
+
return killed;
|
|
1839
|
+
}
|
|
1840
|
+
/** Full sweep: sample CPU probes (advancing CPU = activity), then `reap()` including probed jobs. */
|
|
1841
|
+
async sweep() {
|
|
1842
|
+
const now4 = this.options.now();
|
|
1843
|
+
for (const j of [...this.jobs.values()]) {
|
|
1844
|
+
if (j.status !== "running" || !j.cpuTime || !j.idleMs) continue;
|
|
1845
|
+
let cpu;
|
|
1846
|
+
try {
|
|
1847
|
+
cpu = await j.cpuTime();
|
|
1848
|
+
} catch (e) {
|
|
1849
|
+
log5.debug(`cpu probe for ${j.id} failed: ${e}`);
|
|
1850
|
+
}
|
|
1851
|
+
if (cpu != null && j.lastCpu != null && cpu > j.lastCpu) j.lastActivityAt = this.options.now();
|
|
1852
|
+
const prev = j.lastCpu;
|
|
1853
|
+
if (cpu != null) j.lastCpu = cpu;
|
|
1854
|
+
const verdict = this.verdict(j, now4);
|
|
1855
|
+
if (!verdict?.idle || j.status !== "running") continue;
|
|
1856
|
+
if (cpu == null) {
|
|
1857
|
+
log5.debug(`not reaping ${j.id}: ${verdict.reason} but CPU unknown`);
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
if (prev == null) continue;
|
|
1861
|
+
this.stuck(j, `${verdict.reason}, no CPU progress (${prev}s \u2192 ${cpu}s)`, now4);
|
|
1862
|
+
}
|
|
1863
|
+
return this.reap();
|
|
1864
|
+
}
|
|
1865
|
+
verdict(j, now4) {
|
|
1866
|
+
if (j.hardCapMs && now4 - j.startedAt >= j.hardCapMs) return { reason: `stuck: exceeded hard cap ${secs(j.hardCapMs)}`, idle: false };
|
|
1867
|
+
if (j.idleMs && (!j.idleAfterActivity || j.active) && now4 - j.lastActivityAt >= j.idleMs) return { reason: `stuck: idle ${secs(now4 - j.lastActivityAt)}`, idle: true };
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
stuck(j, reason, now4) {
|
|
1871
|
+
log5.warn(`reaping ${j.id} (${j.kind} "${j.label}"): ${reason} \u2014 age ${secs(now4 - j.startedAt)}, last activity ${secs(now4 - j.lastActivityAt)} ago, idleMs=${j.idleMs}, hardCapMs=${j.hardCapMs}, ${j.bytes} byte(s) output`);
|
|
1872
|
+
if (j.onKill) j.notify = true;
|
|
1873
|
+
this.kill(j.id, reason);
|
|
1874
|
+
}
|
|
1875
|
+
/** Keep a reaper interval alive only while a reapable job is running (unref'd: never holds the process open). */
|
|
1876
|
+
arm() {
|
|
1877
|
+
if (this.timer || !this.options.sweepMs) return;
|
|
1878
|
+
this.timer = setInterval(() => {
|
|
1879
|
+
const live = [...this.jobs.values()].some((j) => j.status === "running" && (j.idleMs || j.hardCapMs));
|
|
1880
|
+
if (!live) {
|
|
1881
|
+
clearInterval(this.timer);
|
|
1882
|
+
this.timer = void 0;
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
void this.sweep().catch((e) => log5.warn(`job sweep failed: ${e}`));
|
|
1886
|
+
}, this.options.sweepMs);
|
|
1887
|
+
this.timer.unref?.();
|
|
1888
|
+
}
|
|
1889
|
+
/** Stop the reaper timer (tests / teardown). */
|
|
1890
|
+
dispose() {
|
|
1891
|
+
if (this.timer) clearInterval(this.timer);
|
|
1892
|
+
this.timer = void 0;
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
NO_JOB = (id) => `Error: no background job '${id}'. Use JobStatus with no id to list jobs.`;
|
|
1896
|
+
describe = (j, now4) => `${j.id} ${j.status}${j.reason ? ` (${j.reason})` : ""} ${j.kind}${j.label ? " " + j.label : ""} \xB7 ${secs((j.endedAt ?? now4) - j.startedAt)} old \xB7 last activity ${secs(now4 - j.lastActivityAt)} ago \xB7 ${j.bytes} byte(s)`;
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1512
1900
|
// src/worktree.ts
|
|
1513
1901
|
var worktree_exports = {};
|
|
1514
1902
|
__export(worktree_exports, {
|
|
@@ -2118,7 +2506,10 @@ __export(tools_shell_exports, {
|
|
|
2118
2506
|
ShellJobRegistry: () => ShellJobRegistry,
|
|
2119
2507
|
formatJobExit: () => formatJobExit,
|
|
2120
2508
|
makeRealShellTool: () => makeRealShellTool,
|
|
2121
|
-
makeShellJobTools: () => makeShellJobTools
|
|
2509
|
+
makeShellJobTools: () => makeShellJobTools,
|
|
2510
|
+
parsePsTime: () => parsePsTime,
|
|
2511
|
+
psGroupCpuSeconds: () => psGroupCpuSeconds,
|
|
2512
|
+
sumTreeCpu: () => sumTreeCpu
|
|
2122
2513
|
});
|
|
2123
2514
|
function killGroup(proc, signal) {
|
|
2124
2515
|
if (!proc?.pid) return false;
|
|
@@ -2148,10 +2539,39 @@ async function nodeSpawn() {
|
|
|
2148
2539
|
return _spawn;
|
|
2149
2540
|
}
|
|
2150
2541
|
function formatJobExit(n) {
|
|
2151
|
-
return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
|
|
2542
|
+
return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}${n.reason ? `: ${n.reason}` : ""}] \`${n.command}\`
|
|
2152
2543
|
` + (n.tail ? `${n.tail}
|
|
2153
2544
|
` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
|
|
2154
2545
|
}
|
|
2546
|
+
function parsePsTime(t) {
|
|
2547
|
+
const m = t.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/);
|
|
2548
|
+
if (!m) return void 0;
|
|
2549
|
+
return Number(m[1] ?? 0) * 86400 + Number(m[2] ?? 0) * 3600 + Number(m[3]) * 60 + Number(m[4]);
|
|
2550
|
+
}
|
|
2551
|
+
async function psGroupCpuSeconds(pgid) {
|
|
2552
|
+
try {
|
|
2553
|
+
const { execFile: execFile4 } = await import("child_process");
|
|
2554
|
+
const out = await new Promise((res, rej) => execFile4("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
|
|
2555
|
+
return sumTreeCpu(out, pgid);
|
|
2556
|
+
} catch (e) {
|
|
2557
|
+
log17.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
|
|
2558
|
+
return void 0;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
function sumTreeCpu(psOut, root) {
|
|
2562
|
+
const rows = psOut.split("\n").map((l) => l.trim().split(/\s+/)).filter((r) => r.length >= 4).map(([pid, ppid, pgid, time]) => ({ pid: Number(pid), ppid: Number(ppid), pgid: Number(pgid), sec: parsePsTime(time) }));
|
|
2563
|
+
const inJob = new Set(rows.filter((r) => r.pgid === root || r.pid === root).map((r) => r.pid));
|
|
2564
|
+
for (let grew = true; grew; ) {
|
|
2565
|
+
grew = false;
|
|
2566
|
+
for (const r of rows) if (!inJob.has(r.pid) && inJob.has(r.ppid)) {
|
|
2567
|
+
inJob.add(r.pid);
|
|
2568
|
+
grew = true;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
let total;
|
|
2572
|
+
for (const r of rows) if (inJob.has(r.pid) && r.sec != null) total = (total ?? 0) + r.sec;
|
|
2573
|
+
return total;
|
|
2574
|
+
}
|
|
2155
2575
|
function makeRealShellTool(options) {
|
|
2156
2576
|
const defaultTimeoutMs = options.timeoutMs ?? 12e4;
|
|
2157
2577
|
const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
|
|
@@ -2282,7 +2702,7 @@ function makeRealShellTool(options) {
|
|
|
2282
2702
|
proc.stderr?.on("data", collect);
|
|
2283
2703
|
proc.on("error", (err2) => {
|
|
2284
2704
|
if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
2285
|
-
|
|
2705
|
+
log17.debug("shell spawn error", err2);
|
|
2286
2706
|
finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
|
|
2287
2707
|
});
|
|
2288
2708
|
proc.on("close", (code) => {
|
|
@@ -2302,7 +2722,7 @@ function makeRealShellTool(options) {
|
|
|
2302
2722
|
};
|
|
2303
2723
|
}
|
|
2304
2724
|
function handoffFor(timeoutMs, id, body, notifies) {
|
|
2305
|
-
const head = `[still running] exceeded ${timeoutMs}ms, so it was handed to background job ${id} \u2014 NOT killed, it is still going. Check on it with ShellOutput({id:"${id}"}) / ShellStatus({id:"${id}"}), stop it with ShellKill({id:"${id}"}). ` + (notifies ? "Its completion will be reported to you, so you may continue with other work meanwhile." : "Nothing will tell you when it finishes \u2014 poll it yourself before you rely on its result.");
|
|
2725
|
+
const head = `[still running] exceeded ${timeoutMs}ms, so it was handed to background job ${id} \u2014 NOT killed, it is still going (it is only stopped if it goes silent with no CPU activity for a long time, and then you are told why). Check on it with ShellOutput({id:"${id}"}) / ShellStatus({id:"${id}"}), stop it with ShellKill({id:"${id}"}). ` + (notifies ? "Its completion will be reported to you, so you may continue with other work meanwhile." : "Nothing will tell you when it finishes \u2014 poll it yourself before you rely on its result.");
|
|
2306
2726
|
return body ? `${head}
|
|
2307
2727
|
Output so far:
|
|
2308
2728
|
${body}` : head;
|
|
@@ -2326,7 +2746,7 @@ function makeShellJobTools(registry) {
|
|
|
2326
2746
|
const out = registry.output(String(id));
|
|
2327
2747
|
if (out == null) return NO_JOB2(String(id));
|
|
2328
2748
|
const st = registry.status(String(id));
|
|
2329
|
-
return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}]
|
|
2749
|
+
return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}${st.reason ? `: ${st.reason}` : ""}]
|
|
2330
2750
|
${clean(out) || "(no output yet)"}`;
|
|
2331
2751
|
}
|
|
2332
2752
|
},
|
|
@@ -2341,13 +2761,13 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
2341
2761
|
return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join("\n") : "(no background jobs)";
|
|
2342
2762
|
}
|
|
2343
2763
|
const st = registry.status(String(id));
|
|
2344
|
-
return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ""} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB2(String(id));
|
|
2764
|
+
return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ""}${st.reason ? ` \u2014 ${st.reason}` : ""} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB2(String(id));
|
|
2345
2765
|
}
|
|
2346
2766
|
},
|
|
2347
2767
|
{
|
|
2348
2768
|
...dropOnRebind,
|
|
2349
2769
|
name: "ShellKill",
|
|
2350
|
-
description: "Stop a running background Shell job by id (SIGTERM).",
|
|
2770
|
+
description: "Stop a running background Shell job by id (SIGTERM to its process group, SIGKILL if it ignores that).",
|
|
2351
2771
|
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
2352
2772
|
async run({ id }) {
|
|
2353
2773
|
return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB2(String(id));
|
|
@@ -2355,7 +2775,7 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
2355
2775
|
}
|
|
2356
2776
|
];
|
|
2357
2777
|
}
|
|
2358
|
-
var
|
|
2778
|
+
var log17, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
|
|
2359
2779
|
var init_tools_shell = __esm({
|
|
2360
2780
|
"src/tools.shell.ts"() {
|
|
2361
2781
|
"use strict";
|
|
@@ -2363,62 +2783,94 @@ var init_tools_shell = __esm({
|
|
|
2363
2783
|
init_redact();
|
|
2364
2784
|
init_logging();
|
|
2365
2785
|
init_shell_sandbox();
|
|
2366
|
-
|
|
2786
|
+
init_tools_jobs();
|
|
2787
|
+
log17 = forComponent("shell");
|
|
2367
2788
|
clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
2368
2789
|
DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
|
|
2369
2790
|
SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
|
|
2370
2791
|
ShellJobRegistry = class {
|
|
2371
2792
|
constructor(cfg) {
|
|
2372
2793
|
this.cfg = cfg;
|
|
2794
|
+
this.jobs = cfg.jobs ?? new JobRegistry(cfg.maxBuffer ? { maxBuffer: cfg.maxBuffer } : {});
|
|
2373
2795
|
if (cfg.killOnExit && typeof process !== "undefined") process.once("exit", () => this.killAll());
|
|
2374
2796
|
}
|
|
2375
2797
|
cfg;
|
|
2376
|
-
jobs
|
|
2377
|
-
|
|
2798
|
+
jobs;
|
|
2799
|
+
meta = /* @__PURE__ */ new Map();
|
|
2378
2800
|
async start(command) {
|
|
2379
|
-
const
|
|
2380
|
-
const max = this.cfg.maxBuffer ?? 256 * 1024;
|
|
2381
|
-
const job = { command, buf: "", status: "running" };
|
|
2382
|
-
const append = (chunk) => {
|
|
2383
|
-
const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
|
|
2384
|
-
job.buf = (job.buf + s).slice(-max);
|
|
2385
|
-
};
|
|
2801
|
+
const h = this.track(command, { idleMs: 0 });
|
|
2386
2802
|
try {
|
|
2387
2803
|
const spawn4 = this.cfg.spawn ?? await nodeSpawn();
|
|
2388
2804
|
const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: "/bin/sh", args: ["-c", command] };
|
|
2389
|
-
|
|
2390
|
-
job.proc = proc;
|
|
2391
|
-
proc.stdout?.on("data", append);
|
|
2392
|
-
proc.stderr?.on("data", append);
|
|
2393
|
-
proc.on("error", (err2) => {
|
|
2394
|
-
if (job.status === "running") {
|
|
2395
|
-
job.status = "error";
|
|
2396
|
-
append(`
|
|
2397
|
-
[error] ${err2?.message ?? err2}`);
|
|
2398
|
-
this.notifyExit(id, job);
|
|
2399
|
-
}
|
|
2400
|
-
});
|
|
2401
|
-
proc.on("close", (code) => {
|
|
2402
|
-
if (job.status === "running") {
|
|
2403
|
-
job.status = "exited";
|
|
2404
|
-
job.exitCode = code ?? void 0;
|
|
2405
|
-
this.notifyExit(id, job);
|
|
2406
|
-
}
|
|
2407
|
-
});
|
|
2805
|
+
this.attach(h, spawn4(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED }));
|
|
2408
2806
|
} catch (e) {
|
|
2409
|
-
|
|
2410
|
-
|
|
2807
|
+
h.chunk(`failed to spawn: ${e?.message ?? e}`);
|
|
2808
|
+
h.finish("error", void 0, e?.message ?? String(e));
|
|
2411
2809
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2810
|
+
return h.id;
|
|
2811
|
+
}
|
|
2812
|
+
track(command, extra) {
|
|
2813
|
+
const meta = { command };
|
|
2814
|
+
let id = "";
|
|
2815
|
+
const h = this.jobs.track({
|
|
2816
|
+
kind: "shell",
|
|
2817
|
+
label: command,
|
|
2818
|
+
seed: extra.seed,
|
|
2819
|
+
idleMs: extra.idleMs,
|
|
2820
|
+
cpuTime: extra.cpu ? async () => meta.proc?.pid ? (this.cfg.cpuTime ?? psGroupCpuSeconds)(meta.proc.pid) : void 0 : void 0,
|
|
2821
|
+
onKill: () => this.signal(meta),
|
|
2822
|
+
onExit: (n) => this.notifyExit(id, meta, n.status, n.reason),
|
|
2823
|
+
notify: true
|
|
2824
|
+
});
|
|
2825
|
+
id = h.id;
|
|
2826
|
+
this.meta.set(id, meta);
|
|
2827
|
+
return h;
|
|
2828
|
+
}
|
|
2829
|
+
attach(h, proc) {
|
|
2830
|
+
const meta = this.meta.get(h.id);
|
|
2831
|
+
meta.proc = proc;
|
|
2832
|
+
const append = (chunk) => h.chunk(typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "");
|
|
2833
|
+
proc.stdout?.on("data", append);
|
|
2834
|
+
proc.stderr?.on("data", append);
|
|
2835
|
+
proc.on("error", (err2) => {
|
|
2836
|
+
if (h.status === "running") {
|
|
2837
|
+
append(`
|
|
2838
|
+
[error] ${err2?.message ?? err2}`);
|
|
2839
|
+
h.finish("error", void 0, err2?.message ?? String(err2));
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
proc.on("close", (code) => {
|
|
2843
|
+
if (h.status === "running") {
|
|
2844
|
+
meta.exitCode = code ?? void 0;
|
|
2845
|
+
h.finish("done");
|
|
2846
|
+
}
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
/** SIGTERM the whole group (bg children are detached group leaders — /bin/sh alone would orphan a forked
|
|
2850
|
+
* server), then SIGKILL after the grace period if it is still there. Falls back to the pid for fakes. */
|
|
2851
|
+
signal(meta) {
|
|
2852
|
+
const proc = meta.proc;
|
|
2853
|
+
if (!killGroup(proc, "SIGTERM")) {
|
|
2854
|
+
try {
|
|
2855
|
+
proc?.kill("SIGTERM");
|
|
2856
|
+
} catch {
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
if (!proc?.pid) return;
|
|
2860
|
+
let closed = false;
|
|
2861
|
+
proc.on("close", () => {
|
|
2862
|
+
closed = true;
|
|
2863
|
+
});
|
|
2864
|
+
const t = setTimeout(() => {
|
|
2865
|
+
if (!closed) killGroup(proc, "SIGKILL");
|
|
2866
|
+
}, this.jobs.options.killGraceMs);
|
|
2867
|
+
t.unref?.();
|
|
2868
|
+
}
|
|
2869
|
+
/** Fire `onExit` at most once per job (the registry guarantees one terminal state), with the tail so the model can act without a second round-trip. */
|
|
2870
|
+
notifyExit(id, meta, status, reason) {
|
|
2871
|
+
if (!this.cfg.onExit || status === "killed" && !reason?.startsWith("stuck")) return;
|
|
2420
2872
|
try {
|
|
2421
|
-
this.cfg.onExit({ id, command:
|
|
2873
|
+
this.cfg.onExit({ id, command: meta.command, status: shellStatus(status), exitCode: meta.exitCode, tail: clean(this.jobs.output(id) ?? "").slice(-4e3), ...reason && status !== "done" ? { reason } : {} });
|
|
2422
2874
|
} catch {
|
|
2423
2875
|
}
|
|
2424
2876
|
}
|
|
@@ -2437,14 +2889,20 @@ var init_tools_shell = __esm({
|
|
|
2437
2889
|
}
|
|
2438
2890
|
/** Current tail output for a job (null = no such job). */
|
|
2439
2891
|
output(id) {
|
|
2440
|
-
return this.
|
|
2892
|
+
return this.meta.has(id) ? this.jobs.output(id) : null;
|
|
2441
2893
|
}
|
|
2894
|
+
// null too once the shared registry pruned it
|
|
2442
2895
|
status(id) {
|
|
2443
|
-
const
|
|
2444
|
-
|
|
2896
|
+
const m = this.meta.get(id);
|
|
2897
|
+
const j = m && this.jobs.get(id);
|
|
2898
|
+
if (!m || !j) {
|
|
2899
|
+
this.meta.delete(id);
|
|
2900
|
+
return null;
|
|
2901
|
+
}
|
|
2902
|
+
return { status: shellStatus(j.status), exitCode: m.exitCode, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} };
|
|
2445
2903
|
}
|
|
2446
2904
|
list() {
|
|
2447
|
-
return [...this.
|
|
2905
|
+
return [...this.meta].filter(([id]) => this.jobs.get(id)).map(([id, m]) => ({ id, command: m.command, status: shellStatus(this.jobs.get(id).status) }));
|
|
2448
2906
|
}
|
|
2449
2907
|
/**
|
|
2450
2908
|
* Take over an ALREADY-RUNNING child as a background job. A foreground command that outruns its
|
|
@@ -2452,53 +2910,22 @@ var init_tools_shell = __esm({
|
|
|
2452
2910
|
* worse, a launcher that spawned its own detached worker leaves that worker running with nothing
|
|
2453
2911
|
* tracking it. Adopting hands the model a handle instead: the command keeps going, its completion
|
|
2454
2912
|
* is reported like any other job, and `seed` carries the output produced before the handoff.
|
|
2913
|
+
* Unlike an explicit background job it IS liveness-checked (idle window + CPU probe): nobody asked for
|
|
2914
|
+
* a long-lived process here, so one that goes silent and CPU-idle is stuck, not serving.
|
|
2455
2915
|
*/
|
|
2456
2916
|
adopt(command, proc, seed = "") {
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const append = (chunk) => {
|
|
2461
|
-
const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
|
|
2462
|
-
job.buf = (job.buf + s).slice(-max);
|
|
2463
|
-
};
|
|
2464
|
-
proc.stdout?.on("data", append);
|
|
2465
|
-
proc.stderr?.on("data", append);
|
|
2466
|
-
proc.on("error", (err2) => {
|
|
2467
|
-
if (job.status === "running") {
|
|
2468
|
-
job.status = "error";
|
|
2469
|
-
append(`
|
|
2470
|
-
[error] ${err2?.message ?? err2}`);
|
|
2471
|
-
this.notifyExit(id, job);
|
|
2472
|
-
}
|
|
2473
|
-
});
|
|
2474
|
-
proc.on("close", (code) => {
|
|
2475
|
-
if (job.status === "running") {
|
|
2476
|
-
job.status = "exited";
|
|
2477
|
-
job.exitCode = code ?? void 0;
|
|
2478
|
-
this.notifyExit(id, job);
|
|
2479
|
-
}
|
|
2480
|
-
});
|
|
2481
|
-
this.jobs.set(id, job);
|
|
2482
|
-
return id;
|
|
2917
|
+
const h = this.track(command, { seed, cpu: true });
|
|
2918
|
+
this.attach(h, proc);
|
|
2919
|
+
return h.id;
|
|
2483
2920
|
}
|
|
2484
|
-
kill(id) {
|
|
2485
|
-
|
|
2486
|
-
if (!j) return false;
|
|
2487
|
-
if (j.status === "running") {
|
|
2488
|
-
if (!killGroup(j.proc, "SIGTERM")) {
|
|
2489
|
-
try {
|
|
2490
|
-
j.proc?.kill("SIGTERM");
|
|
2491
|
-
} catch {
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
j.status = "killed";
|
|
2495
|
-
}
|
|
2496
|
-
return true;
|
|
2921
|
+
kill(id, reason = "killed by ShellKill") {
|
|
2922
|
+
return this.meta.has(id) && this.jobs.kill(id, reason);
|
|
2497
2923
|
}
|
|
2498
2924
|
killAll() {
|
|
2499
|
-
for (const id of this.
|
|
2925
|
+
for (const id of this.meta.keys()) this.jobs.kill(id, "agent exiting");
|
|
2500
2926
|
}
|
|
2501
2927
|
};
|
|
2928
|
+
shellStatus = (s) => s === "done" ? "exited" : s === "running" ? "running" : s === "killed" ? "killed" : "error";
|
|
2502
2929
|
NO_JOB2 = (id) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;
|
|
2503
2930
|
}
|
|
2504
2931
|
});
|
|
@@ -2580,111 +3007,7 @@ import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo
|
|
|
2580
3007
|
// src/Agent.ts
|
|
2581
3008
|
init_llm();
|
|
2582
3009
|
init_tools();
|
|
2583
|
-
|
|
2584
|
-
// src/tools.jobs.ts
|
|
2585
|
-
var SandboxJobRegistry = class {
|
|
2586
|
-
constructor(maxBuffer = 256 * 1024) {
|
|
2587
|
-
this.maxBuffer = maxBuffer;
|
|
2588
|
-
}
|
|
2589
|
-
maxBuffer;
|
|
2590
|
-
jobs = /* @__PURE__ */ new Map();
|
|
2591
|
-
seq = 0;
|
|
2592
|
-
/** Start `fn` in the background and return a job id immediately (does not await completion). */
|
|
2593
|
-
start(fn, opts = {}) {
|
|
2594
|
-
const id = `job-${++this.seq}`;
|
|
2595
|
-
const ctl = new AbortController();
|
|
2596
|
-
const onChunk = (s) => {
|
|
2597
|
-
job.buf = (job.buf + s).slice(-this.maxBuffer);
|
|
2598
|
-
};
|
|
2599
|
-
const job = { kind: opts.kind ?? "task", label: opts.label ?? "", status: "running", buf: "", ctl, promise: Promise.resolve() };
|
|
2600
|
-
job.promise = fn({ signal: ctl.signal, onChunk }).then(
|
|
2601
|
-
(res) => {
|
|
2602
|
-
if (job.status === "running") {
|
|
2603
|
-
job.status = "done";
|
|
2604
|
-
if (res) onChunk(res);
|
|
2605
|
-
}
|
|
2606
|
-
},
|
|
2607
|
-
(e) => {
|
|
2608
|
-
if (job.status === "running") {
|
|
2609
|
-
job.status = "error";
|
|
2610
|
-
onChunk(`
|
|
2611
|
-
[error] ${e?.message ?? e}`);
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
);
|
|
2615
|
-
this.jobs.set(id, job);
|
|
2616
|
-
return id;
|
|
2617
|
-
}
|
|
2618
|
-
/** Tail output for a job (null = no such job, '' = job exists but no output yet). */
|
|
2619
|
-
output(id) {
|
|
2620
|
-
return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
|
|
2621
|
-
}
|
|
2622
|
-
status(id) {
|
|
2623
|
-
const j = this.jobs.get(id);
|
|
2624
|
-
return j ? { status: j.status, bytes: j.buf.length } : null;
|
|
2625
|
-
}
|
|
2626
|
-
list() {
|
|
2627
|
-
return [...this.jobs].map(([id, j]) => ({ id, kind: j.kind, label: j.label, status: j.status }));
|
|
2628
|
-
}
|
|
2629
|
-
/** Signal cancel (the producer decides what abort means — e.g. skip the overlay commit). */
|
|
2630
|
-
kill(id) {
|
|
2631
|
-
const j = this.jobs.get(id);
|
|
2632
|
-
if (!j) return false;
|
|
2633
|
-
if (j.status === "running") {
|
|
2634
|
-
j.ctl.abort();
|
|
2635
|
-
j.status = "killed";
|
|
2636
|
-
}
|
|
2637
|
-
return true;
|
|
2638
|
-
}
|
|
2639
|
-
killAll() {
|
|
2640
|
-
for (const id of this.jobs.keys()) this.kill(id);
|
|
2641
|
-
}
|
|
2642
|
-
/** Await every still-running job. Called at turn end so a job's committed writes aren't lost from view
|
|
2643
|
-
* just because the model didn't poll it. Returns the ids that were still running when drain began. */
|
|
2644
|
-
async drain() {
|
|
2645
|
-
const pending = [...this.jobs].filter(([, j]) => j.status === "running").map(([id]) => id);
|
|
2646
|
-
await Promise.all([...this.jobs.values()].map((j) => j.promise));
|
|
2647
|
-
return pending;
|
|
2648
|
-
}
|
|
2649
|
-
};
|
|
2650
|
-
var NO_JOB = (id) => `Error: no background job '${id}'. Use JobStatus with no id to list jobs, or start one with bash({background:true}).`;
|
|
2651
|
-
function makeJobTools(registry) {
|
|
2652
|
-
return [
|
|
2653
|
-
{
|
|
2654
|
-
name: "JobOutput",
|
|
2655
|
-
description: "Read the accumulated output (tail) of a background job by id (from bash({background:true})).",
|
|
2656
|
-
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
2657
|
-
async run({ id }) {
|
|
2658
|
-
const out = registry.output(String(id));
|
|
2659
|
-
if (out == null) return NO_JOB(String(id));
|
|
2660
|
-
const st = registry.status(String(id));
|
|
2661
|
-
return `[${st.status}]
|
|
2662
|
-
${out.replace(/\n+$/, "") || "(no output yet)"}`;
|
|
2663
|
-
}
|
|
2664
|
-
},
|
|
2665
|
-
{
|
|
2666
|
-
name: "JobStatus",
|
|
2667
|
-
description: "Status of a background job (running/done/killed/error). Omit `id` to list all jobs.",
|
|
2668
|
-
parameters: { type: "object", properties: { id: { type: "string" } } },
|
|
2669
|
-
async run({ id }) {
|
|
2670
|
-
if (!id) {
|
|
2671
|
-
const jobs = registry.list();
|
|
2672
|
-
return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.kind}${j.label ? " " + j.label : ""}`).join("\n") : "(no background jobs)";
|
|
2673
|
-
}
|
|
2674
|
-
const st = registry.status(String(id));
|
|
2675
|
-
return st ? `${st.status} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB(String(id));
|
|
2676
|
-
}
|
|
2677
|
-
},
|
|
2678
|
-
{
|
|
2679
|
-
name: "JobKill",
|
|
2680
|
-
description: "Stop a running background job by id (the job is cancelled; its overlay writes are NOT committed).",
|
|
2681
|
-
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
2682
|
-
async run({ id }) {
|
|
2683
|
-
return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB(String(id));
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
];
|
|
2687
|
-
}
|
|
3010
|
+
init_tools_jobs();
|
|
2688
3011
|
|
|
2689
3012
|
// src/host.ts
|
|
2690
3013
|
var askUserQuestionTool = {
|
|
@@ -3243,7 +3566,7 @@ init_OverlayFilesystem();
|
|
|
3243
3566
|
init_worktree();
|
|
3244
3567
|
init_NodeDiskFilesystem();
|
|
3245
3568
|
init_logging();
|
|
3246
|
-
var
|
|
3569
|
+
var log6 = forComponent("subagent");
|
|
3247
3570
|
async function boundedPool(items, limit, fn) {
|
|
3248
3571
|
const out = new Array(items.length);
|
|
3249
3572
|
let next = 0;
|
|
@@ -3317,7 +3640,7 @@ function traceFileFor(dir, label, depth) {
|
|
|
3317
3640
|
const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
|
|
3318
3641
|
return join4(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
|
|
3319
3642
|
} catch (e) {
|
|
3320
|
-
|
|
3643
|
+
log6.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
|
|
3321
3644
|
return void 0;
|
|
3322
3645
|
}
|
|
3323
3646
|
}
|
|
@@ -3325,7 +3648,7 @@ function flushTrace(file, meta, messages, status) {
|
|
|
3325
3648
|
try {
|
|
3326
3649
|
writeFileSync2(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
|
|
3327
3650
|
} catch (e) {
|
|
3328
|
-
|
|
3651
|
+
log6.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
|
|
3329
3652
|
}
|
|
3330
3653
|
}
|
|
3331
3654
|
function installTrace(child, file, meta) {
|
|
@@ -3366,7 +3689,7 @@ async function runChildTraced(args) {
|
|
|
3366
3689
|
if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
|
|
3367
3690
|
let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
|
|
3368
3691
|
if (wc.timedOut()) {
|
|
3369
|
-
|
|
3692
|
+
log6.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
|
|
3370
3693
|
result += deadlineNote(deadlineMs);
|
|
3371
3694
|
}
|
|
3372
3695
|
await opts.hooks?.onSubagentStop?.(result, { label, agentType });
|
|
@@ -3820,7 +4143,7 @@ function reasoningToChatFragment(model, effort) {
|
|
|
3820
4143
|
}
|
|
3821
4144
|
|
|
3822
4145
|
// src/Agent.ts
|
|
3823
|
-
var
|
|
4146
|
+
var log7 = forComponent("Agent");
|
|
3824
4147
|
function envInt(key, fallback) {
|
|
3825
4148
|
const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
|
|
3826
4149
|
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
@@ -3972,6 +4295,12 @@ var AgentOptions = class {
|
|
|
3972
4295
|
/** Enable `bash({background:true})` + JobOutput/JobStatus/JobKill — sandbox background jobs (overlay-isolated,
|
|
3973
4296
|
* committed on completion, drained at turn end). Useful when the VFS backend is slow (remote) or for sub-agents. */
|
|
3974
4297
|
backgroundJobs = false;
|
|
4298
|
+
/** The job registry every long-lived tool call lives in (MCP calls past their soft timeout, delegated host/native
|
|
4299
|
+
* tools, and — when the host shares it with its `ShellJobRegistry` — shell jobs). Its liveness is what the stall
|
|
4300
|
+
* watchdog and the stuck-job reaper decide from. Default: a fresh registry per Agent. Knobs (idle windows, hard
|
|
4301
|
+
* caps, MCP soft timeout) live on `JobRegistryOptions`. Completion notices reach the model via `inject` unless
|
|
4302
|
+
* the host set its own `onExit`. */
|
|
4303
|
+
jobs;
|
|
3975
4304
|
/** Plan mode: block mutating tools until the agent calls `ExitPlanMode` (host-approved). */
|
|
3976
4305
|
planMode = false;
|
|
3977
4306
|
/** Permission policy gating each tool call (allow / ask / deny). */
|
|
@@ -4038,16 +4367,14 @@ var Agent = class _Agent {
|
|
|
4038
4367
|
/** Per-run ground truth: every tool name actually INVOKED this run (native dispatch + delegated
|
|
4039
4368
|
* runtime activity). The ledger that makes a fabricated tool claim provable. Reset per run. */
|
|
4040
4369
|
invokedTools = /* @__PURE__ */ new Set();
|
|
4041
|
-
/**
|
|
4042
|
-
*
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
*
|
|
4046
|
-
|
|
4047
|
-
/**
|
|
4048
|
-
|
|
4049
|
-
* bounded by the same tool deadline (`toolHoldMs`) so a lost `completed` event can't disable it. */
|
|
4050
|
-
nativeToolsInFlight = /* @__PURE__ */ new Map();
|
|
4370
|
+
/** Every long-lived tool call is a job here (see AgentOptions.jobs). Delegated-runtime HOST tools (cursor
|
|
4371
|
+
* toolExecutor) and NATIVE tools (cursor's own shell/mcp/task, `running` → `completed`) are tracked as
|
|
4372
|
+
* `holds` jobs: while one runs the provider is not silent — it is waiting on a tool — so the idle-stall
|
|
4373
|
+
* watchdog must not fire. Each is bounded by the transport tool deadline (`hardCapMs = toolHoldMs`), so a
|
|
4374
|
+
* tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection. */
|
|
4375
|
+
jobs;
|
|
4376
|
+
/** Native delegated tool activity id → its job (per step attempt). */
|
|
4377
|
+
nativeJobs = /* @__PURE__ */ new Map();
|
|
4051
4378
|
toolHoldMs = 0;
|
|
4052
4379
|
/** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
|
|
4053
4380
|
* from the pre-step transcript, which would re-run it — so such a stall is not retried. */
|
|
@@ -4096,6 +4423,7 @@ var Agent = class _Agent {
|
|
|
4096
4423
|
addTools(tools) {
|
|
4097
4424
|
this.injectedTools.push(...tools);
|
|
4098
4425
|
this.activeTools.push(...tools);
|
|
4426
|
+
if (this.prepared && tools.some(isMcpTool) && !this.activeTools.some((t) => t.name === "JobWait")) this.activeTools.push(...makeJobTools(this.jobs));
|
|
4099
4427
|
}
|
|
4100
4428
|
/** Remove tools by name from a running agent. Returns the count removed. Also filters
|
|
4101
4429
|
* `options.tools` — otherwise a constructor-passed tool resurrects on the next prepare()
|
|
@@ -4147,6 +4475,11 @@ var Agent = class _Agent {
|
|
|
4147
4475
|
}
|
|
4148
4476
|
constructor(options) {
|
|
4149
4477
|
this.options = { ...new AgentOptions(), ...options };
|
|
4478
|
+
this.jobs = this.options.jobs ?? new JobRegistry();
|
|
4479
|
+
this.jobs.options.onExit ??= (n) => {
|
|
4480
|
+
const imgs = n.result?.images;
|
|
4481
|
+
this.inject(imgs?.length ? [{ type: "text", text: formatJobNotice(n) }, ...imgs.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : formatJobNotice(n));
|
|
4482
|
+
};
|
|
4150
4483
|
if (this.options.fs) this.buildCtx();
|
|
4151
4484
|
}
|
|
4152
4485
|
/** Build the tool context from the resolved fs + options. Idempotent-safe: called once (ctor or ensureFs). */
|
|
@@ -4158,7 +4491,8 @@ var Agent = class _Agent {
|
|
|
4158
4491
|
this.ctx.ai = this.options.ai;
|
|
4159
4492
|
this.ctx.model = this.options.model;
|
|
4160
4493
|
this.ctx.parkHuman = (p) => this.park(p);
|
|
4161
|
-
|
|
4494
|
+
this.ctx.toolJobs = this.jobs;
|
|
4495
|
+
if (this.options.backgroundJobs) this.ctx.jobs = this.jobs;
|
|
4162
4496
|
}
|
|
4163
4497
|
/**
|
|
4164
4498
|
* Resolve the filesystem + build the tool context if the ctor couldn't (no `fs` was passed). The disk
|
|
@@ -4174,7 +4508,7 @@ var Agent = class _Agent {
|
|
|
4174
4508
|
const disk = new NodeDiskFilesystem2(process.cwd());
|
|
4175
4509
|
await disk.init();
|
|
4176
4510
|
this.options.fs = new JailedFilesystem2(disk);
|
|
4177
|
-
|
|
4511
|
+
log7.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
|
|
4178
4512
|
}
|
|
4179
4513
|
this.buildCtx();
|
|
4180
4514
|
}
|
|
@@ -4244,7 +4578,7 @@ var Agent = class _Agent {
|
|
|
4244
4578
|
tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
|
|
4245
4579
|
}
|
|
4246
4580
|
if (o.checkpoints) tools = [...tools, ...checkpointTools()];
|
|
4247
|
-
if (this.ctx.jobs) tools = [...tools, ...makeJobTools(this.
|
|
4581
|
+
if (this.ctx.jobs || [...tools, ...this.injectedTools].some(isMcpTool)) tools = [...tools, ...makeJobTools(this.jobs)];
|
|
4248
4582
|
const plan = o.planMode ? planMode({ host: o.host }) : void 0;
|
|
4249
4583
|
if (plan) tools = [...tools, plan.tool];
|
|
4250
4584
|
this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
|
|
@@ -4360,8 +4694,8 @@ var Agent = class _Agent {
|
|
|
4360
4694
|
let closureNudged = false;
|
|
4361
4695
|
let lastProgressAt = start;
|
|
4362
4696
|
const kill = (finishReason) => {
|
|
4363
|
-
|
|
4364
|
-
this.
|
|
4697
|
+
log7.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
|
|
4698
|
+
this.jobs.killAll(`run ended: ${finishReason}`, true);
|
|
4365
4699
|
const claims = this.survivingClaims();
|
|
4366
4700
|
return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
|
|
4367
4701
|
};
|
|
@@ -4385,20 +4719,23 @@ var Agent = class _Agent {
|
|
|
4385
4719
|
this.toolHoldMs = effectiveToolTimeoutMs;
|
|
4386
4720
|
if (isCursorWithTools) {
|
|
4387
4721
|
if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
|
|
4388
|
-
|
|
4722
|
+
log7.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
|
|
4389
4723
|
}
|
|
4390
|
-
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs))
|
|
4724
|
+
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
|
|
4391
4725
|
}
|
|
4392
4726
|
const cursorPo = isCursorWithTools ? {
|
|
4393
4727
|
toolTimeoutMs: effectiveToolTimeoutMs,
|
|
4394
4728
|
toolExecutor: async (name, args) => {
|
|
4395
4729
|
const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
|
|
4396
|
-
this.
|
|
4397
|
-
this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
|
|
4730
|
+
const job = this.jobs.track({ kind: "host", label: name, holds: true, hardCapMs: effectiveToolTimeoutMs });
|
|
4398
4731
|
try {
|
|
4399
|
-
|
|
4732
|
+
const r = await this.dispatch(tc);
|
|
4733
|
+
job.finish("done");
|
|
4734
|
+
return r;
|
|
4735
|
+
} catch (e) {
|
|
4736
|
+
job.finish("error", void 0, String(e));
|
|
4737
|
+
throw e;
|
|
4400
4738
|
} finally {
|
|
4401
|
-
this.hostToolsInFlight--;
|
|
4402
4739
|
this.toolRanThisAttempt = true;
|
|
4403
4740
|
this.stallPoke?.();
|
|
4404
4741
|
}
|
|
@@ -4412,7 +4749,8 @@ var Agent = class _Agent {
|
|
|
4412
4749
|
};
|
|
4413
4750
|
try {
|
|
4414
4751
|
for (let attempt = 0; ; attempt++) {
|
|
4415
|
-
this.
|
|
4752
|
+
for (const [, h] of this.nativeJobs) if (h.status === "running") this.jobs.kill(h.id, "step attempt ended");
|
|
4753
|
+
this.nativeJobs.clear();
|
|
4416
4754
|
this.toolRanThisAttempt = false;
|
|
4417
4755
|
try {
|
|
4418
4756
|
if (useStream) {
|
|
@@ -4444,11 +4782,11 @@ var Agent = class _Agent {
|
|
|
4444
4782
|
const empty = err2?.code === "empty" || err2?.code === "cursor_silent";
|
|
4445
4783
|
const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
|
|
4446
4784
|
const replaysTool = stalled && this.toolRanThisAttempt;
|
|
4447
|
-
if (replaysTool)
|
|
4785
|
+
if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
|
|
4448
4786
|
const transient = !o.signal?.aborted && !isAbortError(err2) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
|
|
4449
4787
|
if (!transient) throw err2;
|
|
4450
4788
|
const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
|
|
4451
|
-
|
|
4789
|
+
log7.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
|
|
4452
4790
|
o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
|
|
4453
4791
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
4454
4792
|
}
|
|
@@ -4456,12 +4794,12 @@ var Agent = class _Agent {
|
|
|
4456
4794
|
} catch (err2) {
|
|
4457
4795
|
if (err2?.code === "budget") return kill("budget");
|
|
4458
4796
|
if (err2?.code === "stall") {
|
|
4459
|
-
|
|
4797
|
+
log7.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
|
|
4460
4798
|
return { ...kill("timeout"), error: err2 };
|
|
4461
4799
|
}
|
|
4462
4800
|
if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
|
|
4463
4801
|
if (err2?.code === "empty" || err2?.code === "cursor_silent") {
|
|
4464
|
-
|
|
4802
|
+
log7.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
|
|
4465
4803
|
return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err2 };
|
|
4466
4804
|
}
|
|
4467
4805
|
const body = err2?.body ?? err2?.response?.data ?? err2?.error;
|
|
@@ -4473,7 +4811,7 @@ var Agent = class _Agent {
|
|
|
4473
4811
|
}
|
|
4474
4812
|
if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
|
|
4475
4813
|
const errLog = err2 instanceof Error ? { message: err2.message, ...err2.detail ? { detail: err2.detail } : {}, stack: err2.stack } : err2;
|
|
4476
|
-
|
|
4814
|
+
log7.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
|
|
4477
4815
|
return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
|
|
4478
4816
|
}
|
|
4479
4817
|
if (o.signal?.aborted) return kill("aborted");
|
|
@@ -4529,21 +4867,21 @@ var Agent = class _Agent {
|
|
|
4529
4867
|
if (emptyTurn && o.failEmptyTurns) {
|
|
4530
4868
|
if (!emptyNudged && canNudge()) {
|
|
4531
4869
|
emptyNudged = true;
|
|
4532
|
-
|
|
4870
|
+
log7.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
|
|
4533
4871
|
this.transcript.push({ role: "user", content: "Continue \u2014 you returned nothing. Answer the user now using the tool results above, or keep working if you are not done." });
|
|
4534
4872
|
continue;
|
|
4535
4873
|
}
|
|
4536
|
-
|
|
4874
|
+
log7.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
|
|
4537
4875
|
const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
|
|
4538
4876
|
e.code = "empty";
|
|
4539
4877
|
return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
|
|
4540
4878
|
}
|
|
4541
4879
|
const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
|
|
4542
4880
|
for (const n of fabricated) this.fabricatedClaims.add(n);
|
|
4543
|
-
if (fabricated.length && !canNudge())
|
|
4881
|
+
if (fabricated.length && !canNudge()) log7.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
|
|
4544
4882
|
if (fabricated.length && canNudge()) {
|
|
4545
4883
|
closureNudged = true;
|
|
4546
|
-
|
|
4884
|
+
log7.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
|
|
4547
4885
|
this.transcript.push({
|
|
4548
4886
|
role: "user",
|
|
4549
4887
|
content: `You referred to ${fabricated.map((n) => `\`${n}\``).join(", ")} in your reply, but you never actually called ${fabricated.length > 1 ? "those tools" : "that tool"} \u2014 no tool call was made, so any result you reported for ${fabricated.length > 1 ? "them" : "it"} was invented. Either call ${fabricated.length > 1 ? "them" : "it"} for real now, or answer without ${fabricated.length > 1 ? "them" : "it"} and do NOT claim any outcome \u2014 including a failure or a timeout \u2014 for a call you did not make.`
|
|
@@ -4552,15 +4890,18 @@ var Agent = class _Agent {
|
|
|
4552
4890
|
}
|
|
4553
4891
|
if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
|
|
4554
4892
|
closureNudged = true;
|
|
4555
|
-
|
|
4893
|
+
log7.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
|
|
4556
4894
|
this.transcript.push({
|
|
4557
4895
|
role: "user",
|
|
4558
4896
|
content: "You ran tools but did not reply. Answer the user now using the tool results above; if a tool failed, say so plainly. Do NOT re-run those tools, do NOT narrate what you did, and do NOT ask for permission to continue."
|
|
4559
4897
|
});
|
|
4560
4898
|
continue;
|
|
4561
4899
|
}
|
|
4562
|
-
|
|
4563
|
-
await this.
|
|
4900
|
+
log7.verbose(`completed in ${steps} step(s)`);
|
|
4901
|
+
const drained = await this.jobs.drain(o.signal);
|
|
4902
|
+
if (o.signal?.aborted) return kill("aborted");
|
|
4903
|
+
if (drained.length) lastProgressAt = Date.now();
|
|
4904
|
+
if (this.drainInjections()) continue;
|
|
4564
4905
|
this.releaseStaleImages();
|
|
4565
4906
|
await this.activeHooks?.onStop?.(res.content ?? "");
|
|
4566
4907
|
const claims = this.survivingClaims();
|
|
@@ -4586,11 +4927,12 @@ var Agent = class _Agent {
|
|
|
4586
4927
|
}
|
|
4587
4928
|
this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
|
|
4588
4929
|
}
|
|
4930
|
+
lastProgressAt = Date.now();
|
|
4589
4931
|
if (this.ctx.needsInput) {
|
|
4590
4932
|
const question = this.ctx.needsInput;
|
|
4591
4933
|
this.ctx.needsInput = void 0;
|
|
4592
|
-
|
|
4593
|
-
await this.
|
|
4934
|
+
log7.warn(`needs_input: ${question.question} (steps=${steps})`);
|
|
4935
|
+
await this.jobs.drain();
|
|
4594
4936
|
const text = [
|
|
4595
4937
|
`OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
|
|
4596
4938
|
...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
|
|
@@ -4629,6 +4971,8 @@ var Agent = class _Agent {
|
|
|
4629
4971
|
if (timer) clearTimeout(timer);
|
|
4630
4972
|
timer = setTimeout(() => {
|
|
4631
4973
|
if (this.toolHoldActive()) return poke();
|
|
4974
|
+
const inFlight = this.jobs.list().filter((j) => j.status === "running");
|
|
4975
|
+
log7.warn(`stall watchdog: no stream output for ${stallMs}ms and no tool holding the stream \u2014 aborting the request` + (inFlight.length ? ` (${inFlight.length} detached job(s) running, not awaited by the stream: ${inFlight.map((j) => `${j.id}/${j.kind}`).join(", ")})` : ""));
|
|
4632
4976
|
fired = true;
|
|
4633
4977
|
ac.abort(stallError());
|
|
4634
4978
|
}, stallMs);
|
|
@@ -4648,12 +4992,11 @@ var Agent = class _Agent {
|
|
|
4648
4992
|
reclassify: (err2) => stalled() ? stallError() : err2
|
|
4649
4993
|
};
|
|
4650
4994
|
}
|
|
4651
|
-
/** A host or native delegated tool is running
|
|
4995
|
+
/** A host or native delegated tool is running and not stuck — the provider is quiet, not stalled. The reap runs
|
|
4996
|
+
* FIRST, so a holder past its deadline is released (logged with evidence) instead of holding forever. */
|
|
4652
4997
|
toolHoldActive() {
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
for (const until of this.nativeToolsInFlight.values()) if (now4 < until) return true;
|
|
4656
|
-
return false;
|
|
4998
|
+
this.jobs.reap();
|
|
4999
|
+
return this.jobs.holding().length > 0;
|
|
4657
5000
|
}
|
|
4658
5001
|
/** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
|
|
4659
5002
|
* non-result the model produced by going silent. Callers retry it rather than report "done". */
|
|
@@ -4693,10 +5036,16 @@ var Agent = class _Agent {
|
|
|
4693
5036
|
if (a.output !== void 0) prev.output = a.output;
|
|
4694
5037
|
prev.status = a.status;
|
|
4695
5038
|
}
|
|
5039
|
+
const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
|
|
4696
5040
|
if (a.status === "running") {
|
|
4697
|
-
if (
|
|
5041
|
+
if (nj) nj.touch();
|
|
5042
|
+
else if (a.id) this.nativeJobs.set(a.id, this.jobs.track({ kind: "native", label: a.name, holds: true, hardCapMs: this.toolHoldMs }));
|
|
4698
5043
|
} else {
|
|
4699
|
-
if (
|
|
5044
|
+
if (nj) {
|
|
5045
|
+
if (typeof a.output === "string") nj.chunk(a.output.slice(-4e3));
|
|
5046
|
+
nj.finish(a.status === "error" ? "error" : "done");
|
|
5047
|
+
this.nativeJobs.delete(a.id);
|
|
5048
|
+
}
|
|
4700
5049
|
this.toolRanThisAttempt = true;
|
|
4701
5050
|
}
|
|
4702
5051
|
sawTool = true;
|
|
@@ -4787,13 +5136,13 @@ var Agent = class _Agent {
|
|
|
4787
5136
|
const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
|
|
4788
5137
|
if (decision?.block) {
|
|
4789
5138
|
const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
|
|
4790
|
-
|
|
5139
|
+
log7.debug(`${tc.function.name} -> ${blocked}`);
|
|
4791
5140
|
await hooks?.postToolUse?.(call, blocked, meta);
|
|
4792
5141
|
return blocked;
|
|
4793
5142
|
}
|
|
4794
5143
|
this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
|
|
4795
5144
|
if (earlyError) {
|
|
4796
|
-
|
|
5145
|
+
log7.debug(`${tc.function.name} -> ${earlyError}`);
|
|
4797
5146
|
await hooks?.postToolUse?.(call, earlyError, meta);
|
|
4798
5147
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
|
|
4799
5148
|
return earlyError;
|
|
@@ -4802,12 +5151,12 @@ var Agent = class _Agent {
|
|
|
4802
5151
|
let images;
|
|
4803
5152
|
let threw = false;
|
|
4804
5153
|
try {
|
|
4805
|
-
|
|
5154
|
+
log7.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
4806
5155
|
this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
|
|
4807
5156
|
try {
|
|
4808
5157
|
hooks.onToolOutput(call, chunk, meta);
|
|
4809
5158
|
} catch (e) {
|
|
4810
|
-
|
|
5159
|
+
log7.debug(`onToolOutput hook error: ${e}`);
|
|
4811
5160
|
}
|
|
4812
5161
|
} : void 0;
|
|
4813
5162
|
const raw = await tool.run(args, this.ctx);
|
|
@@ -4819,7 +5168,7 @@ var Agent = class _Agent {
|
|
|
4819
5168
|
}
|
|
4820
5169
|
} catch (e) {
|
|
4821
5170
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4822
|
-
|
|
5171
|
+
log7.debug(`${tc.function.name} -> error: ${msg}`);
|
|
4823
5172
|
result = `Error: ${msg}`;
|
|
4824
5173
|
threw = true;
|
|
4825
5174
|
} finally {
|
|
@@ -4911,6 +5260,7 @@ function estimateTokens(m, keepRecentImages = 1) {
|
|
|
4911
5260
|
return Math.ceil(sendBytes(m, keepRecentImages) / 4);
|
|
4912
5261
|
}
|
|
4913
5262
|
var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
|
|
5263
|
+
var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
|
|
4914
5264
|
function transportHeadroomMs(t) {
|
|
4915
5265
|
const d = Number(t.maxDurationMs) || 0;
|
|
4916
5266
|
if (!d) return 0;
|
|
@@ -5086,7 +5436,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
|
|
|
5086
5436
|
const ids = callIdSet(messages.slice(from));
|
|
5087
5437
|
while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
|
|
5088
5438
|
if (total > maxTokens)
|
|
5089
|
-
|
|
5439
|
+
log7.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
|
|
5090
5440
|
return [...head, ...messages.slice(from)];
|
|
5091
5441
|
}
|
|
5092
5442
|
function compact(m, max, focus) {
|
|
@@ -5391,6 +5741,7 @@ init_OverlayFilesystem();
|
|
|
5391
5741
|
|
|
5392
5742
|
// src/index.ts
|
|
5393
5743
|
init_tools();
|
|
5744
|
+
init_tools_jobs();
|
|
5394
5745
|
|
|
5395
5746
|
// src/scheduler.ts
|
|
5396
5747
|
function parseCronField(field, min, max) {
|
|
@@ -5663,7 +6014,7 @@ init_tools_web();
|
|
|
5663
6014
|
init_tools();
|
|
5664
6015
|
init_tools_structured();
|
|
5665
6016
|
init_logging();
|
|
5666
|
-
var
|
|
6017
|
+
var log8 = forComponent("scratch");
|
|
5667
6018
|
var SCRATCH_DIR = "/scratch";
|
|
5668
6019
|
function shortArgs(args) {
|
|
5669
6020
|
try {
|
|
@@ -5703,7 +6054,7 @@ var Scratch = class {
|
|
|
5703
6054
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5704
6055
|
await this.fs.writeFile(path, header + raw);
|
|
5705
6056
|
} catch (e) {
|
|
5706
|
-
|
|
6057
|
+
log8.debug("scratch write failed; returning raw", e);
|
|
5707
6058
|
return raw;
|
|
5708
6059
|
}
|
|
5709
6060
|
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
@@ -5733,7 +6084,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
|
|
|
5733
6084
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5734
6085
|
await this.fs.writeFile(path, header + full);
|
|
5735
6086
|
} catch (e) {
|
|
5736
|
-
|
|
6087
|
+
log8.debug("scratch spill failed; cropping lossy", e);
|
|
5737
6088
|
return full.slice(0, pageBytes) + `
|
|
5738
6089
|
|
|
5739
6090
|
[output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
|
|
@@ -5779,7 +6130,7 @@ Question: ${q2}`);
|
|
|
5779
6130
|
const answer = (res.text ?? "").trim();
|
|
5780
6131
|
return answer || "(no answer found in scratch)";
|
|
5781
6132
|
} catch (e) {
|
|
5782
|
-
|
|
6133
|
+
log8.debug("Ask peek failed", e);
|
|
5783
6134
|
return `Error querying scratch: ${e?.message ?? e}`;
|
|
5784
6135
|
}
|
|
5785
6136
|
}
|
|
@@ -5788,7 +6139,7 @@ Question: ${q2}`);
|
|
|
5788
6139
|
|
|
5789
6140
|
// src/lessons.ts
|
|
5790
6141
|
init_logging();
|
|
5791
|
-
var
|
|
6142
|
+
var log9 = forComponent("Lessons");
|
|
5792
6143
|
var LessonOptionsDefaults = class {
|
|
5793
6144
|
minRepeats = 2;
|
|
5794
6145
|
};
|
|
@@ -5813,8 +6164,8 @@ function lessonCapture(options) {
|
|
|
5813
6164
|
counts.set(lesson.slug, n);
|
|
5814
6165
|
if (n < o.minRepeats) return;
|
|
5815
6166
|
written.add(lesson.slug);
|
|
5816
|
-
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) =>
|
|
5817
|
-
|
|
6167
|
+
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log9.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
|
|
6168
|
+
log9.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
|
|
5818
6169
|
}
|
|
5819
6170
|
};
|
|
5820
6171
|
}
|
|
@@ -5822,7 +6173,7 @@ function lessonCapture(options) {
|
|
|
5822
6173
|
// src/reflect.ts
|
|
5823
6174
|
init_llm();
|
|
5824
6175
|
init_logging();
|
|
5825
|
-
var
|
|
6176
|
+
var log10 = forComponent("Reflect");
|
|
5826
6177
|
async function reflectOnRun(o) {
|
|
5827
6178
|
const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
|
|
5828
6179
|
if (!digest.trim()) return null;
|
|
@@ -5838,7 +6189,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5838
6189
|
const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
|
|
5839
6190
|
text = r?.content ?? "";
|
|
5840
6191
|
} catch (e) {
|
|
5841
|
-
|
|
6192
|
+
log10.warn(`reflection call failed: ${e?.message ?? e}`);
|
|
5842
6193
|
return null;
|
|
5843
6194
|
}
|
|
5844
6195
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
@@ -5848,10 +6199,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5848
6199
|
try {
|
|
5849
6200
|
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
5850
6201
|
} catch (e) {
|
|
5851
|
-
|
|
6202
|
+
log10.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
5852
6203
|
return null;
|
|
5853
6204
|
}
|
|
5854
|
-
|
|
6205
|
+
log10.debug(`reflection persisted ${slug2}`);
|
|
5855
6206
|
return slug2;
|
|
5856
6207
|
}
|
|
5857
6208
|
function digestRun(messages, maxChars) {
|
|
@@ -5900,7 +6251,7 @@ function forComponent2(name) {
|
|
|
5900
6251
|
}
|
|
5901
6252
|
|
|
5902
6253
|
// node_modules/@bod.ee/voice/src/core/emotion.ts
|
|
5903
|
-
var
|
|
6254
|
+
var log11 = forComponent2("Emotion");
|
|
5904
6255
|
var EMOTIONS = [
|
|
5905
6256
|
// primary (best results)
|
|
5906
6257
|
"neutral",
|
|
@@ -6032,7 +6383,7 @@ function renderEmotions(text, opts = { show: true }) {
|
|
|
6032
6383
|
speech += cartesiaTag(tag);
|
|
6033
6384
|
if (opts.show) display += m[0];
|
|
6034
6385
|
} else {
|
|
6035
|
-
|
|
6386
|
+
log11.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
6036
6387
|
}
|
|
6037
6388
|
last = m.index + m[0].length;
|
|
6038
6389
|
}
|
|
@@ -6074,7 +6425,7 @@ var EmotionStream = class {
|
|
|
6074
6425
|
if (tag) {
|
|
6075
6426
|
this.pending = tag;
|
|
6076
6427
|
if (this.show) display += m[0];
|
|
6077
|
-
} else
|
|
6428
|
+
} else log11.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
6078
6429
|
last = m.index + m[0].length;
|
|
6079
6430
|
}
|
|
6080
6431
|
this.emit(body.slice(last), (s, d, p) => {
|
|
@@ -6097,7 +6448,7 @@ var EmotionStream = class {
|
|
|
6097
6448
|
};
|
|
6098
6449
|
|
|
6099
6450
|
// node_modules/@bod.ee/voice/src/core/engine.ts
|
|
6100
|
-
var
|
|
6451
|
+
var log12 = forComponent2("VoiceEngine");
|
|
6101
6452
|
var realClock = {
|
|
6102
6453
|
now: () => performance.now(),
|
|
6103
6454
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
@@ -6372,7 +6723,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6372
6723
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
6373
6724
|
this.tts.warmup?.();
|
|
6374
6725
|
this.setState("listening");
|
|
6375
|
-
|
|
6726
|
+
log12.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
6376
6727
|
}
|
|
6377
6728
|
get usingAec() {
|
|
6378
6729
|
return this.stt.usingAec;
|
|
@@ -6394,7 +6745,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6394
6745
|
this.options.onDiag({ t: this.clock.now(), kind, ...fields });
|
|
6395
6746
|
} catch (e) {
|
|
6396
6747
|
this.diagOn = false;
|
|
6397
|
-
|
|
6748
|
+
log12.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
6398
6749
|
}
|
|
6399
6750
|
}
|
|
6400
6751
|
idleWaiters = [];
|
|
@@ -6503,7 +6854,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6503
6854
|
if (prose.trim()) this.repliedSinceDispatch = true;
|
|
6504
6855
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
6505
6856
|
this.tts.speak(forSpeech(speech), true);
|
|
6506
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
6857
|
+
if (!this.spokeDeltas && this.turnStartAt) log12.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
|
|
6507
6858
|
this.spokeDeltas = true;
|
|
6508
6859
|
this.setState("speaking");
|
|
6509
6860
|
if (this.options.revealMode !== "off" && display) {
|
|
@@ -6580,7 +6931,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6580
6931
|
this.drainTimer = null;
|
|
6581
6932
|
this.speaking = false;
|
|
6582
6933
|
this.stopWordReveal(true);
|
|
6583
|
-
if (this.turnStartAt)
|
|
6934
|
+
if (this.turnStartAt) log12.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
|
|
6584
6935
|
this.echoUntil = this.clock.now() + 2500;
|
|
6585
6936
|
if (!this.usingAec) this.stt.reset();
|
|
6586
6937
|
this.setState("listening");
|
|
@@ -6668,7 +7019,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6668
7019
|
this.clearAckTimer("interrupt");
|
|
6669
7020
|
this.adaptiveAckFired = false;
|
|
6670
7021
|
this.ackProbe = null;
|
|
6671
|
-
if (this.uttQueue.length)
|
|
7022
|
+
if (this.uttQueue.length) log12.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
|
|
6672
7023
|
const droppedQueued = this.uttQueue.length;
|
|
6673
7024
|
this.uttQueue = [];
|
|
6674
7025
|
if (!this.speaking && !this.drainTimer) return;
|
|
@@ -6814,7 +7165,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6814
7165
|
if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
|
|
6815
7166
|
this.specSpent = true;
|
|
6816
7167
|
this.specText = this.specPartial;
|
|
6817
|
-
|
|
7168
|
+
log12.debug(`speculate: "${this.specText.slice(0, 60)}"`);
|
|
6818
7169
|
this.diag("speculate", { text: this.specText });
|
|
6819
7170
|
this.options.onSpeculate(this.specText);
|
|
6820
7171
|
}, this.options.speculativeMs);
|
|
@@ -6883,7 +7234,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6883
7234
|
};
|
|
6884
7235
|
this.tts.newContext();
|
|
6885
7236
|
this.tts.speak(phrase, false);
|
|
6886
|
-
|
|
7237
|
+
log12.debug(`backchannel: "${phrase}"`);
|
|
6887
7238
|
this.diag("backchannel", { phrase });
|
|
6888
7239
|
this.options.onBackchannel(phrase);
|
|
6889
7240
|
}
|
|
@@ -6916,7 +7267,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6916
7267
|
});
|
|
6917
7268
|
if (idx < 0) return text;
|
|
6918
7269
|
toks.splice(idx, 1);
|
|
6919
|
-
|
|
7270
|
+
log12.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
|
|
6920
7271
|
return toks.join(" ");
|
|
6921
7272
|
}
|
|
6922
7273
|
/** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
|
|
@@ -7007,7 +7358,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7007
7358
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
7008
7359
|
if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
|
|
7009
7360
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
7010
|
-
|
|
7361
|
+
log12.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
7011
7362
|
this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
|
|
7012
7363
|
this.options.onHold();
|
|
7013
7364
|
if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
|
|
@@ -7042,13 +7393,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7042
7393
|
if (text) {
|
|
7043
7394
|
const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
7044
7395
|
if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
|
|
7045
|
-
|
|
7396
|
+
log12.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
|
|
7046
7397
|
this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
|
|
7047
7398
|
return;
|
|
7048
7399
|
}
|
|
7049
7400
|
const refinal = this.refinalizes(text);
|
|
7050
7401
|
if (refinal && this.repliedSinceDispatch) {
|
|
7051
|
-
|
|
7402
|
+
log12.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
|
|
7052
7403
|
this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
|
|
7053
7404
|
this.lastDispatchFlat = flat;
|
|
7054
7405
|
this.lastDispatchWords = text.trim().split(/\s+/);
|
|
@@ -7208,7 +7559,7 @@ var SpokenSplitter = class {
|
|
|
7208
7559
|
};
|
|
7209
7560
|
|
|
7210
7561
|
// node_modules/@bod.ee/voice/src/adapters/soniox.ts
|
|
7211
|
-
var
|
|
7562
|
+
var log13 = forComponent2("SonioxSTT");
|
|
7212
7563
|
var now = () => performance.now();
|
|
7213
7564
|
var SonioxSTTOptions = class {
|
|
7214
7565
|
auth = "";
|
|
@@ -7249,7 +7600,7 @@ var SonioxSTT = class {
|
|
|
7249
7600
|
this.onDiag({ t: now(), kind, ...fields });
|
|
7250
7601
|
} catch (e) {
|
|
7251
7602
|
this.diagOn = false;
|
|
7252
|
-
|
|
7603
|
+
log13.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7253
7604
|
}
|
|
7254
7605
|
}
|
|
7255
7606
|
lastChunkAt = 0;
|
|
@@ -7291,11 +7642,11 @@ var SonioxSTT = class {
|
|
|
7291
7642
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
7292
7643
|
this.ws.onclose = (ev) => {
|
|
7293
7644
|
if (this.stopped) return;
|
|
7294
|
-
|
|
7645
|
+
log13.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
7295
7646
|
this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
|
|
7296
7647
|
this.reset();
|
|
7297
7648
|
this.connectWs().catch((e) => {
|
|
7298
|
-
|
|
7649
|
+
log13.error(`soniox reconnect failed: ${e.message}`);
|
|
7299
7650
|
this.diag("stt_reconnect_failed", { message: e.message });
|
|
7300
7651
|
});
|
|
7301
7652
|
};
|
|
@@ -7307,7 +7658,7 @@ var SonioxSTT = class {
|
|
|
7307
7658
|
this.endpointTimer = setInterval(() => {
|
|
7308
7659
|
const combined = (this.finalText + this.partialText).trim();
|
|
7309
7660
|
if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
7310
|
-
if (this.firstTokenAt)
|
|
7661
|
+
if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
7311
7662
|
this.reset();
|
|
7312
7663
|
this.onUtterance(combined, now());
|
|
7313
7664
|
}, 120);
|
|
@@ -7319,7 +7670,7 @@ var SonioxSTT = class {
|
|
|
7319
7670
|
if (this.stopped) return;
|
|
7320
7671
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
7321
7672
|
if (now() - ref > noAudioMs) {
|
|
7322
|
-
|
|
7673
|
+
log13.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
7323
7674
|
this.diag("stt_watchdog_fatal", { noAudioMs });
|
|
7324
7675
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
7325
7676
|
this.stop();
|
|
@@ -7342,7 +7693,7 @@ var SonioxSTT = class {
|
|
|
7342
7693
|
handle(m) {
|
|
7343
7694
|
if (m.error_message) {
|
|
7344
7695
|
this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
|
|
7345
|
-
return
|
|
7696
|
+
return log13.error(`soniox: ${m.error_message}`);
|
|
7346
7697
|
}
|
|
7347
7698
|
let endpoint = false;
|
|
7348
7699
|
for (const t of m.tokens ?? []) {
|
|
@@ -7359,7 +7710,7 @@ var SonioxSTT = class {
|
|
|
7359
7710
|
this.onPartial(combined);
|
|
7360
7711
|
if (endpoint && this.finalText.trim()) {
|
|
7361
7712
|
const utterance = this.finalText.trim();
|
|
7362
|
-
if (this.firstTokenAt)
|
|
7713
|
+
if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
7363
7714
|
this.reset();
|
|
7364
7715
|
this.onUtterance(utterance, now());
|
|
7365
7716
|
}
|
|
@@ -7381,7 +7732,7 @@ var SonioxSTT = class {
|
|
|
7381
7732
|
};
|
|
7382
7733
|
|
|
7383
7734
|
// node_modules/@bod.ee/voice/src/adapters/cartesia.ts
|
|
7384
|
-
var
|
|
7735
|
+
var log14 = forComponent2("CartesiaTTS");
|
|
7385
7736
|
var now2 = () => performance.now();
|
|
7386
7737
|
var CartesiaTTSOptions = class {
|
|
7387
7738
|
auth = "";
|
|
@@ -7417,7 +7768,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7417
7768
|
this.onDiag({ t: now2(), kind, ...fields });
|
|
7418
7769
|
} catch (e) {
|
|
7419
7770
|
this.diagOn = false;
|
|
7420
|
-
|
|
7771
|
+
log14.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7421
7772
|
}
|
|
7422
7773
|
}
|
|
7423
7774
|
firstAudioAt = 0;
|
|
@@ -7452,11 +7803,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7452
7803
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
7453
7804
|
});
|
|
7454
7805
|
this.ws.onclose = (ev) => {
|
|
7455
|
-
|
|
7806
|
+
log14.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
7456
7807
|
this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
|
|
7457
7808
|
if (!this.closed) {
|
|
7458
7809
|
this.connecting = this.doConnect().catch((e) => {
|
|
7459
|
-
|
|
7810
|
+
log14.error(`cartesia reconnect failed: ${e.message}`);
|
|
7460
7811
|
this.diag("tts_reconnect_failed", { message: e.message });
|
|
7461
7812
|
});
|
|
7462
7813
|
}
|
|
@@ -7484,12 +7835,12 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7484
7835
|
this.down = true;
|
|
7485
7836
|
this.downAt = now2();
|
|
7486
7837
|
this.consecutiveOk = 0;
|
|
7487
|
-
|
|
7838
|
+
log14.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
7488
7839
|
this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
|
|
7489
7840
|
this.onDone();
|
|
7490
7841
|
this.startProbe();
|
|
7491
7842
|
} else if (!this.down) {
|
|
7492
|
-
(/No valid transcripts/i.test(m.message || "") ?
|
|
7843
|
+
(/No valid transcripts/i.test(m.message || "") ? log14.debug : log14.warn)(`cartesia: ${JSON.stringify(m)}`);
|
|
7493
7844
|
}
|
|
7494
7845
|
}
|
|
7495
7846
|
};
|
|
@@ -7504,7 +7855,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7504
7855
|
this.stopProbe();
|
|
7505
7856
|
const downMs = this.downAt ? now2() - this.downAt : 0;
|
|
7506
7857
|
this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
|
|
7507
|
-
(downMs < 2e3 ?
|
|
7858
|
+
(downMs < 2e3 ? log14.debug : log14.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
7508
7859
|
}
|
|
7509
7860
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
7510
7861
|
async ensureConnected() {
|
|
@@ -7595,7 +7946,7 @@ configureLogging((name) => {
|
|
|
7595
7946
|
});
|
|
7596
7947
|
|
|
7597
7948
|
// src/duplex.ts
|
|
7598
|
-
var
|
|
7949
|
+
var log15 = forComponent("DuplexAgent");
|
|
7599
7950
|
function describeCall(call) {
|
|
7600
7951
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
7601
7952
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -7806,7 +8157,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7806
8157
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
7807
8158
|
if (m) {
|
|
7808
8159
|
this.fabricationCut = true;
|
|
7809
|
-
|
|
8160
|
+
log15.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
7810
8161
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
7811
8162
|
if (!safe) return;
|
|
7812
8163
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -7901,7 +8252,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7901
8252
|
return {
|
|
7902
8253
|
preToolUse: (call) => {
|
|
7903
8254
|
if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
|
|
7904
|
-
|
|
8255
|
+
log15.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
|
|
7905
8256
|
this.abortSpeculation();
|
|
7906
8257
|
return { block: true, reason: "Speculative turn aborted." };
|
|
7907
8258
|
}
|
|
@@ -7939,7 +8290,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7939
8290
|
try {
|
|
7940
8291
|
await this.voice.send(fallback ? "[reminder] You said nothing to the user this turn. Tell them, in ONE short spoken sentence, what just happened \u2014 no tools." : dispatched ? "[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools." : "[reminder] You said nothing to the user this turn. Give your ONE short spoken reply now \u2014 no tools.");
|
|
7941
8292
|
} catch (e) {
|
|
7942
|
-
|
|
8293
|
+
log15.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
7943
8294
|
} finally {
|
|
7944
8295
|
this.nudging = false;
|
|
7945
8296
|
}
|
|
@@ -8032,7 +8383,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8032
8383
|
try {
|
|
8033
8384
|
res = await this.voice.send(spec.text);
|
|
8034
8385
|
} catch (e) {
|
|
8035
|
-
|
|
8386
|
+
log15.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
|
|
8036
8387
|
} finally {
|
|
8037
8388
|
this.voice.options.signal = prevSignal;
|
|
8038
8389
|
}
|
|
@@ -8046,7 +8397,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8046
8397
|
if (d === "abort") {
|
|
8047
8398
|
if (this.voice.transcript.length > base) this.voice.transcript.length = base;
|
|
8048
8399
|
this.speculativeAbortedCalls++;
|
|
8049
|
-
|
|
8400
|
+
log15.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
|
|
8050
8401
|
if (this.spec === spec) this.spec = void 0;
|
|
8051
8402
|
return res ?? empty;
|
|
8052
8403
|
}
|
|
@@ -8311,7 +8662,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8311
8662
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
8312
8663
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
8313
8664
|
if (cres.finishReason !== "stop") {
|
|
8314
|
-
|
|
8665
|
+
log15.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
8315
8666
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
8316
8667
|
}
|
|
8317
8668
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -8447,7 +8798,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8447
8798
|
rec.status = "done";
|
|
8448
8799
|
rec.result = res.text;
|
|
8449
8800
|
const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
|
|
8450
|
-
|
|
8801
|
+
log15.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
8451
8802
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
8452
8803
|
id,
|
|
8453
8804
|
text: res.text,
|
|
@@ -8489,14 +8840,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8489
8840
|
foldIfSuperseded(rec, note) {
|
|
8490
8841
|
if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
|
|
8491
8842
|
if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
|
|
8492
|
-
|
|
8843
|
+
log15.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
|
|
8493
8844
|
return true;
|
|
8494
8845
|
}
|
|
8495
8846
|
failTask(rec, msg) {
|
|
8496
8847
|
this.dropAsk(rec.id);
|
|
8497
8848
|
rec.status = "error";
|
|
8498
8849
|
rec.result = msg;
|
|
8499
|
-
|
|
8850
|
+
log15.warn(`task ${rec.id} failed: ${msg}`);
|
|
8500
8851
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
8501
8852
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
8502
8853
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
@@ -8745,17 +9096,42 @@ async function withDeadline(p, ms) {
|
|
|
8745
9096
|
}
|
|
8746
9097
|
}
|
|
8747
9098
|
var mcpTimedOut = (name, ms = MCP_CALL_TIMEOUT_MS) => `[${name}] no response after ${ms / 1e3}s \u2014 the MCP server never answered. The call's outcome is UNKNOWN: do not assume it ran or that it failed. Verify the state (or pick a bounded/narrower call) before retrying.`;
|
|
9099
|
+
async function runMcpCall(name, call, ctx, timeoutMs) {
|
|
9100
|
+
const jobs = ctx?.toolJobs;
|
|
9101
|
+
const shape = (raw) => {
|
|
9102
|
+
const r2 = toResult(raw);
|
|
9103
|
+
return r2.images?.length ? r2 : r2.text;
|
|
9104
|
+
};
|
|
9105
|
+
if (!jobs) {
|
|
9106
|
+
const raw = await withDeadline(call({}), timeoutMs);
|
|
9107
|
+
return raw === TIMED_OUT ? mcpTimedOut(name, timeoutMs) : shape(raw);
|
|
9108
|
+
}
|
|
9109
|
+
const r = await jobs.runForeground(async ({ signal, touch, onChunk }) => {
|
|
9110
|
+
const raw = await call({
|
|
9111
|
+
signal,
|
|
9112
|
+
onProgress: (p) => {
|
|
9113
|
+
touch();
|
|
9114
|
+
if (p?.message) onChunk(`[progress${p.progress != null ? ` ${p.progress}${p.total != null ? `/${p.total}` : ""}` : ""}] ${p.message}
|
|
9115
|
+
`);
|
|
9116
|
+
}
|
|
9117
|
+
});
|
|
9118
|
+
const res = toResult(raw);
|
|
9119
|
+
onChunk(res.text + (res.images?.length ? `
|
|
9120
|
+
[${res.images.length} image(s) in the result]` : ""));
|
|
9121
|
+
return res;
|
|
9122
|
+
}, { kind: "mcp", label: name, idleAfterActivity: true });
|
|
9123
|
+
if (r.done) return r.value.images?.length ? r.value : r.value.text;
|
|
9124
|
+
const soft = jobs.options.foregroundMs.mcp ?? 0;
|
|
9125
|
+
return `[still running] MCP call ${name} is still working after ${soft >= 1e3 ? `${Math.round(soft / 1e3)}s` : `${soft}ms`}, so it was handed to background job ${r.id} \u2014 NOT cancelled, the server keeps going. Its result will be delivered to you when it finishes, so continue with other work; to block on it use JobWait({id:"${r.id}"}) instead of sleeping. JobStatus/JobOutput show its progress; JobKill({id:"${r.id}"}) cancels it. Do NOT re-issue the same call \u2014 that would run it twice.`;
|
|
9126
|
+
}
|
|
8748
9127
|
function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
|
|
8749
9128
|
return {
|
|
8750
9129
|
name: `${prefix}${spec.name}`,
|
|
8751
9130
|
description: spec.description ?? `MCP tool ${spec.name}`,
|
|
8752
9131
|
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
8753
9132
|
maxDurationMs: timeoutMs,
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
|
|
8757
|
-
const r = toResult(raw);
|
|
8758
|
-
return r.images?.length ? r : r.text;
|
|
9133
|
+
run(args, ctx) {
|
|
9134
|
+
return runMcpCall(spec.name, (o) => callTool(spec.name, args ?? {}, o), ctx, timeoutMs);
|
|
8759
9135
|
}
|
|
8760
9136
|
};
|
|
8761
9137
|
}
|
|
@@ -8796,13 +9172,10 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
|
8796
9172
|
}
|
|
8797
9173
|
},
|
|
8798
9174
|
maxDurationMs: callTimeoutMs,
|
|
8799
|
-
async run({ name, args }) {
|
|
9175
|
+
async run({ name, args }, ctx) {
|
|
8800
9176
|
const n = String(name ?? "");
|
|
8801
9177
|
if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
|
|
8802
|
-
|
|
8803
|
-
if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
|
|
8804
|
-
const r = toResult(raw);
|
|
8805
|
-
return r.images?.length ? r : r.text;
|
|
9178
|
+
return runMcpCall(n, (o) => callTool(n, args ?? {}, o), ctx, callTimeoutMs);
|
|
8806
9179
|
}
|
|
8807
9180
|
};
|
|
8808
9181
|
return [searchTool, callMcpTool];
|
|
@@ -8822,17 +9195,17 @@ function buildMcpCatalog(servers) {
|
|
|
8822
9195
|
return { specs, routes };
|
|
8823
9196
|
}
|
|
8824
9197
|
function searchOverCatalog(servers, specs, routes, resolve5, options) {
|
|
8825
|
-
const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
|
|
9198
|
+
const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
|
|
8826
9199
|
const r = routes.get(name);
|
|
8827
9200
|
if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
|
|
8828
|
-
return resolve5(r.server, r.rawName, args ?? {});
|
|
9201
|
+
return resolve5(r.server, r.rawName, args ?? {}, o);
|
|
8829
9202
|
}, options) : [];
|
|
8830
9203
|
return { tools, serverNames: servers, toolCount: specs.length };
|
|
8831
9204
|
}
|
|
8832
9205
|
function makeMcpToolSearchFromMounted(mounted, options) {
|
|
8833
9206
|
const { specs, routes } = buildMcpCatalog(mounted);
|
|
8834
9207
|
const byName = new Map(mounted.map((m) => [m.name, m]));
|
|
8835
|
-
return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
|
|
9208
|
+
return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args, o) => byName.get(server).client.callTool(rawName, args, o), options);
|
|
8836
9209
|
}
|
|
8837
9210
|
|
|
8838
9211
|
// src/index.ts
|
|
@@ -8845,9 +9218,20 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
|
|
|
8845
9218
|
init_logging();
|
|
8846
9219
|
import { spawn } from "child_process";
|
|
8847
9220
|
import { createHash } from "crypto";
|
|
8848
|
-
var
|
|
9221
|
+
var log16 = forComponent("mcp");
|
|
8849
9222
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
8850
9223
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
9224
|
+
var DEFAULT_CALL_TIMEOUT_MS = 30 * 6e4;
|
|
9225
|
+
function deadlineFor(method, spec, opts) {
|
|
9226
|
+
if (opts?.timeoutMs != null) return opts.timeoutMs;
|
|
9227
|
+
if (method === "tools/call") return spec.callTimeoutMs ?? Math.max(DEFAULT_CALL_TIMEOUT_MS, spec.timeoutMs ?? 0);
|
|
9228
|
+
return spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
9229
|
+
}
|
|
9230
|
+
function withProgressToken(params, token) {
|
|
9231
|
+
const p = params && typeof params === "object" ? params : {};
|
|
9232
|
+
return { ...p, _meta: { ...p._meta ?? {}, progressToken: token } };
|
|
9233
|
+
}
|
|
9234
|
+
var abortError = (signal) => signal.reason instanceof Error ? signal.reason : Object.assign(new Error("MCP request cancelled"), { name: "AbortError" });
|
|
8851
9235
|
var StdioTransport = class {
|
|
8852
9236
|
constructor(spec) {
|
|
8853
9237
|
this.spec = spec;
|
|
@@ -8864,7 +9248,7 @@ var StdioTransport = class {
|
|
|
8864
9248
|
proc.stdout.setEncoding("utf8");
|
|
8865
9249
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
8866
9250
|
proc.stderr.setEncoding("utf8");
|
|
8867
|
-
proc.stderr.on("data", (chunk) =>
|
|
9251
|
+
proc.stderr.on("data", (chunk) => log16.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
8868
9252
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
8869
9253
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
8870
9254
|
}
|
|
@@ -8878,21 +9262,32 @@ var StdioTransport = class {
|
|
|
8878
9262
|
try {
|
|
8879
9263
|
this.dispatch(JSON.parse(line));
|
|
8880
9264
|
} catch (e) {
|
|
8881
|
-
|
|
9265
|
+
log16.debug("dropping non-JSON line from MCP server:", line, e);
|
|
8882
9266
|
}
|
|
8883
9267
|
}
|
|
8884
9268
|
}
|
|
8885
9269
|
dispatch(msg) {
|
|
9270
|
+
if (msg?.method === "notifications/progress") {
|
|
9271
|
+
const p2 = this.pending.get(msg.params?.progressToken);
|
|
9272
|
+
try {
|
|
9273
|
+
p2?.onProgress?.(msg.params ?? {});
|
|
9274
|
+
} catch (e) {
|
|
9275
|
+
log16.debug("onProgress handler threw", e);
|
|
9276
|
+
}
|
|
9277
|
+
return;
|
|
9278
|
+
}
|
|
8886
9279
|
if (msg?.id == null || !this.pending.has(msg.id)) return;
|
|
8887
9280
|
const p = this.pending.get(msg.id);
|
|
8888
9281
|
this.pending.delete(msg.id);
|
|
8889
9282
|
clearTimeout(p.timer);
|
|
9283
|
+
p.done?.();
|
|
8890
9284
|
if (msg.error) p.reject(new Error(msg.error?.message ?? JSON.stringify(msg.error)));
|
|
8891
9285
|
else p.resolve(msg.result);
|
|
8892
9286
|
}
|
|
8893
9287
|
failAll(e) {
|
|
8894
9288
|
for (const p of this.pending.values()) {
|
|
8895
9289
|
clearTimeout(p.timer);
|
|
9290
|
+
p.done?.();
|
|
8896
9291
|
p.reject(e);
|
|
8897
9292
|
}
|
|
8898
9293
|
this.pending.clear();
|
|
@@ -8901,19 +9296,31 @@ var StdioTransport = class {
|
|
|
8901
9296
|
if (!this.proc?.stdin) throw new Error("MCP stdio transport not started");
|
|
8902
9297
|
this.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
8903
9298
|
}
|
|
8904
|
-
request(method, params) {
|
|
9299
|
+
request(method, params, opts = {}) {
|
|
8905
9300
|
const id = this.nextId++;
|
|
8906
|
-
const timeoutMs = this.spec
|
|
9301
|
+
const timeoutMs = deadlineFor(method, this.spec, opts);
|
|
9302
|
+
const { signal, onProgress } = opts;
|
|
8907
9303
|
return new Promise((resolve5, reject) => {
|
|
8908
|
-
|
|
9304
|
+
if (signal?.aborted) return reject(abortError(signal));
|
|
9305
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
8909
9306
|
this.pending.delete(id);
|
|
9307
|
+
done();
|
|
8910
9308
|
reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`));
|
|
8911
|
-
}, timeoutMs);
|
|
8912
|
-
|
|
9309
|
+
}, timeoutMs) : void 0;
|
|
9310
|
+
const onAbort = () => {
|
|
9311
|
+
if (!this.pending.delete(id)) return;
|
|
9312
|
+
clearTimeout(timer);
|
|
9313
|
+
this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log16.debug("cancel notify failed", e));
|
|
9314
|
+
reject(abortError(signal));
|
|
9315
|
+
};
|
|
9316
|
+
const done = () => signal?.removeEventListener("abort", onAbort);
|
|
9317
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
9318
|
+
this.pending.set(id, { resolve: resolve5, reject, timer, onProgress, done });
|
|
8913
9319
|
try {
|
|
8914
|
-
this.write({ jsonrpc: "2.0", id, method, params });
|
|
9320
|
+
this.write({ jsonrpc: "2.0", id, method, params: onProgress ? withProgressToken(params, id) : params });
|
|
8915
9321
|
} catch (e) {
|
|
8916
9322
|
clearTimeout(timer);
|
|
9323
|
+
done();
|
|
8917
9324
|
this.pending.delete(id);
|
|
8918
9325
|
reject(e);
|
|
8919
9326
|
}
|
|
@@ -8927,7 +9334,7 @@ var StdioTransport = class {
|
|
|
8927
9334
|
try {
|
|
8928
9335
|
this.proc?.stdin?.end();
|
|
8929
9336
|
} catch (e) {
|
|
8930
|
-
|
|
9337
|
+
log16.debug("stdin end failed", e);
|
|
8931
9338
|
}
|
|
8932
9339
|
this.proc?.kill();
|
|
8933
9340
|
}
|
|
@@ -8954,13 +9361,24 @@ var HttpTransport = class {
|
|
|
8954
9361
|
fetchImpl;
|
|
8955
9362
|
async start() {
|
|
8956
9363
|
}
|
|
8957
|
-
request(method, params) {
|
|
8958
|
-
|
|
9364
|
+
async request(method, params, opts = {}) {
|
|
9365
|
+
const id = this.nextId++;
|
|
9366
|
+
const deadline = AbortSignal.timeout(deadlineFor(method, this.spec, opts));
|
|
9367
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, deadline]) : deadline;
|
|
9368
|
+
try {
|
|
9369
|
+
return await this.post({ jsonrpc: "2.0", id, method, params: opts.onProgress ? withProgressToken(params, id) : params }, false, signal, opts.onProgress);
|
|
9370
|
+
} catch (e) {
|
|
9371
|
+
if (opts.signal?.aborted) {
|
|
9372
|
+
void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) => log16.debug("cancel notify failed", err2));
|
|
9373
|
+
throw abortError(opts.signal);
|
|
9374
|
+
}
|
|
9375
|
+
throw e;
|
|
9376
|
+
}
|
|
8959
9377
|
}
|
|
8960
9378
|
async notify(method, params) {
|
|
8961
|
-
await this.post({ jsonrpc: "2.0", method, params }, true);
|
|
9379
|
+
await this.post({ jsonrpc: "2.0", method, params }, true, AbortSignal.timeout(this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS));
|
|
8962
9380
|
}
|
|
8963
|
-
async post(msg, isNotify) {
|
|
9381
|
+
async post(msg, isNotify, signal, onProgress) {
|
|
8964
9382
|
const headers = {
|
|
8965
9383
|
"content-type": "application/json",
|
|
8966
9384
|
accept: "application/json, text/event-stream",
|
|
@@ -8973,7 +9391,7 @@ var HttpTransport = class {
|
|
|
8973
9391
|
method: "POST",
|
|
8974
9392
|
headers,
|
|
8975
9393
|
body: JSON.stringify(msg),
|
|
8976
|
-
signal
|
|
9394
|
+
signal
|
|
8977
9395
|
});
|
|
8978
9396
|
const sid = resp.headers.get("mcp-session-id");
|
|
8979
9397
|
if (sid) this.sessionId = sid;
|
|
@@ -8981,25 +9399,63 @@ var HttpTransport = class {
|
|
|
8981
9399
|
if (!resp.ok) throw new Error(`MCP HTTP ${resp.status} ${resp.statusText}`);
|
|
8982
9400
|
if (isNotify) return;
|
|
8983
9401
|
const ct = resp.headers.get("content-type") ?? "";
|
|
8984
|
-
const data = ct.includes("text/event-stream") ?
|
|
9402
|
+
const data = ct.includes("text/event-stream") ? await readSseResponse(resp, msg.id, onProgress) : await resp.json();
|
|
8985
9403
|
if (data?.error) throw new Error(data.error?.message ?? JSON.stringify(data.error));
|
|
8986
9404
|
return data?.result;
|
|
8987
9405
|
}
|
|
8988
9406
|
async close() {
|
|
8989
9407
|
}
|
|
8990
9408
|
};
|
|
8991
|
-
function
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9409
|
+
function sseMessage(line) {
|
|
9410
|
+
const trimmed = line.trimStart();
|
|
9411
|
+
if (!trimmed.startsWith("data:")) return void 0;
|
|
9412
|
+
try {
|
|
9413
|
+
return JSON.parse(trimmed.slice(5).trim());
|
|
9414
|
+
} catch (e) {
|
|
9415
|
+
log16.debug("skipping unparseable SSE data line", e);
|
|
9416
|
+
return void 0;
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
9419
|
+
async function readSseResponse(resp, id, onProgress) {
|
|
9420
|
+
const handle = (obj) => {
|
|
9421
|
+
if (obj?.method === "notifications/progress") {
|
|
9422
|
+
try {
|
|
9423
|
+
onProgress?.(obj.params ?? {});
|
|
9424
|
+
} catch (e) {
|
|
9425
|
+
log16.debug("onProgress handler threw", e);
|
|
9426
|
+
}
|
|
9427
|
+
return void 0;
|
|
9000
9428
|
}
|
|
9429
|
+
if (obj && (obj.result !== void 0 || obj.error !== void 0) && (obj.id == null || id == null || obj.id === id)) return obj;
|
|
9430
|
+
return void 0;
|
|
9431
|
+
};
|
|
9432
|
+
const reader = resp.body?.getReader?.();
|
|
9433
|
+
if (!reader) {
|
|
9434
|
+
for (const l of (await resp.text()).split("\n")) {
|
|
9435
|
+
const r = handle(sseMessage(l));
|
|
9436
|
+
if (r) return r;
|
|
9437
|
+
}
|
|
9438
|
+
return {};
|
|
9439
|
+
}
|
|
9440
|
+
const dec = new TextDecoder();
|
|
9441
|
+
let buf = "";
|
|
9442
|
+
try {
|
|
9443
|
+
for (; ; ) {
|
|
9444
|
+
const { value, done } = await reader.read();
|
|
9445
|
+
buf += done ? dec.decode() : dec.decode(value, { stream: true });
|
|
9446
|
+
let nl;
|
|
9447
|
+
while ((nl = buf.indexOf("\n")) >= 0 || done && buf) {
|
|
9448
|
+
const line = nl >= 0 ? buf.slice(0, nl) : buf;
|
|
9449
|
+
buf = nl >= 0 ? buf.slice(nl + 1) : "";
|
|
9450
|
+
const r = handle(sseMessage(line));
|
|
9451
|
+
if (r) return r;
|
|
9452
|
+
}
|
|
9453
|
+
if (done) return {};
|
|
9454
|
+
}
|
|
9455
|
+
} finally {
|
|
9456
|
+
reader.cancel().catch(() => {
|
|
9457
|
+
});
|
|
9001
9458
|
}
|
|
9002
|
-
return {};
|
|
9003
9459
|
}
|
|
9004
9460
|
var McpClient = class {
|
|
9005
9461
|
constructor(transport, clientInfo = { name: "agentx", version: "0" }) {
|
|
@@ -9023,8 +9479,9 @@ var McpClient = class {
|
|
|
9023
9479
|
const r = await this.transport.request("tools/list");
|
|
9024
9480
|
return Array.isArray(r?.tools) ? r.tools : [];
|
|
9025
9481
|
}
|
|
9026
|
-
|
|
9027
|
-
|
|
9482
|
+
/** `tools/call`. Pass `onProgress` to request progress notifications (liveness) and `signal` to cancel. */
|
|
9483
|
+
async callTool(name, args, opts) {
|
|
9484
|
+
return this.transport.request("tools/call", { name, arguments: args ?? {} }, opts);
|
|
9028
9485
|
}
|
|
9029
9486
|
async listResources() {
|
|
9030
9487
|
const r = await this.transport.request("resources/list");
|
|
@@ -9038,7 +9495,7 @@ var McpClient = class {
|
|
|
9038
9495
|
}
|
|
9039
9496
|
};
|
|
9040
9497
|
function buildTransport(cfg) {
|
|
9041
|
-
return cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs });
|
|
9498
|
+
return cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs, callTimeoutMs: cfg.callTimeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs, callTimeoutMs: cfg.callTimeoutMs });
|
|
9042
9499
|
}
|
|
9043
9500
|
function withTimeout(p, ms, label) {
|
|
9044
9501
|
if (!ms || ms <= 0) return p;
|
|
@@ -9060,11 +9517,11 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
|
|
|
9060
9517
|
return await withTimeout((async () => {
|
|
9061
9518
|
const init = await client.connect();
|
|
9062
9519
|
const specs = await client.listTools();
|
|
9063
|
-
const tools = mcpToolsToAgentTools(specs, (tool, a) => client.callTool(tool, a), `mcp__${name}__`);
|
|
9520
|
+
const tools = mcpToolsToAgentTools(specs, (tool, a, o) => client.callTool(tool, a, o), `mcp__${name}__`);
|
|
9064
9521
|
return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
|
|
9065
9522
|
})(), mountTimeoutMs, name);
|
|
9066
9523
|
} catch (e) {
|
|
9067
|
-
await client.close().catch((err2) =>
|
|
9524
|
+
await client.close().catch((err2) => log16.debug(`close after failed mount of "${name}": ${err2}`));
|
|
9068
9525
|
throw e;
|
|
9069
9526
|
}
|
|
9070
9527
|
}
|
|
@@ -9075,15 +9532,15 @@ function validEntries(servers) {
|
|
|
9075
9532
|
return Object.entries(servers).filter(([name, cfg]) => {
|
|
9076
9533
|
if (!cfg || cfg.disabled) return false;
|
|
9077
9534
|
if (!cfg.command && !cfg.url) {
|
|
9078
|
-
|
|
9535
|
+
log16.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
9079
9536
|
return false;
|
|
9080
9537
|
}
|
|
9081
9538
|
return true;
|
|
9082
9539
|
});
|
|
9083
9540
|
}
|
|
9084
9541
|
function logMountFailure(name, e) {
|
|
9085
|
-
if (e instanceof McpAuthError)
|
|
9086
|
-
else
|
|
9542
|
+
if (e instanceof McpAuthError) log16.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
9543
|
+
else log16.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
9087
9544
|
}
|
|
9088
9545
|
async function mountMcpServers(servers = {}, opts = {}) {
|
|
9089
9546
|
const entries = validEntries(servers);
|
|
@@ -9093,7 +9550,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
|
|
|
9093
9550
|
const name = entries[i][0];
|
|
9094
9551
|
if (r.status === "fulfilled") {
|
|
9095
9552
|
out.push(r.value);
|
|
9096
|
-
|
|
9553
|
+
log16.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
|
|
9097
9554
|
} else logMountFailure(name, r.reason);
|
|
9098
9555
|
});
|
|
9099
9556
|
return out;
|
|
@@ -9133,7 +9590,7 @@ var McpPool = class {
|
|
|
9133
9590
|
const prev = this.warm.get(key);
|
|
9134
9591
|
if (prev) {
|
|
9135
9592
|
clearTimeout(prev.timer);
|
|
9136
|
-
if (prev.client !== client) void prev.client.close().catch((err2) =>
|
|
9593
|
+
if (prev.client !== client) void prev.client.close().catch((err2) => log16.debug(`warm-pool replace close failed: ${err2}`));
|
|
9137
9594
|
}
|
|
9138
9595
|
const e = { client, timer: void 0 };
|
|
9139
9596
|
this.warm.set(key, e);
|
|
@@ -9150,7 +9607,7 @@ var McpPool = class {
|
|
|
9150
9607
|
const e = this.warm.get(key);
|
|
9151
9608
|
if (!e) return;
|
|
9152
9609
|
this.warm.delete(key);
|
|
9153
|
-
await e.client.close().catch((err2) =>
|
|
9610
|
+
await e.client.close().catch((err2) => log16.debug(`warm-pool evict close failed: ${err2}`));
|
|
9154
9611
|
}
|
|
9155
9612
|
async closeAll() {
|
|
9156
9613
|
for (const e of this.warm.values()) {
|
|
@@ -9368,7 +9825,7 @@ init_tools_shell();
|
|
|
9368
9825
|
// src/tools.notify.ts
|
|
9369
9826
|
init_logging();
|
|
9370
9827
|
import { execFile } from "child_process";
|
|
9371
|
-
var
|
|
9828
|
+
var log18 = forComponent("notify");
|
|
9372
9829
|
function makeNotifyTool(opts = {}) {
|
|
9373
9830
|
const platform2 = opts.platform ?? process.platform;
|
|
9374
9831
|
const run = opts.exec ?? execFile;
|
|
@@ -9392,7 +9849,7 @@ function makeNotifyTool(opts = {}) {
|
|
|
9392
9849
|
return new Promise((resolve5) => {
|
|
9393
9850
|
run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
|
|
9394
9851
|
if (e) {
|
|
9395
|
-
|
|
9852
|
+
log18.debug("notification failed", e);
|
|
9396
9853
|
resolve5(`Notification failed: ${e.message}`);
|
|
9397
9854
|
} else resolve5("Notification shown.");
|
|
9398
9855
|
});
|
|
@@ -9408,7 +9865,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
|
|
|
9408
9865
|
init_logging();
|
|
9409
9866
|
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
9410
9867
|
import { homedir } from "os";
|
|
9411
|
-
var
|
|
9868
|
+
var log19 = forComponent("cli-util");
|
|
9412
9869
|
function dotDirs(base, sub, opts = {}) {
|
|
9413
9870
|
const home = opts.home ?? homedir();
|
|
9414
9871
|
const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
|
|
@@ -9425,7 +9882,7 @@ function parseJson(text, fallback, what = "json") {
|
|
|
9425
9882
|
try {
|
|
9426
9883
|
return JSON.parse(text);
|
|
9427
9884
|
} catch (e) {
|
|
9428
|
-
|
|
9885
|
+
log19.debug(`parseJson(${what}) failed: ${e.message}`);
|
|
9429
9886
|
return fallback;
|
|
9430
9887
|
}
|
|
9431
9888
|
}
|
|
@@ -9435,7 +9892,7 @@ function readJsonFile(path, fallback) {
|
|
|
9435
9892
|
try {
|
|
9436
9893
|
text = readFileSync4(path, "utf8");
|
|
9437
9894
|
} catch (e) {
|
|
9438
|
-
|
|
9895
|
+
log19.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
|
|
9439
9896
|
return fallback;
|
|
9440
9897
|
}
|
|
9441
9898
|
return parseJson(text, fallback, path);
|
|
@@ -9490,9 +9947,9 @@ function toCursorMcp(servers) {
|
|
|
9490
9947
|
}
|
|
9491
9948
|
return Object.keys(out).length ? { mcpServers: out } : void 0;
|
|
9492
9949
|
}
|
|
9493
|
-
function cursorProviderOptions(model, cwd, mcpServers) {
|
|
9950
|
+
function cursorProviderOptions(model, cwd, mcpServers, hostMcpServers) {
|
|
9494
9951
|
if (!(model ?? "").startsWith("cursor/")) return void 0;
|
|
9495
|
-
return { cwd, ...toCursorMcp(mcpServers) ?? {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } };
|
|
9952
|
+
return { cwd, ...toCursorMcp(mcpServers) ?? {}, ...hostMcpServers?.length ? { hostMcpServers } : {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } };
|
|
9496
9953
|
}
|
|
9497
9954
|
function toClaudeCodeMcp(servers) {
|
|
9498
9955
|
if (!servers) return void 0;
|
|
@@ -9519,8 +9976,8 @@ function claudeCodeProviderOptions(model, cwd, mcpServers) {
|
|
|
9519
9976
|
const warm = process.env.AGENTX_CC_WARM === "0" ? {} : { claudeCodeSession: randomUUID() };
|
|
9520
9977
|
return { cwd, ...env ? { env } : {}, ...warm, ...toClaudeCodeMcp(mcpServers) ?? {} };
|
|
9521
9978
|
}
|
|
9522
|
-
function providerOptionsFor(model, cwd, mcpServers) {
|
|
9523
|
-
return cursorProviderOptions(model, cwd, mcpServers) ?? claudeCodeProviderOptions(model, cwd, mcpServers);
|
|
9979
|
+
function providerOptionsFor(model, cwd, mcpServers, hostMcpServers) {
|
|
9980
|
+
return cursorProviderOptions(model, cwd, mcpServers, hostMcpServers) ?? claudeCodeProviderOptions(model, cwd, mcpServers);
|
|
9524
9981
|
}
|
|
9525
9982
|
async function buildAgent(o) {
|
|
9526
9983
|
if (typeof o.sandbox !== "boolean")
|
|
@@ -9605,12 +10062,14 @@ Reference files in them by their mount path (the left side).`;
|
|
|
9605
10062
|
const hooks = o.learnFromMistakes && memoryWriteDir ? composeHooks(o.hooks, lessonCapture({ fs, dir: memoryWriteDir, minRepeats: 2 })) : o.hooks;
|
|
9606
10063
|
let realShell = [];
|
|
9607
10064
|
const agentRef = {};
|
|
10065
|
+
const jobRegistry = new JobRegistry();
|
|
9608
10066
|
const useRealShell = o.realShell ?? !virtual;
|
|
9609
10067
|
if (useRealShell && !virtual && !isClaudeCode) {
|
|
9610
10068
|
const jobs = new ShellJobRegistry({
|
|
9611
10069
|
cwd,
|
|
9612
10070
|
killOnExit: true,
|
|
9613
10071
|
osSandbox: o.osSandbox,
|
|
10072
|
+
jobs: jobRegistry,
|
|
9614
10073
|
onExit: (n) => agentRef.agent?.inject(formatJobExit(n))
|
|
9615
10074
|
});
|
|
9616
10075
|
realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox, jobsSurviveRun: o.singleRun !== true }), ...makeShellJobTools(jobs)];
|
|
@@ -9619,6 +10078,7 @@ Reference files in them by their mount path (the left side).`;
|
|
|
9619
10078
|
const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
|
|
9620
10079
|
return agentRef.agent = new Agent({
|
|
9621
10080
|
ai: o.ai,
|
|
10081
|
+
jobs: jobRegistry,
|
|
9622
10082
|
fs,
|
|
9623
10083
|
model: o.model ?? "anthropic/claude-sonnet-4-6",
|
|
9624
10084
|
// PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
|
|
@@ -9630,7 +10090,7 @@ Reference files in them by their mount path (the left side).`;
|
|
|
9630
10090
|
// Warm cursor session (default on; CURSOR_WARM=0 to disable): one long-lived agent reused across
|
|
9631
10091
|
// turns, amortising the ~2s Agent.create + h2 handshake. Keyed per agentx launch.
|
|
9632
10092
|
...(() => {
|
|
9633
|
-
const po = providerOptionsFor(o.model, cwd, o.mcpServers);
|
|
10093
|
+
const po = providerOptionsFor(o.model, cwd, o.mcpServers, o.hostMcpServers);
|
|
9634
10094
|
return po ? { providerOptions: po } : {};
|
|
9635
10095
|
})(),
|
|
9636
10096
|
...(() => {
|
|
@@ -9731,7 +10191,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
|
|
|
9731
10191
|
import { homedir as homedir3 } from "os";
|
|
9732
10192
|
import { dirname as dirname3, join as join6 } from "path";
|
|
9733
10193
|
import { fileURLToPath } from "url";
|
|
9734
|
-
var
|
|
10194
|
+
var log20 = forComponent("VoiceIO");
|
|
9735
10195
|
var now3 = () => performance.now();
|
|
9736
10196
|
var Player = class {
|
|
9737
10197
|
proc = null;
|
|
@@ -9745,7 +10205,7 @@ var Player = class {
|
|
|
9745
10205
|
["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
|
|
9746
10206
|
{ stdio: ["pipe", "ignore", "ignore"] }
|
|
9747
10207
|
);
|
|
9748
|
-
this.proc.on("error", (e) =>
|
|
10208
|
+
this.proc.on("error", (e) => log20.warn(`ffplay error: ${e.message}`));
|
|
9749
10209
|
this.proc.stdin.on("error", () => {
|
|
9750
10210
|
});
|
|
9751
10211
|
this.bytesWritten = 0;
|
|
@@ -9783,7 +10243,7 @@ function detectFfmpegMic() {
|
|
|
9783
10243
|
const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
|
|
9784
10244
|
const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
|
|
9785
10245
|
if (!mic) throw new Error("no audio input device found");
|
|
9786
|
-
|
|
10246
|
+
log20.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
|
|
9787
10247
|
return `:${mic.idx}`;
|
|
9788
10248
|
}
|
|
9789
10249
|
function detectedInputDevice() {
|
|
@@ -9819,15 +10279,15 @@ function resolveAecBinary() {
|
|
|
9819
10279
|
if (existsSync4(bin) && statSync4(bin).mtimeMs >= statSync4(src).mtimeMs) return bin;
|
|
9820
10280
|
if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
|
|
9821
10281
|
mkdirSync5(cacheDir, { recursive: true });
|
|
9822
|
-
|
|
10282
|
+
log20.info("compiling AEC mic helper (first run)\u2026");
|
|
9823
10283
|
const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
|
|
9824
10284
|
if (build.status !== 0) {
|
|
9825
|
-
|
|
10285
|
+
log20.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
|
|
9826
10286
|
return null;
|
|
9827
10287
|
}
|
|
9828
10288
|
const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
|
|
9829
10289
|
if (sign.status !== 0) {
|
|
9830
|
-
|
|
10290
|
+
log20.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
|
|
9831
10291
|
return null;
|
|
9832
10292
|
}
|
|
9833
10293
|
return bin;
|
|
@@ -9869,16 +10329,16 @@ var NodeMicSource = class {
|
|
|
9869
10329
|
this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
|
|
9870
10330
|
} else {
|
|
9871
10331
|
if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
|
|
9872
|
-
|
|
10332
|
+
log20.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
|
|
9873
10333
|
this.proc = spawn2(
|
|
9874
10334
|
"ffmpeg",
|
|
9875
10335
|
["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
|
|
9876
10336
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
9877
10337
|
);
|
|
9878
|
-
this.proc.stderr.on("data", (d) =>
|
|
10338
|
+
this.proc.stderr.on("data", (d) => log20.warn(`ffmpeg: ${String(d).trim()}`));
|
|
9879
10339
|
}
|
|
9880
10340
|
this.proc.on("exit", (c) => {
|
|
9881
|
-
if (c && !this.stopped)
|
|
10341
|
+
if (c && !this.stopped) log20.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
|
|
9882
10342
|
});
|
|
9883
10343
|
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
9884
10344
|
}
|
|
@@ -9944,7 +10404,7 @@ var AecDuplexAudio = class {
|
|
|
9944
10404
|
this.proc.stdin.on("error", () => {
|
|
9945
10405
|
});
|
|
9946
10406
|
this.proc.on("exit", (c) => {
|
|
9947
|
-
if (c && !this.stopped)
|
|
10407
|
+
if (c && !this.stopped) log20.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
|
|
9948
10408
|
});
|
|
9949
10409
|
this.proc.stdout.on("data", (chunk) => {
|
|
9950
10410
|
this.gotChunk = true;
|
|
@@ -9960,7 +10420,7 @@ var AecDuplexAudio = class {
|
|
|
9960
10420
|
openMicSettings();
|
|
9961
10421
|
this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
|
|
9962
10422
|
}
|
|
9963
|
-
} else
|
|
10423
|
+
} else log20.debug(`mic-aec: ${s}`);
|
|
9964
10424
|
}
|
|
9965
10425
|
});
|
|
9966
10426
|
if (!this.noVpio && !this.triedFallback) {
|
|
@@ -9969,7 +10429,7 @@ var AecDuplexAudio = class {
|
|
|
9969
10429
|
this.triedFallback = true;
|
|
9970
10430
|
this.noVpio = true;
|
|
9971
10431
|
this._aec = false;
|
|
9972
|
-
|
|
10432
|
+
log20.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
|
|
9973
10433
|
this.onDegrade?.();
|
|
9974
10434
|
this.killProc();
|
|
9975
10435
|
this.spawnHelper();
|
|
@@ -10189,7 +10649,7 @@ function fakeVoiceParts(uttFile) {
|
|
|
10189
10649
|
init_logging();
|
|
10190
10650
|
import { appendFile, mkdirSync as mkdirSync6 } from "fs";
|
|
10191
10651
|
import { dirname as dirname4 } from "path";
|
|
10192
|
-
var
|
|
10652
|
+
var log21 = forComponent("VoiceDiag");
|
|
10193
10653
|
var JsonlDiagSink = class {
|
|
10194
10654
|
constructor(path) {
|
|
10195
10655
|
this.path = path;
|
|
@@ -10230,7 +10690,7 @@ var JsonlDiagSink = class {
|
|
|
10230
10690
|
disable(e) {
|
|
10231
10691
|
this.disabled = true;
|
|
10232
10692
|
this.queue.length = 0;
|
|
10233
|
-
|
|
10693
|
+
log21.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
|
|
10234
10694
|
}
|
|
10235
10695
|
/** True once a failure permanently disabled the sink (tests/inspection). */
|
|
10236
10696
|
get isDisabled() {
|
|
@@ -10300,7 +10760,7 @@ async function loadConfig(cwd) {
|
|
|
10300
10760
|
|
|
10301
10761
|
// cli/hooks-config.ts
|
|
10302
10762
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
10303
|
-
var
|
|
10763
|
+
var log22 = forComponent("hooks");
|
|
10304
10764
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
10305
10765
|
function ruleMatches(rule, toolName) {
|
|
10306
10766
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -10317,7 +10777,7 @@ function runCmd(rule, env) {
|
|
|
10317
10777
|
});
|
|
10318
10778
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
10319
10779
|
} catch (e) {
|
|
10320
|
-
|
|
10780
|
+
log22.debug(`hook command failed: ${rule.command}`, e);
|
|
10321
10781
|
return { code: 1, out: String(e?.message ?? e) };
|
|
10322
10782
|
}
|
|
10323
10783
|
}
|
|
@@ -10424,7 +10884,7 @@ function formatDiff(ops, opts = {}) {
|
|
|
10424
10884
|
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
|
|
10425
10885
|
import { homedir as homedir5 } from "os";
|
|
10426
10886
|
import { join as join8 } from "path";
|
|
10427
|
-
var
|
|
10887
|
+
var log23 = forComponent("session");
|
|
10428
10888
|
var globalDir = () => join8(homedir5(), ".agent", "sessions");
|
|
10429
10889
|
var SessionStore = class {
|
|
10430
10890
|
dir;
|
|
@@ -10469,7 +10929,7 @@ var SessionStore = class {
|
|
|
10469
10929
|
}
|
|
10470
10930
|
load(id) {
|
|
10471
10931
|
if (!this.safeId(id)) {
|
|
10472
|
-
|
|
10932
|
+
log23.debug(`rejecting unsafe session id: ${id}`);
|
|
10473
10933
|
return void 0;
|
|
10474
10934
|
}
|
|
10475
10935
|
const path = join8(this.dir, `${id}.json`);
|
|
@@ -10477,7 +10937,7 @@ var SessionStore = class {
|
|
|
10477
10937
|
try {
|
|
10478
10938
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
10479
10939
|
} catch (e) {
|
|
10480
|
-
|
|
10940
|
+
log23.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
10481
10941
|
return void 0;
|
|
10482
10942
|
}
|
|
10483
10943
|
}
|
|
@@ -10490,7 +10950,7 @@ var SessionStore = class {
|
|
|
10490
10950
|
try {
|
|
10491
10951
|
metas.push(JSON.parse(readFileSync7(join8(this.dir, f), "utf8")).meta);
|
|
10492
10952
|
} catch (e) {
|
|
10493
|
-
|
|
10953
|
+
log23.debug(`skipping unreadable session file ${f}`, e);
|
|
10494
10954
|
}
|
|
10495
10955
|
}
|
|
10496
10956
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -10659,7 +11119,7 @@ import { promisify } from "util";
|
|
|
10659
11119
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync8 } from "fs";
|
|
10660
11120
|
import { homedir as homedir6 } from "os";
|
|
10661
11121
|
import { join as join9, resolve as resolve3, sep as sep3, parse as parsePath } from "path";
|
|
10662
|
-
var
|
|
11122
|
+
var log24 = forComponent("checkpoints");
|
|
10663
11123
|
var exec = promisify(execFile3);
|
|
10664
11124
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
10665
11125
|
function isBroadRoot(p) {
|
|
@@ -10705,7 +11165,7 @@ var ShadowRepo = class {
|
|
|
10705
11165
|
writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
10706
11166
|
this.ready = true;
|
|
10707
11167
|
} catch (e) {
|
|
10708
|
-
|
|
11168
|
+
log24.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
10709
11169
|
this.ready = false;
|
|
10710
11170
|
}
|
|
10711
11171
|
return this.ready;
|
|
@@ -10716,7 +11176,7 @@ var ShadowRepo = class {
|
|
|
10716
11176
|
}
|
|
10717
11177
|
async commit(label, forced = []) {
|
|
10718
11178
|
await this.run("add", "-A");
|
|
10719
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
11179
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log24.debug(`force-add failed: ${p}`, e));
|
|
10720
11180
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
10721
11181
|
}
|
|
10722
11182
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -10724,8 +11184,8 @@ var ShadowRepo = class {
|
|
|
10724
11184
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
10725
11185
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
10726
11186
|
async amendForced(paths) {
|
|
10727
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
10728
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
11187
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log24.debug(`force-capture failed: ${p}`, e));
|
|
11188
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log24.debug("amend failed", e));
|
|
10729
11189
|
}
|
|
10730
11190
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
10731
11191
|
async log(ref) {
|
|
@@ -10785,7 +11245,7 @@ var ShadowRepo = class {
|
|
|
10785
11245
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
10786
11246
|
});
|
|
10787
11247
|
} catch (e) {
|
|
10788
|
-
|
|
11248
|
+
log24.debug("checkpoint prune failed", e);
|
|
10789
11249
|
}
|
|
10790
11250
|
}
|
|
10791
11251
|
};
|
|
@@ -10853,7 +11313,7 @@ var GitCheckpoints = class {
|
|
|
10853
11313
|
use(sessionId) {
|
|
10854
11314
|
if (sessionId === this.session) return;
|
|
10855
11315
|
this.session = sessionId;
|
|
10856
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
11316
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log24.debug("re-point failed", e));
|
|
10857
11317
|
}
|
|
10858
11318
|
async begin(label) {
|
|
10859
11319
|
if (!await this.start()) return;
|
|
@@ -10873,7 +11333,7 @@ var GitCheckpoints = class {
|
|
|
10873
11333
|
process.stderr.write(`\x1B[2m checkpoints: '${this.repos[i].root}' too large to snapshot in ${Math.round(this.options.opTimeoutMs / 1e3)}s \u2014 file rewind disabled here\x1B[0m
|
|
10874
11334
|
`);
|
|
10875
11335
|
} else {
|
|
10876
|
-
|
|
11336
|
+
log24.debug("checkpoint commit failed", e);
|
|
10877
11337
|
survivors.push(this.repos[i]);
|
|
10878
11338
|
survivorCaches.push(this.caches[i] ?? []);
|
|
10879
11339
|
}
|
|
@@ -10908,7 +11368,7 @@ var GitCheckpoints = class {
|
|
|
10908
11368
|
if (this.forced.has(abs)) continue;
|
|
10909
11369
|
this.forced.add(abs);
|
|
10910
11370
|
const i = this.repoIndexFor(abs);
|
|
10911
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
11371
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log24.debug("amendForced failed", e));
|
|
10912
11372
|
}
|
|
10913
11373
|
}
|
|
10914
11374
|
};
|
|
@@ -12731,7 +13191,7 @@ import { spawnSync as spawnSync5 } from "child_process";
|
|
|
12731
13191
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
12732
13192
|
import { homedir as homedir8 } from "os";
|
|
12733
13193
|
import { join as join12 } from "path";
|
|
12734
|
-
var
|
|
13194
|
+
var log25 = forComponent("os-sched");
|
|
12735
13195
|
var OsScheduler = class {
|
|
12736
13196
|
options;
|
|
12737
13197
|
constructor(options) {
|
|
@@ -12793,7 +13253,7 @@ var OsScheduler = class {
|
|
|
12793
13253
|
}
|
|
12794
13254
|
}
|
|
12795
13255
|
} catch (e) {
|
|
12796
|
-
|
|
13256
|
+
log25.debug(`cancel ${id}`, e);
|
|
12797
13257
|
}
|
|
12798
13258
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
12799
13259
|
try {
|
|
@@ -12910,7 +13370,7 @@ import { spawn as spawn3 } from "child_process";
|
|
|
12910
13370
|
import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
|
|
12911
13371
|
import { homedir as homedir9 } from "os";
|
|
12912
13372
|
import { join as join13 } from "path";
|
|
12913
|
-
var
|
|
13373
|
+
var log26 = forComponent("remote-trigger");
|
|
12914
13374
|
var TRIGGER_DIR = () => join13(homedir9(), ".agent", "triggers");
|
|
12915
13375
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
|
|
12916
13376
|
var TriggerServer = class {
|
|
@@ -12948,13 +13408,13 @@ var TriggerServer = class {
|
|
|
12948
13408
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
12949
13409
|
}
|
|
12950
13410
|
});
|
|
12951
|
-
conn.on("error", (e) =>
|
|
13411
|
+
conn.on("error", (e) => log26.debug("trigger conn error", e));
|
|
12952
13412
|
});
|
|
12953
|
-
this.server.on("error", (e) =>
|
|
13413
|
+
this.server.on("error", (e) => log26.debug("trigger server error", e));
|
|
12954
13414
|
this.server.listen(p);
|
|
12955
13415
|
this.path = p;
|
|
12956
13416
|
} catch (e) {
|
|
12957
|
-
|
|
13417
|
+
log26.debug("trigger server unavailable", e);
|
|
12958
13418
|
}
|
|
12959
13419
|
}
|
|
12960
13420
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -13076,7 +13536,7 @@ var strike = C("9");
|
|
|
13076
13536
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
13077
13537
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
13078
13538
|
var err = (s) => process.stderr.write(s);
|
|
13079
|
-
var
|
|
13539
|
+
var log27 = forComponent("cli");
|
|
13080
13540
|
var VERSION = (() => {
|
|
13081
13541
|
try {
|
|
13082
13542
|
return JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -13126,9 +13586,9 @@ var spinner = /* @__PURE__ */ (() => {
|
|
|
13126
13586
|
if (!tty || timer) return;
|
|
13127
13587
|
t0 = this.turnStart || Date.now();
|
|
13128
13588
|
timer = setInterval(() => {
|
|
13129
|
-
const
|
|
13589
|
+
const secs2 = Math.round((Date.now() - t0) / 1e3);
|
|
13130
13590
|
const tail = this.tail?.();
|
|
13131
|
-
err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${
|
|
13591
|
+
err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs2 ? `${secs2}s \xB7 ` : ""}esc to interrupt`) + (tail ? " " + tail : ""));
|
|
13132
13592
|
}, 90);
|
|
13133
13593
|
},
|
|
13134
13594
|
stop() {
|
|
@@ -13821,7 +14281,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
|
|
|
13821
14281
|
function turnCost(model, usage) {
|
|
13822
14282
|
return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
|
|
13823
14283
|
}
|
|
13824
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
14284
|
+
async function evaluateGoal(ai, condition, transcript, log28) {
|
|
13825
14285
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
13826
14286
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
13827
14287
|
return text.slice(0, 600);
|
|
@@ -13841,7 +14301,7 @@ ${recent}` }
|
|
|
13841
14301
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
13842
14302
|
if (match) return JSON.parse(match[0]);
|
|
13843
14303
|
} catch (e) {
|
|
13844
|
-
|
|
14304
|
+
log28(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
13845
14305
|
`));
|
|
13846
14306
|
}
|
|
13847
14307
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -14009,7 +14469,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
|
|
|
14009
14469
|
anthropicWebSearch: cfg.anthropicWebSearch
|
|
14010
14470
|
};
|
|
14011
14471
|
}
|
|
14012
|
-
async function makeAgent(args, ai, cfg, extraTools = []) {
|
|
14472
|
+
async function makeAgent(args, ai, cfg, extraTools = [], hostMcpServers) {
|
|
14013
14473
|
const virtual = args.vfs || !!args.boddb;
|
|
14014
14474
|
if (virtual && args.shell) err(yellow(` \u26A0 --shell ignored in ${args.boddb ? "--boddb" : "--vfs sandbox"} (a real shell would escape to disk)
|
|
14015
14475
|
`));
|
|
@@ -14023,7 +14483,7 @@ async function makeAgent(args, ai, cfg, extraTools = []) {
|
|
|
14023
14483
|
else if (args.shell === false) err(dim(" \u2296 --no-shell: VFS bash only (no real /bin/sh)\n"));
|
|
14024
14484
|
if (args.harden && !virtual) err(dim(` \u26E8 hardened shell: writes confined to cwd+tmp${args.hardenNet ? "" : ", network blocked"} (sandbox-exec/bwrap)
|
|
14025
14485
|
`));
|
|
14026
|
-
const opts = optsFor(args, ai, cfg, extraTools);
|
|
14486
|
+
const opts = { ...optsFor(args, ai, cfg, extraTools), hostMcpServers };
|
|
14027
14487
|
const agent = await buildAgent(opts);
|
|
14028
14488
|
const display = displayHooks(agent.options.fs, { flush: agent.options.host?.flushText });
|
|
14029
14489
|
agent.options.hooks = cfg.hooks ? composeHooks(display, hooksFromConfig(cfg.hooks)) : display;
|
|
@@ -14062,7 +14522,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
14062
14522
|
return tools;
|
|
14063
14523
|
}
|
|
14064
14524
|
async function closeMcp(mounted) {
|
|
14065
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
14525
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log27.debug("mcp close failed", e))));
|
|
14066
14526
|
}
|
|
14067
14527
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
14068
14528
|
function mentionRefs(line) {
|
|
@@ -14114,7 +14574,7 @@ async function expandMentions(fs, line) {
|
|
|
14114
14574
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
14115
14575
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
14116
14576
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
14117
|
-
|
|
14577
|
+
log27.debug("mcp mention resolve failed", e);
|
|
14118
14578
|
return null;
|
|
14119
14579
|
});
|
|
14120
14580
|
if (body != null) {
|
|
@@ -14201,7 +14661,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
14201
14661
|
try {
|
|
14202
14662
|
store.save(session);
|
|
14203
14663
|
} catch (ex) {
|
|
14204
|
-
|
|
14664
|
+
log27.debug("mid-turn session flush failed", ex);
|
|
14205
14665
|
}
|
|
14206
14666
|
}
|
|
14207
14667
|
return origNotify(e);
|
|
@@ -14228,7 +14688,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
14228
14688
|
}
|
|
14229
14689
|
agent.options.host?.flushText?.();
|
|
14230
14690
|
flushLookupGroup();
|
|
14231
|
-
const
|
|
14691
|
+
const secs2 = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
14232
14692
|
const cost = turnCost(agent.options.model, res.usage);
|
|
14233
14693
|
const m = res.usageEstimated ? "~" : "";
|
|
14234
14694
|
const tok = res.usage?.totalTokens ? `${m}${(res.usage.totalTokens / 1e3).toFixed(1)}k tok \xB7 ${cost > 0 ? m + fmtUsd(cost) + " \xB7 " : ""}` : "";
|
|
@@ -14239,7 +14699,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
14239
14699
|
const didWork = agent.transcript.slice(msgsBefore).some((m2) => m2.role === "assistant" || m2.role === "tool");
|
|
14240
14700
|
const silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens && !didWork;
|
|
14241
14701
|
if (!silentAbort)
|
|
14242
|
-
err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : res.finishReason === "needs_input" ? yellow(" ? needs input") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${
|
|
14702
|
+
err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : res.finishReason === "needs_input" ? yellow(" ? needs input") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs2}s \xB7 ${shortId}
|
|
14243
14703
|
`));
|
|
14244
14704
|
if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
|
|
14245
14705
|
if (res.finishReason === "error" && res.error) {
|
|
@@ -14350,14 +14810,14 @@ var isCancelTeardown = (e) => {
|
|
|
14350
14810
|
function installCancelGuards(mounted) {
|
|
14351
14811
|
process.on("unhandledRejection", (e) => {
|
|
14352
14812
|
if (isCancelTeardown(e)) {
|
|
14353
|
-
|
|
14813
|
+
log27.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
14354
14814
|
return;
|
|
14355
14815
|
}
|
|
14356
|
-
|
|
14816
|
+
log27.error("unhandledRejection", e);
|
|
14357
14817
|
});
|
|
14358
14818
|
process.on("uncaughtException", (e) => {
|
|
14359
14819
|
if (isCancelTeardown(e)) {
|
|
14360
|
-
|
|
14820
|
+
log27.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
14361
14821
|
return;
|
|
14362
14822
|
}
|
|
14363
14823
|
console.error(e);
|
|
@@ -14371,7 +14831,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
14371
14831
|
let mcpToolNames = [];
|
|
14372
14832
|
const initialMcpTools = mcpAgentTools(mounted);
|
|
14373
14833
|
mcpToolNames = initialMcpTools.map((t) => t.name);
|
|
14374
|
-
const agent = await makeAgent(args, ai, cfg, initialMcpTools);
|
|
14834
|
+
const agent = await makeAgent(args, ai, cfg, initialMcpTools, mounted.map((m) => m.name));
|
|
14375
14835
|
if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
|
|
14376
14836
|
exitRequested = true;
|
|
14377
14837
|
})];
|
|
@@ -14613,7 +15073,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
14613
15073
|
...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
|
|
14614
15074
|
actModel: agent.options.model,
|
|
14615
15075
|
actOptions: workerOptions,
|
|
14616
|
-
providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers),
|
|
15076
|
+
providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name)),
|
|
14617
15077
|
...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
|
|
14618
15078
|
host,
|
|
14619
15079
|
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true, emotionTags: emotionsOn } : {},
|
|
@@ -14695,7 +15155,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
14695
15155
|
const setModel = (m) => {
|
|
14696
15156
|
work.model = m;
|
|
14697
15157
|
work.contextWindow = getModelInfo(m)?.contextWindow ?? 0;
|
|
14698
|
-
work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers);
|
|
15158
|
+
work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name));
|
|
14699
15159
|
if (dx) dx.options.actModel = m;
|
|
14700
15160
|
persistModel(cwd, m);
|
|
14701
15161
|
err(dim(" model \u2192 " + m + "\n"));
|
|
@@ -14762,7 +15222,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
14762
15222
|
try {
|
|
14763
15223
|
mkdirSync12(dir, { recursive: true });
|
|
14764
15224
|
} catch (e) {
|
|
14765
|
-
|
|
15225
|
+
log27.debug("paste image dir create failed", e);
|
|
14766
15226
|
}
|
|
14767
15227
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
14768
15228
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -14875,7 +15335,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
14875
15335
|
mkdirSync12(join14(cwd, ".agent"), { recursive: true });
|
|
14876
15336
|
appendFileSync(histPath, line + "\n");
|
|
14877
15337
|
} catch (e) {
|
|
14878
|
-
|
|
15338
|
+
log27.debug("history write failed", e);
|
|
14879
15339
|
}
|
|
14880
15340
|
};
|
|
14881
15341
|
const ago = (t) => {
|
|
@@ -14947,7 +15407,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
14947
15407
|
try {
|
|
14948
15408
|
store.save(session);
|
|
14949
15409
|
} catch (e) {
|
|
14950
|
-
|
|
15410
|
+
log27.debug("session save after rewind failed", e);
|
|
14951
15411
|
}
|
|
14952
15412
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
14953
15413
|
`));
|
|
@@ -14981,7 +15441,7 @@ ${task}`;
|
|
|
14981
15441
|
bangContext.length = 0;
|
|
14982
15442
|
}
|
|
14983
15443
|
const delta = await refreshCatalogs().catch((e) => {
|
|
14984
|
-
|
|
15444
|
+
log27.debug("catalog refresh failed", e);
|
|
14985
15445
|
return "";
|
|
14986
15446
|
});
|
|
14987
15447
|
if (delta) {
|
|
@@ -15245,7 +15705,7 @@ ${task}`;
|
|
|
15245
15705
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
15246
15706
|
run: async () => {
|
|
15247
15707
|
await refreshCatalogs().catch((e) => {
|
|
15248
|
-
|
|
15708
|
+
log27.debug("catalog refresh failed", e);
|
|
15249
15709
|
});
|
|
15250
15710
|
face.reprepare();
|
|
15251
15711
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -15747,7 +16207,7 @@ ${task}`;
|
|
|
15747
16207
|
try {
|
|
15748
16208
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
15749
16209
|
} catch (e) {
|
|
15750
|
-
|
|
16210
|
+
log27.debug(`loadAgents(${d}) failed`, e);
|
|
15751
16211
|
}
|
|
15752
16212
|
}
|
|
15753
16213
|
if (!seen.size) {
|
|
@@ -15835,7 +16295,7 @@ ${task}`;
|
|
|
15835
16295
|
}
|
|
15836
16296
|
if (idx >= 0) {
|
|
15837
16297
|
const old = mounted.splice(idx, 1)[0];
|
|
15838
|
-
await old.client.close().catch((e) =>
|
|
16298
|
+
await old.client.close().catch((e) => log27.debug("mcp close failed", e));
|
|
15839
16299
|
}
|
|
15840
16300
|
try {
|
|
15841
16301
|
const m = await mountMcpServer(name, conf);
|
|
@@ -15863,7 +16323,7 @@ ${task}`;
|
|
|
15863
16323
|
}
|
|
15864
16324
|
const m = mounted.splice(idx, 1)[0];
|
|
15865
16325
|
remountMcpTools();
|
|
15866
|
-
await m.client.close().catch((e) =>
|
|
16326
|
+
await m.client.close().catch((e) => log27.debug("mcp close failed", e));
|
|
15867
16327
|
err(dim(` removed "${name}"
|
|
15868
16328
|
`));
|
|
15869
16329
|
return;
|
|
@@ -16066,16 +16526,16 @@ ${task}`;
|
|
|
16066
16526
|
try {
|
|
16067
16527
|
return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
16068
16528
|
} catch (e) {
|
|
16069
|
-
|
|
16529
|
+
log27.debug("completion readdir failed", absDir, e);
|
|
16070
16530
|
return null;
|
|
16071
16531
|
}
|
|
16072
16532
|
};
|
|
16073
16533
|
const agentCommandNames = () => [...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
|
|
16074
16534
|
const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...agentCommandNames()];
|
|
16075
|
-
const
|
|
16535
|
+
const describe2 = (hit) => hit.startsWith("/") ? builtins[hit.slice(1)]?.desc ?? cmds.find((c) => c.name === hit.slice(1))?.description ?? skills.find((s) => s.name === hit.slice(1))?.description : void 0;
|
|
16076
16536
|
const suggest = (lineToCursor) => {
|
|
16077
16537
|
const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), agentCommands: agentCommandNames(), listDir, bangHistory: history });
|
|
16078
|
-
return { hits, token, describe };
|
|
16538
|
+
return { hits, token, describe: describe2 };
|
|
16079
16539
|
};
|
|
16080
16540
|
const editor = createLineEditor(process.stderr);
|
|
16081
16541
|
editorRef = editor;
|
|
@@ -16202,7 +16662,7 @@ ${out}
|
|
|
16202
16662
|
return;
|
|
16203
16663
|
}
|
|
16204
16664
|
await refreshCatalogs().catch((e) => {
|
|
16205
|
-
|
|
16665
|
+
log27.debug("catalog refresh failed", e);
|
|
16206
16666
|
});
|
|
16207
16667
|
const sk = skills.find((x) => x.name === name);
|
|
16208
16668
|
if (sk) {
|
|
@@ -16690,7 +17150,7 @@ async function main() {
|
|
|
16690
17150
|
});
|
|
16691
17151
|
if (args.task) {
|
|
16692
17152
|
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join14(cwd, ".agent", "mcp-auth.json") }));
|
|
16693
|
-
const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted));
|
|
17153
|
+
const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted), mounted.map((m) => m.name));
|
|
16694
17154
|
const store = new SessionStore(cwd);
|
|
16695
17155
|
const session = startSession(args, store, agent, cwd);
|
|
16696
17156
|
process.once("SIGINT", () => activeTurn?.abort());
|