@livx.cc/agentx 0.99.46 → 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-OcAg2yxA.d.ts → Agent-BeoEFl2T.d.ts} +22 -7
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +904 -417
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +712 -304
- 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,12 +4367,18 @@ 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
|
-
|
|
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();
|
|
4378
|
+
toolHoldMs = 0;
|
|
4379
|
+
/** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
|
|
4380
|
+
* from the pre-step transcript, which would re-run it — so such a stall is not retried. */
|
|
4381
|
+
toolRanThisAttempt = false;
|
|
4047
4382
|
/** Resets the active request's stall timer; a host tool's completion restarts the model's idle window. */
|
|
4048
4383
|
stallPoke;
|
|
4049
4384
|
/** Advertised tools this run's text claimed to have used while the ledger showed zero calls. */
|
|
@@ -4088,6 +4423,7 @@ var Agent = class _Agent {
|
|
|
4088
4423
|
addTools(tools) {
|
|
4089
4424
|
this.injectedTools.push(...tools);
|
|
4090
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));
|
|
4091
4427
|
}
|
|
4092
4428
|
/** Remove tools by name from a running agent. Returns the count removed. Also filters
|
|
4093
4429
|
* `options.tools` — otherwise a constructor-passed tool resurrects on the next prepare()
|
|
@@ -4139,6 +4475,11 @@ var Agent = class _Agent {
|
|
|
4139
4475
|
}
|
|
4140
4476
|
constructor(options) {
|
|
4141
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
|
+
};
|
|
4142
4483
|
if (this.options.fs) this.buildCtx();
|
|
4143
4484
|
}
|
|
4144
4485
|
/** Build the tool context from the resolved fs + options. Idempotent-safe: called once (ctor or ensureFs). */
|
|
@@ -4150,7 +4491,8 @@ var Agent = class _Agent {
|
|
|
4150
4491
|
this.ctx.ai = this.options.ai;
|
|
4151
4492
|
this.ctx.model = this.options.model;
|
|
4152
4493
|
this.ctx.parkHuman = (p) => this.park(p);
|
|
4153
|
-
|
|
4494
|
+
this.ctx.toolJobs = this.jobs;
|
|
4495
|
+
if (this.options.backgroundJobs) this.ctx.jobs = this.jobs;
|
|
4154
4496
|
}
|
|
4155
4497
|
/**
|
|
4156
4498
|
* Resolve the filesystem + build the tool context if the ctor couldn't (no `fs` was passed). The disk
|
|
@@ -4166,7 +4508,7 @@ var Agent = class _Agent {
|
|
|
4166
4508
|
const disk = new NodeDiskFilesystem2(process.cwd());
|
|
4167
4509
|
await disk.init();
|
|
4168
4510
|
this.options.fs = new JailedFilesystem2(disk);
|
|
4169
|
-
|
|
4511
|
+
log7.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
|
|
4170
4512
|
}
|
|
4171
4513
|
this.buildCtx();
|
|
4172
4514
|
}
|
|
@@ -4236,7 +4578,7 @@ var Agent = class _Agent {
|
|
|
4236
4578
|
tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
|
|
4237
4579
|
}
|
|
4238
4580
|
if (o.checkpoints) tools = [...tools, ...checkpointTools()];
|
|
4239
|
-
if (this.ctx.jobs) tools = [...tools, ...makeJobTools(this.
|
|
4581
|
+
if (this.ctx.jobs || [...tools, ...this.injectedTools].some(isMcpTool)) tools = [...tools, ...makeJobTools(this.jobs)];
|
|
4240
4582
|
const plan = o.planMode ? planMode({ host: o.host }) : void 0;
|
|
4241
4583
|
if (plan) tools = [...tools, plan.tool];
|
|
4242
4584
|
this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
|
|
@@ -4352,8 +4694,8 @@ var Agent = class _Agent {
|
|
|
4352
4694
|
let closureNudged = false;
|
|
4353
4695
|
let lastProgressAt = start;
|
|
4354
4696
|
const kill = (finishReason) => {
|
|
4355
|
-
|
|
4356
|
-
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);
|
|
4357
4699
|
const claims = this.survivingClaims();
|
|
4358
4700
|
return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
|
|
4359
4701
|
};
|
|
@@ -4374,22 +4716,27 @@ var Agent = class _Agent {
|
|
|
4374
4716
|
const hostToolTimeoutMs = o.providerOptions?.toolTimeoutMs;
|
|
4375
4717
|
const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
|
|
4376
4718
|
const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
|
|
4719
|
+
this.toolHoldMs = effectiveToolTimeoutMs;
|
|
4377
4720
|
if (isCursorWithTools) {
|
|
4378
4721
|
if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
|
|
4379
|
-
|
|
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.`);
|
|
4380
4723
|
}
|
|
4381
|
-
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs))
|
|
4724
|
+
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
|
|
4382
4725
|
}
|
|
4383
4726
|
const cursorPo = isCursorWithTools ? {
|
|
4384
4727
|
toolTimeoutMs: effectiveToolTimeoutMs,
|
|
4385
4728
|
toolExecutor: async (name, args) => {
|
|
4386
4729
|
const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
|
|
4387
|
-
this.
|
|
4388
|
-
this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
|
|
4730
|
+
const job = this.jobs.track({ kind: "host", label: name, holds: true, hardCapMs: effectiveToolTimeoutMs });
|
|
4389
4731
|
try {
|
|
4390
|
-
|
|
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;
|
|
4391
4738
|
} finally {
|
|
4392
|
-
this.
|
|
4739
|
+
this.toolRanThisAttempt = true;
|
|
4393
4740
|
this.stallPoke?.();
|
|
4394
4741
|
}
|
|
4395
4742
|
}
|
|
@@ -4402,6 +4749,9 @@ var Agent = class _Agent {
|
|
|
4402
4749
|
};
|
|
4403
4750
|
try {
|
|
4404
4751
|
for (let attempt = 0; ; attempt++) {
|
|
4752
|
+
for (const [, h] of this.nativeJobs) if (h.status === "running") this.jobs.kill(h.id, "step attempt ended");
|
|
4753
|
+
this.nativeJobs.clear();
|
|
4754
|
+
this.toolRanThisAttempt = false;
|
|
4405
4755
|
try {
|
|
4406
4756
|
if (useStream) {
|
|
4407
4757
|
const stall = this.armStallWatchdog(o.signal, o.stallMs);
|
|
@@ -4431,10 +4781,12 @@ var Agent = class _Agent {
|
|
|
4431
4781
|
const stalled = err2?.code === "stall";
|
|
4432
4782
|
const empty = err2?.code === "empty" || err2?.code === "cursor_silent";
|
|
4433
4783
|
const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
|
|
4434
|
-
const
|
|
4784
|
+
const replaysTool = stalled && this.toolRanThisAttempt;
|
|
4785
|
+
if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
|
|
4786
|
+
const transient = !o.signal?.aborted && !isAbortError(err2) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
|
|
4435
4787
|
if (!transient) throw err2;
|
|
4436
4788
|
const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
|
|
4437
|
-
|
|
4789
|
+
log7.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
|
|
4438
4790
|
o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
|
|
4439
4791
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
4440
4792
|
}
|
|
@@ -4442,12 +4794,12 @@ var Agent = class _Agent {
|
|
|
4442
4794
|
} catch (err2) {
|
|
4443
4795
|
if (err2?.code === "budget") return kill("budget");
|
|
4444
4796
|
if (err2?.code === "stall") {
|
|
4445
|
-
|
|
4797
|
+
log7.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
|
|
4446
4798
|
return { ...kill("timeout"), error: err2 };
|
|
4447
4799
|
}
|
|
4448
4800
|
if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
|
|
4449
4801
|
if (err2?.code === "empty" || err2?.code === "cursor_silent") {
|
|
4450
|
-
|
|
4802
|
+
log7.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
|
|
4451
4803
|
return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err2 };
|
|
4452
4804
|
}
|
|
4453
4805
|
const body = err2?.body ?? err2?.response?.data ?? err2?.error;
|
|
@@ -4459,7 +4811,7 @@ var Agent = class _Agent {
|
|
|
4459
4811
|
}
|
|
4460
4812
|
if (bodyStr && err2 instanceof Error && !err2.message.includes(bodyStr)) err2.detail = bodyStr;
|
|
4461
4813
|
const errLog = err2 instanceof Error ? { message: err2.message, ...err2.detail ? { detail: err2.detail } : {}, stack: err2.stack } : err2;
|
|
4462
|
-
|
|
4814
|
+
log7.error(`chat() failed: ${err2?.message ?? err2}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
|
|
4463
4815
|
return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err2 };
|
|
4464
4816
|
}
|
|
4465
4817
|
if (o.signal?.aborted) return kill("aborted");
|
|
@@ -4515,21 +4867,21 @@ var Agent = class _Agent {
|
|
|
4515
4867
|
if (emptyTurn && o.failEmptyTurns) {
|
|
4516
4868
|
if (!emptyNudged && canNudge()) {
|
|
4517
4869
|
emptyNudged = true;
|
|
4518
|
-
|
|
4870
|
+
log7.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
|
|
4519
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." });
|
|
4520
4872
|
continue;
|
|
4521
4873
|
}
|
|
4522
|
-
|
|
4874
|
+
log7.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
|
|
4523
4875
|
const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
|
|
4524
4876
|
e.code = "empty";
|
|
4525
4877
|
return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
|
|
4526
4878
|
}
|
|
4527
4879
|
const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
|
|
4528
4880
|
for (const n of fabricated) this.fabricatedClaims.add(n);
|
|
4529
|
-
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`);
|
|
4530
4882
|
if (fabricated.length && canNudge()) {
|
|
4531
4883
|
closureNudged = true;
|
|
4532
|
-
|
|
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`);
|
|
4533
4885
|
this.transcript.push({
|
|
4534
4886
|
role: "user",
|
|
4535
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.`
|
|
@@ -4538,15 +4890,18 @@ var Agent = class _Agent {
|
|
|
4538
4890
|
}
|
|
4539
4891
|
if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
|
|
4540
4892
|
closureNudged = true;
|
|
4541
|
-
|
|
4893
|
+
log7.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
|
|
4542
4894
|
this.transcript.push({
|
|
4543
4895
|
role: "user",
|
|
4544
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."
|
|
4545
4897
|
});
|
|
4546
4898
|
continue;
|
|
4547
4899
|
}
|
|
4548
|
-
|
|
4549
|
-
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;
|
|
4550
4905
|
this.releaseStaleImages();
|
|
4551
4906
|
await this.activeHooks?.onStop?.(res.content ?? "");
|
|
4552
4907
|
const claims = this.survivingClaims();
|
|
@@ -4572,11 +4927,12 @@ var Agent = class _Agent {
|
|
|
4572
4927
|
}
|
|
4573
4928
|
this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
|
|
4574
4929
|
}
|
|
4930
|
+
lastProgressAt = Date.now();
|
|
4575
4931
|
if (this.ctx.needsInput) {
|
|
4576
4932
|
const question = this.ctx.needsInput;
|
|
4577
4933
|
this.ctx.needsInput = void 0;
|
|
4578
|
-
|
|
4579
|
-
await this.
|
|
4934
|
+
log7.warn(`needs_input: ${question.question} (steps=${steps})`);
|
|
4935
|
+
await this.jobs.drain();
|
|
4580
4936
|
const text = [
|
|
4581
4937
|
`OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
|
|
4582
4938
|
...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
|
|
@@ -4614,7 +4970,9 @@ var Agent = class _Agent {
|
|
|
4614
4970
|
if (!stallMs) return;
|
|
4615
4971
|
if (timer) clearTimeout(timer);
|
|
4616
4972
|
timer = setTimeout(() => {
|
|
4617
|
-
if (this.
|
|
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(", ")})` : ""));
|
|
4618
4976
|
fired = true;
|
|
4619
4977
|
ac.abort(stallError());
|
|
4620
4978
|
}, stallMs);
|
|
@@ -4634,6 +4992,12 @@ var Agent = class _Agent {
|
|
|
4634
4992
|
reclassify: (err2) => stalled() ? stallError() : err2
|
|
4635
4993
|
};
|
|
4636
4994
|
}
|
|
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. */
|
|
4997
|
+
toolHoldActive() {
|
|
4998
|
+
this.jobs.reap();
|
|
4999
|
+
return this.jobs.holding().length > 0;
|
|
5000
|
+
}
|
|
4637
5001
|
/** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
|
|
4638
5002
|
* non-result the model produced by going silent. Callers retry it rather than report "done". */
|
|
4639
5003
|
isEmptyStop(res) {
|
|
@@ -4672,6 +5036,18 @@ var Agent = class _Agent {
|
|
|
4672
5036
|
if (a.output !== void 0) prev.output = a.output;
|
|
4673
5037
|
prev.status = a.status;
|
|
4674
5038
|
}
|
|
5039
|
+
const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
|
|
5040
|
+
if (a.status === "running") {
|
|
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 }));
|
|
5043
|
+
} else {
|
|
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
|
+
}
|
|
5049
|
+
this.toolRanThisAttempt = true;
|
|
5050
|
+
}
|
|
4675
5051
|
sawTool = true;
|
|
4676
5052
|
this.invokedTools.add(a.name);
|
|
4677
5053
|
trailing = "";
|
|
@@ -4760,13 +5136,13 @@ var Agent = class _Agent {
|
|
|
4760
5136
|
const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
|
|
4761
5137
|
if (decision?.block) {
|
|
4762
5138
|
const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
|
|
4763
|
-
|
|
5139
|
+
log7.debug(`${tc.function.name} -> ${blocked}`);
|
|
4764
5140
|
await hooks?.postToolUse?.(call, blocked, meta);
|
|
4765
5141
|
return blocked;
|
|
4766
5142
|
}
|
|
4767
5143
|
this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
|
|
4768
5144
|
if (earlyError) {
|
|
4769
|
-
|
|
5145
|
+
log7.debug(`${tc.function.name} -> ${earlyError}`);
|
|
4770
5146
|
await hooks?.postToolUse?.(call, earlyError, meta);
|
|
4771
5147
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
|
|
4772
5148
|
return earlyError;
|
|
@@ -4775,12 +5151,12 @@ var Agent = class _Agent {
|
|
|
4775
5151
|
let images;
|
|
4776
5152
|
let threw = false;
|
|
4777
5153
|
try {
|
|
4778
|
-
|
|
5154
|
+
log7.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
4779
5155
|
this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
|
|
4780
5156
|
try {
|
|
4781
5157
|
hooks.onToolOutput(call, chunk, meta);
|
|
4782
5158
|
} catch (e) {
|
|
4783
|
-
|
|
5159
|
+
log7.debug(`onToolOutput hook error: ${e}`);
|
|
4784
5160
|
}
|
|
4785
5161
|
} : void 0;
|
|
4786
5162
|
const raw = await tool.run(args, this.ctx);
|
|
@@ -4792,7 +5168,7 @@ var Agent = class _Agent {
|
|
|
4792
5168
|
}
|
|
4793
5169
|
} catch (e) {
|
|
4794
5170
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4795
|
-
|
|
5171
|
+
log7.debug(`${tc.function.name} -> error: ${msg}`);
|
|
4796
5172
|
result = `Error: ${msg}`;
|
|
4797
5173
|
threw = true;
|
|
4798
5174
|
} finally {
|
|
@@ -4884,6 +5260,7 @@ function estimateTokens(m, keepRecentImages = 1) {
|
|
|
4884
5260
|
return Math.ceil(sendBytes(m, keepRecentImages) / 4);
|
|
4885
5261
|
}
|
|
4886
5262
|
var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
|
|
5263
|
+
var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
|
|
4887
5264
|
function transportHeadroomMs(t) {
|
|
4888
5265
|
const d = Number(t.maxDurationMs) || 0;
|
|
4889
5266
|
if (!d) return 0;
|
|
@@ -5059,7 +5436,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
|
|
|
5059
5436
|
const ids = callIdSet(messages.slice(from));
|
|
5060
5437
|
while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
|
|
5061
5438
|
if (total > maxTokens)
|
|
5062
|
-
|
|
5439
|
+
log7.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
|
|
5063
5440
|
return [...head, ...messages.slice(from)];
|
|
5064
5441
|
}
|
|
5065
5442
|
function compact(m, max, focus) {
|
|
@@ -5364,6 +5741,7 @@ init_OverlayFilesystem();
|
|
|
5364
5741
|
|
|
5365
5742
|
// src/index.ts
|
|
5366
5743
|
init_tools();
|
|
5744
|
+
init_tools_jobs();
|
|
5367
5745
|
|
|
5368
5746
|
// src/scheduler.ts
|
|
5369
5747
|
function parseCronField(field, min, max) {
|
|
@@ -5636,7 +6014,7 @@ init_tools_web();
|
|
|
5636
6014
|
init_tools();
|
|
5637
6015
|
init_tools_structured();
|
|
5638
6016
|
init_logging();
|
|
5639
|
-
var
|
|
6017
|
+
var log8 = forComponent("scratch");
|
|
5640
6018
|
var SCRATCH_DIR = "/scratch";
|
|
5641
6019
|
function shortArgs(args) {
|
|
5642
6020
|
try {
|
|
@@ -5676,7 +6054,7 @@ var Scratch = class {
|
|
|
5676
6054
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5677
6055
|
await this.fs.writeFile(path, header + raw);
|
|
5678
6056
|
} catch (e) {
|
|
5679
|
-
|
|
6057
|
+
log8.debug("scratch write failed; returning raw", e);
|
|
5680
6058
|
return raw;
|
|
5681
6059
|
}
|
|
5682
6060
|
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
@@ -5706,7 +6084,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
|
|
|
5706
6084
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5707
6085
|
await this.fs.writeFile(path, header + full);
|
|
5708
6086
|
} catch (e) {
|
|
5709
|
-
|
|
6087
|
+
log8.debug("scratch spill failed; cropping lossy", e);
|
|
5710
6088
|
return full.slice(0, pageBytes) + `
|
|
5711
6089
|
|
|
5712
6090
|
[output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
|
|
@@ -5752,7 +6130,7 @@ Question: ${q2}`);
|
|
|
5752
6130
|
const answer = (res.text ?? "").trim();
|
|
5753
6131
|
return answer || "(no answer found in scratch)";
|
|
5754
6132
|
} catch (e) {
|
|
5755
|
-
|
|
6133
|
+
log8.debug("Ask peek failed", e);
|
|
5756
6134
|
return `Error querying scratch: ${e?.message ?? e}`;
|
|
5757
6135
|
}
|
|
5758
6136
|
}
|
|
@@ -5761,7 +6139,7 @@ Question: ${q2}`);
|
|
|
5761
6139
|
|
|
5762
6140
|
// src/lessons.ts
|
|
5763
6141
|
init_logging();
|
|
5764
|
-
var
|
|
6142
|
+
var log9 = forComponent("Lessons");
|
|
5765
6143
|
var LessonOptionsDefaults = class {
|
|
5766
6144
|
minRepeats = 2;
|
|
5767
6145
|
};
|
|
@@ -5786,8 +6164,8 @@ function lessonCapture(options) {
|
|
|
5786
6164
|
counts.set(lesson.slug, n);
|
|
5787
6165
|
if (n < o.minRepeats) return;
|
|
5788
6166
|
written.add(lesson.slug);
|
|
5789
|
-
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) =>
|
|
5790
|
-
|
|
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)`);
|
|
5791
6169
|
}
|
|
5792
6170
|
};
|
|
5793
6171
|
}
|
|
@@ -5795,7 +6173,7 @@ function lessonCapture(options) {
|
|
|
5795
6173
|
// src/reflect.ts
|
|
5796
6174
|
init_llm();
|
|
5797
6175
|
init_logging();
|
|
5798
|
-
var
|
|
6176
|
+
var log10 = forComponent("Reflect");
|
|
5799
6177
|
async function reflectOnRun(o) {
|
|
5800
6178
|
const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
|
|
5801
6179
|
if (!digest.trim()) return null;
|
|
@@ -5811,7 +6189,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5811
6189
|
const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
|
|
5812
6190
|
text = r?.content ?? "";
|
|
5813
6191
|
} catch (e) {
|
|
5814
|
-
|
|
6192
|
+
log10.warn(`reflection call failed: ${e?.message ?? e}`);
|
|
5815
6193
|
return null;
|
|
5816
6194
|
}
|
|
5817
6195
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
@@ -5821,10 +6199,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5821
6199
|
try {
|
|
5822
6200
|
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
5823
6201
|
} catch (e) {
|
|
5824
|
-
|
|
6202
|
+
log10.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
5825
6203
|
return null;
|
|
5826
6204
|
}
|
|
5827
|
-
|
|
6205
|
+
log10.debug(`reflection persisted ${slug2}`);
|
|
5828
6206
|
return slug2;
|
|
5829
6207
|
}
|
|
5830
6208
|
function digestRun(messages, maxChars) {
|
|
@@ -5873,7 +6251,7 @@ function forComponent2(name) {
|
|
|
5873
6251
|
}
|
|
5874
6252
|
|
|
5875
6253
|
// node_modules/@bod.ee/voice/src/core/emotion.ts
|
|
5876
|
-
var
|
|
6254
|
+
var log11 = forComponent2("Emotion");
|
|
5877
6255
|
var EMOTIONS = [
|
|
5878
6256
|
// primary (best results)
|
|
5879
6257
|
"neutral",
|
|
@@ -6005,7 +6383,7 @@ function renderEmotions(text, opts = { show: true }) {
|
|
|
6005
6383
|
speech += cartesiaTag(tag);
|
|
6006
6384
|
if (opts.show) display += m[0];
|
|
6007
6385
|
} else {
|
|
6008
|
-
|
|
6386
|
+
log11.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
6009
6387
|
}
|
|
6010
6388
|
last = m.index + m[0].length;
|
|
6011
6389
|
}
|
|
@@ -6047,7 +6425,7 @@ var EmotionStream = class {
|
|
|
6047
6425
|
if (tag) {
|
|
6048
6426
|
this.pending = tag;
|
|
6049
6427
|
if (this.show) display += m[0];
|
|
6050
|
-
} else
|
|
6428
|
+
} else log11.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
6051
6429
|
last = m.index + m[0].length;
|
|
6052
6430
|
}
|
|
6053
6431
|
this.emit(body.slice(last), (s, d, p) => {
|
|
@@ -6070,7 +6448,7 @@ var EmotionStream = class {
|
|
|
6070
6448
|
};
|
|
6071
6449
|
|
|
6072
6450
|
// node_modules/@bod.ee/voice/src/core/engine.ts
|
|
6073
|
-
var
|
|
6451
|
+
var log12 = forComponent2("VoiceEngine");
|
|
6074
6452
|
var realClock = {
|
|
6075
6453
|
now: () => performance.now(),
|
|
6076
6454
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
@@ -6345,7 +6723,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6345
6723
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
6346
6724
|
this.tts.warmup?.();
|
|
6347
6725
|
this.setState("listening");
|
|
6348
|
-
|
|
6726
|
+
log12.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
6349
6727
|
}
|
|
6350
6728
|
get usingAec() {
|
|
6351
6729
|
return this.stt.usingAec;
|
|
@@ -6367,7 +6745,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6367
6745
|
this.options.onDiag({ t: this.clock.now(), kind, ...fields });
|
|
6368
6746
|
} catch (e) {
|
|
6369
6747
|
this.diagOn = false;
|
|
6370
|
-
|
|
6748
|
+
log12.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
6371
6749
|
}
|
|
6372
6750
|
}
|
|
6373
6751
|
idleWaiters = [];
|
|
@@ -6476,7 +6854,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6476
6854
|
if (prose.trim()) this.repliedSinceDispatch = true;
|
|
6477
6855
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
6478
6856
|
this.tts.speak(forSpeech(speech), true);
|
|
6479
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
6857
|
+
if (!this.spokeDeltas && this.turnStartAt) log12.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
|
|
6480
6858
|
this.spokeDeltas = true;
|
|
6481
6859
|
this.setState("speaking");
|
|
6482
6860
|
if (this.options.revealMode !== "off" && display) {
|
|
@@ -6553,7 +6931,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6553
6931
|
this.drainTimer = null;
|
|
6554
6932
|
this.speaking = false;
|
|
6555
6933
|
this.stopWordReveal(true);
|
|
6556
|
-
if (this.turnStartAt)
|
|
6934
|
+
if (this.turnStartAt) log12.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
|
|
6557
6935
|
this.echoUntil = this.clock.now() + 2500;
|
|
6558
6936
|
if (!this.usingAec) this.stt.reset();
|
|
6559
6937
|
this.setState("listening");
|
|
@@ -6641,7 +7019,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6641
7019
|
this.clearAckTimer("interrupt");
|
|
6642
7020
|
this.adaptiveAckFired = false;
|
|
6643
7021
|
this.ackProbe = null;
|
|
6644
|
-
if (this.uttQueue.length)
|
|
7022
|
+
if (this.uttQueue.length) log12.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
|
|
6645
7023
|
const droppedQueued = this.uttQueue.length;
|
|
6646
7024
|
this.uttQueue = [];
|
|
6647
7025
|
if (!this.speaking && !this.drainTimer) return;
|
|
@@ -6787,7 +7165,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6787
7165
|
if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
|
|
6788
7166
|
this.specSpent = true;
|
|
6789
7167
|
this.specText = this.specPartial;
|
|
6790
|
-
|
|
7168
|
+
log12.debug(`speculate: "${this.specText.slice(0, 60)}"`);
|
|
6791
7169
|
this.diag("speculate", { text: this.specText });
|
|
6792
7170
|
this.options.onSpeculate(this.specText);
|
|
6793
7171
|
}, this.options.speculativeMs);
|
|
@@ -6856,7 +7234,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6856
7234
|
};
|
|
6857
7235
|
this.tts.newContext();
|
|
6858
7236
|
this.tts.speak(phrase, false);
|
|
6859
|
-
|
|
7237
|
+
log12.debug(`backchannel: "${phrase}"`);
|
|
6860
7238
|
this.diag("backchannel", { phrase });
|
|
6861
7239
|
this.options.onBackchannel(phrase);
|
|
6862
7240
|
}
|
|
@@ -6889,7 +7267,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6889
7267
|
});
|
|
6890
7268
|
if (idx < 0) return text;
|
|
6891
7269
|
toks.splice(idx, 1);
|
|
6892
|
-
|
|
7270
|
+
log12.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
|
|
6893
7271
|
return toks.join(" ");
|
|
6894
7272
|
}
|
|
6895
7273
|
/** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
|
|
@@ -6980,7 +7358,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6980
7358
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
6981
7359
|
if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
|
|
6982
7360
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
6983
|
-
|
|
7361
|
+
log12.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
6984
7362
|
this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
|
|
6985
7363
|
this.options.onHold();
|
|
6986
7364
|
if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
|
|
@@ -7015,13 +7393,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7015
7393
|
if (text) {
|
|
7016
7394
|
const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
7017
7395
|
if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
|
|
7018
|
-
|
|
7396
|
+
log12.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
|
|
7019
7397
|
this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
|
|
7020
7398
|
return;
|
|
7021
7399
|
}
|
|
7022
7400
|
const refinal = this.refinalizes(text);
|
|
7023
7401
|
if (refinal && this.repliedSinceDispatch) {
|
|
7024
|
-
|
|
7402
|
+
log12.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
|
|
7025
7403
|
this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
|
|
7026
7404
|
this.lastDispatchFlat = flat;
|
|
7027
7405
|
this.lastDispatchWords = text.trim().split(/\s+/);
|
|
@@ -7181,7 +7559,7 @@ var SpokenSplitter = class {
|
|
|
7181
7559
|
};
|
|
7182
7560
|
|
|
7183
7561
|
// node_modules/@bod.ee/voice/src/adapters/soniox.ts
|
|
7184
|
-
var
|
|
7562
|
+
var log13 = forComponent2("SonioxSTT");
|
|
7185
7563
|
var now = () => performance.now();
|
|
7186
7564
|
var SonioxSTTOptions = class {
|
|
7187
7565
|
auth = "";
|
|
@@ -7222,7 +7600,7 @@ var SonioxSTT = class {
|
|
|
7222
7600
|
this.onDiag({ t: now(), kind, ...fields });
|
|
7223
7601
|
} catch (e) {
|
|
7224
7602
|
this.diagOn = false;
|
|
7225
|
-
|
|
7603
|
+
log13.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7226
7604
|
}
|
|
7227
7605
|
}
|
|
7228
7606
|
lastChunkAt = 0;
|
|
@@ -7264,11 +7642,11 @@ var SonioxSTT = class {
|
|
|
7264
7642
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
7265
7643
|
this.ws.onclose = (ev) => {
|
|
7266
7644
|
if (this.stopped) return;
|
|
7267
|
-
|
|
7645
|
+
log13.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
7268
7646
|
this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
|
|
7269
7647
|
this.reset();
|
|
7270
7648
|
this.connectWs().catch((e) => {
|
|
7271
|
-
|
|
7649
|
+
log13.error(`soniox reconnect failed: ${e.message}`);
|
|
7272
7650
|
this.diag("stt_reconnect_failed", { message: e.message });
|
|
7273
7651
|
});
|
|
7274
7652
|
};
|
|
@@ -7280,7 +7658,7 @@ var SonioxSTT = class {
|
|
|
7280
7658
|
this.endpointTimer = setInterval(() => {
|
|
7281
7659
|
const combined = (this.finalText + this.partialText).trim();
|
|
7282
7660
|
if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
7283
|
-
if (this.firstTokenAt)
|
|
7661
|
+
if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
7284
7662
|
this.reset();
|
|
7285
7663
|
this.onUtterance(combined, now());
|
|
7286
7664
|
}, 120);
|
|
@@ -7292,7 +7670,7 @@ var SonioxSTT = class {
|
|
|
7292
7670
|
if (this.stopped) return;
|
|
7293
7671
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
7294
7672
|
if (now() - ref > noAudioMs) {
|
|
7295
|
-
|
|
7673
|
+
log13.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
7296
7674
|
this.diag("stt_watchdog_fatal", { noAudioMs });
|
|
7297
7675
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
7298
7676
|
this.stop();
|
|
@@ -7315,7 +7693,7 @@ var SonioxSTT = class {
|
|
|
7315
7693
|
handle(m) {
|
|
7316
7694
|
if (m.error_message) {
|
|
7317
7695
|
this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
|
|
7318
|
-
return
|
|
7696
|
+
return log13.error(`soniox: ${m.error_message}`);
|
|
7319
7697
|
}
|
|
7320
7698
|
let endpoint = false;
|
|
7321
7699
|
for (const t of m.tokens ?? []) {
|
|
@@ -7332,7 +7710,7 @@ var SonioxSTT = class {
|
|
|
7332
7710
|
this.onPartial(combined);
|
|
7333
7711
|
if (endpoint && this.finalText.trim()) {
|
|
7334
7712
|
const utterance = this.finalText.trim();
|
|
7335
|
-
if (this.firstTokenAt)
|
|
7713
|
+
if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
7336
7714
|
this.reset();
|
|
7337
7715
|
this.onUtterance(utterance, now());
|
|
7338
7716
|
}
|
|
@@ -7354,7 +7732,7 @@ var SonioxSTT = class {
|
|
|
7354
7732
|
};
|
|
7355
7733
|
|
|
7356
7734
|
// node_modules/@bod.ee/voice/src/adapters/cartesia.ts
|
|
7357
|
-
var
|
|
7735
|
+
var log14 = forComponent2("CartesiaTTS");
|
|
7358
7736
|
var now2 = () => performance.now();
|
|
7359
7737
|
var CartesiaTTSOptions = class {
|
|
7360
7738
|
auth = "";
|
|
@@ -7390,7 +7768,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7390
7768
|
this.onDiag({ t: now2(), kind, ...fields });
|
|
7391
7769
|
} catch (e) {
|
|
7392
7770
|
this.diagOn = false;
|
|
7393
|
-
|
|
7771
|
+
log14.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7394
7772
|
}
|
|
7395
7773
|
}
|
|
7396
7774
|
firstAudioAt = 0;
|
|
@@ -7425,11 +7803,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7425
7803
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
7426
7804
|
});
|
|
7427
7805
|
this.ws.onclose = (ev) => {
|
|
7428
|
-
|
|
7806
|
+
log14.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
7429
7807
|
this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
|
|
7430
7808
|
if (!this.closed) {
|
|
7431
7809
|
this.connecting = this.doConnect().catch((e) => {
|
|
7432
|
-
|
|
7810
|
+
log14.error(`cartesia reconnect failed: ${e.message}`);
|
|
7433
7811
|
this.diag("tts_reconnect_failed", { message: e.message });
|
|
7434
7812
|
});
|
|
7435
7813
|
}
|
|
@@ -7457,12 +7835,12 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7457
7835
|
this.down = true;
|
|
7458
7836
|
this.downAt = now2();
|
|
7459
7837
|
this.consecutiveOk = 0;
|
|
7460
|
-
|
|
7838
|
+
log14.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
7461
7839
|
this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
|
|
7462
7840
|
this.onDone();
|
|
7463
7841
|
this.startProbe();
|
|
7464
7842
|
} else if (!this.down) {
|
|
7465
|
-
(/No valid transcripts/i.test(m.message || "") ?
|
|
7843
|
+
(/No valid transcripts/i.test(m.message || "") ? log14.debug : log14.warn)(`cartesia: ${JSON.stringify(m)}`);
|
|
7466
7844
|
}
|
|
7467
7845
|
}
|
|
7468
7846
|
};
|
|
@@ -7477,7 +7855,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7477
7855
|
this.stopProbe();
|
|
7478
7856
|
const downMs = this.downAt ? now2() - this.downAt : 0;
|
|
7479
7857
|
this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
|
|
7480
|
-
(downMs < 2e3 ?
|
|
7858
|
+
(downMs < 2e3 ? log14.debug : log14.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
7481
7859
|
}
|
|
7482
7860
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
7483
7861
|
async ensureConnected() {
|
|
@@ -7568,7 +7946,7 @@ configureLogging((name) => {
|
|
|
7568
7946
|
});
|
|
7569
7947
|
|
|
7570
7948
|
// src/duplex.ts
|
|
7571
|
-
var
|
|
7949
|
+
var log15 = forComponent("DuplexAgent");
|
|
7572
7950
|
function describeCall(call) {
|
|
7573
7951
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
7574
7952
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -7779,7 +8157,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7779
8157
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
7780
8158
|
if (m) {
|
|
7781
8159
|
this.fabricationCut = true;
|
|
7782
|
-
|
|
8160
|
+
log15.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
7783
8161
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
7784
8162
|
if (!safe) return;
|
|
7785
8163
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -7874,7 +8252,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7874
8252
|
return {
|
|
7875
8253
|
preToolUse: (call) => {
|
|
7876
8254
|
if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
|
|
7877
|
-
|
|
8255
|
+
log15.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
|
|
7878
8256
|
this.abortSpeculation();
|
|
7879
8257
|
return { block: true, reason: "Speculative turn aborted." };
|
|
7880
8258
|
}
|
|
@@ -7912,7 +8290,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7912
8290
|
try {
|
|
7913
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.");
|
|
7914
8292
|
} catch (e) {
|
|
7915
|
-
|
|
8293
|
+
log15.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
7916
8294
|
} finally {
|
|
7917
8295
|
this.nudging = false;
|
|
7918
8296
|
}
|
|
@@ -8005,7 +8383,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8005
8383
|
try {
|
|
8006
8384
|
res = await this.voice.send(spec.text);
|
|
8007
8385
|
} catch (e) {
|
|
8008
|
-
|
|
8386
|
+
log15.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
|
|
8009
8387
|
} finally {
|
|
8010
8388
|
this.voice.options.signal = prevSignal;
|
|
8011
8389
|
}
|
|
@@ -8019,7 +8397,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8019
8397
|
if (d === "abort") {
|
|
8020
8398
|
if (this.voice.transcript.length > base) this.voice.transcript.length = base;
|
|
8021
8399
|
this.speculativeAbortedCalls++;
|
|
8022
|
-
|
|
8400
|
+
log15.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
|
|
8023
8401
|
if (this.spec === spec) this.spec = void 0;
|
|
8024
8402
|
return res ?? empty;
|
|
8025
8403
|
}
|
|
@@ -8284,7 +8662,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8284
8662
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
8285
8663
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
8286
8664
|
if (cres.finishReason !== "stop") {
|
|
8287
|
-
|
|
8665
|
+
log15.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
8288
8666
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
8289
8667
|
}
|
|
8290
8668
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -8420,7 +8798,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8420
8798
|
rec.status = "done";
|
|
8421
8799
|
rec.result = res.text;
|
|
8422
8800
|
const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
|
|
8423
|
-
|
|
8801
|
+
log15.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
8424
8802
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
8425
8803
|
id,
|
|
8426
8804
|
text: res.text,
|
|
@@ -8462,14 +8840,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8462
8840
|
foldIfSuperseded(rec, note) {
|
|
8463
8841
|
if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
|
|
8464
8842
|
if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
|
|
8465
|
-
|
|
8843
|
+
log15.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
|
|
8466
8844
|
return true;
|
|
8467
8845
|
}
|
|
8468
8846
|
failTask(rec, msg) {
|
|
8469
8847
|
this.dropAsk(rec.id);
|
|
8470
8848
|
rec.status = "error";
|
|
8471
8849
|
rec.result = msg;
|
|
8472
|
-
|
|
8850
|
+
log15.warn(`task ${rec.id} failed: ${msg}`);
|
|
8473
8851
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
8474
8852
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
8475
8853
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
@@ -8718,17 +9096,42 @@ async function withDeadline(p, ms) {
|
|
|
8718
9096
|
}
|
|
8719
9097
|
}
|
|
8720
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
|
+
}
|
|
8721
9127
|
function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
|
|
8722
9128
|
return {
|
|
8723
9129
|
name: `${prefix}${spec.name}`,
|
|
8724
9130
|
description: spec.description ?? `MCP tool ${spec.name}`,
|
|
8725
9131
|
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
8726
9132
|
maxDurationMs: timeoutMs,
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
|
|
8730
|
-
const r = toResult(raw);
|
|
8731
|
-
return r.images?.length ? r : r.text;
|
|
9133
|
+
run(args, ctx) {
|
|
9134
|
+
return runMcpCall(spec.name, (o) => callTool(spec.name, args ?? {}, o), ctx, timeoutMs);
|
|
8732
9135
|
}
|
|
8733
9136
|
};
|
|
8734
9137
|
}
|
|
@@ -8769,13 +9172,10 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
|
8769
9172
|
}
|
|
8770
9173
|
},
|
|
8771
9174
|
maxDurationMs: callTimeoutMs,
|
|
8772
|
-
async run({ name, args }) {
|
|
9175
|
+
async run({ name, args }, ctx) {
|
|
8773
9176
|
const n = String(name ?? "");
|
|
8774
9177
|
if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
|
|
8775
|
-
|
|
8776
|
-
if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
|
|
8777
|
-
const r = toResult(raw);
|
|
8778
|
-
return r.images?.length ? r : r.text;
|
|
9178
|
+
return runMcpCall(n, (o) => callTool(n, args ?? {}, o), ctx, callTimeoutMs);
|
|
8779
9179
|
}
|
|
8780
9180
|
};
|
|
8781
9181
|
return [searchTool, callMcpTool];
|
|
@@ -8795,17 +9195,17 @@ function buildMcpCatalog(servers) {
|
|
|
8795
9195
|
return { specs, routes };
|
|
8796
9196
|
}
|
|
8797
9197
|
function searchOverCatalog(servers, specs, routes, resolve5, options) {
|
|
8798
|
-
const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
|
|
9198
|
+
const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
|
|
8799
9199
|
const r = routes.get(name);
|
|
8800
9200
|
if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
|
|
8801
|
-
return resolve5(r.server, r.rawName, args ?? {});
|
|
9201
|
+
return resolve5(r.server, r.rawName, args ?? {}, o);
|
|
8802
9202
|
}, options) : [];
|
|
8803
9203
|
return { tools, serverNames: servers, toolCount: specs.length };
|
|
8804
9204
|
}
|
|
8805
9205
|
function makeMcpToolSearchFromMounted(mounted, options) {
|
|
8806
9206
|
const { specs, routes } = buildMcpCatalog(mounted);
|
|
8807
9207
|
const byName = new Map(mounted.map((m) => [m.name, m]));
|
|
8808
|
-
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);
|
|
8809
9209
|
}
|
|
8810
9210
|
|
|
8811
9211
|
// src/index.ts
|
|
@@ -8818,9 +9218,20 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
|
|
|
8818
9218
|
init_logging();
|
|
8819
9219
|
import { spawn } from "child_process";
|
|
8820
9220
|
import { createHash } from "crypto";
|
|
8821
|
-
var
|
|
9221
|
+
var log16 = forComponent("mcp");
|
|
8822
9222
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
8823
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" });
|
|
8824
9235
|
var StdioTransport = class {
|
|
8825
9236
|
constructor(spec) {
|
|
8826
9237
|
this.spec = spec;
|
|
@@ -8837,7 +9248,7 @@ var StdioTransport = class {
|
|
|
8837
9248
|
proc.stdout.setEncoding("utf8");
|
|
8838
9249
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
8839
9250
|
proc.stderr.setEncoding("utf8");
|
|
8840
|
-
proc.stderr.on("data", (chunk) =>
|
|
9251
|
+
proc.stderr.on("data", (chunk) => log16.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
8841
9252
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
8842
9253
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
8843
9254
|
}
|
|
@@ -8851,21 +9262,32 @@ var StdioTransport = class {
|
|
|
8851
9262
|
try {
|
|
8852
9263
|
this.dispatch(JSON.parse(line));
|
|
8853
9264
|
} catch (e) {
|
|
8854
|
-
|
|
9265
|
+
log16.debug("dropping non-JSON line from MCP server:", line, e);
|
|
8855
9266
|
}
|
|
8856
9267
|
}
|
|
8857
9268
|
}
|
|
8858
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
|
+
}
|
|
8859
9279
|
if (msg?.id == null || !this.pending.has(msg.id)) return;
|
|
8860
9280
|
const p = this.pending.get(msg.id);
|
|
8861
9281
|
this.pending.delete(msg.id);
|
|
8862
9282
|
clearTimeout(p.timer);
|
|
9283
|
+
p.done?.();
|
|
8863
9284
|
if (msg.error) p.reject(new Error(msg.error?.message ?? JSON.stringify(msg.error)));
|
|
8864
9285
|
else p.resolve(msg.result);
|
|
8865
9286
|
}
|
|
8866
9287
|
failAll(e) {
|
|
8867
9288
|
for (const p of this.pending.values()) {
|
|
8868
9289
|
clearTimeout(p.timer);
|
|
9290
|
+
p.done?.();
|
|
8869
9291
|
p.reject(e);
|
|
8870
9292
|
}
|
|
8871
9293
|
this.pending.clear();
|
|
@@ -8874,19 +9296,31 @@ var StdioTransport = class {
|
|
|
8874
9296
|
if (!this.proc?.stdin) throw new Error("MCP stdio transport not started");
|
|
8875
9297
|
this.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
8876
9298
|
}
|
|
8877
|
-
request(method, params) {
|
|
9299
|
+
request(method, params, opts = {}) {
|
|
8878
9300
|
const id = this.nextId++;
|
|
8879
|
-
const timeoutMs = this.spec
|
|
9301
|
+
const timeoutMs = deadlineFor(method, this.spec, opts);
|
|
9302
|
+
const { signal, onProgress } = opts;
|
|
8880
9303
|
return new Promise((resolve5, reject) => {
|
|
8881
|
-
|
|
9304
|
+
if (signal?.aborted) return reject(abortError(signal));
|
|
9305
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
8882
9306
|
this.pending.delete(id);
|
|
9307
|
+
done();
|
|
8883
9308
|
reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`));
|
|
8884
|
-
}, timeoutMs);
|
|
8885
|
-
|
|
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 });
|
|
8886
9319
|
try {
|
|
8887
|
-
this.write({ jsonrpc: "2.0", id, method, params });
|
|
9320
|
+
this.write({ jsonrpc: "2.0", id, method, params: onProgress ? withProgressToken(params, id) : params });
|
|
8888
9321
|
} catch (e) {
|
|
8889
9322
|
clearTimeout(timer);
|
|
9323
|
+
done();
|
|
8890
9324
|
this.pending.delete(id);
|
|
8891
9325
|
reject(e);
|
|
8892
9326
|
}
|
|
@@ -8900,7 +9334,7 @@ var StdioTransport = class {
|
|
|
8900
9334
|
try {
|
|
8901
9335
|
this.proc?.stdin?.end();
|
|
8902
9336
|
} catch (e) {
|
|
8903
|
-
|
|
9337
|
+
log16.debug("stdin end failed", e);
|
|
8904
9338
|
}
|
|
8905
9339
|
this.proc?.kill();
|
|
8906
9340
|
}
|
|
@@ -8927,13 +9361,24 @@ var HttpTransport = class {
|
|
|
8927
9361
|
fetchImpl;
|
|
8928
9362
|
async start() {
|
|
8929
9363
|
}
|
|
8930
|
-
request(method, params) {
|
|
8931
|
-
|
|
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
|
+
}
|
|
8932
9377
|
}
|
|
8933
9378
|
async notify(method, params) {
|
|
8934
|
-
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));
|
|
8935
9380
|
}
|
|
8936
|
-
async post(msg, isNotify) {
|
|
9381
|
+
async post(msg, isNotify, signal, onProgress) {
|
|
8937
9382
|
const headers = {
|
|
8938
9383
|
"content-type": "application/json",
|
|
8939
9384
|
accept: "application/json, text/event-stream",
|
|
@@ -8946,7 +9391,7 @@ var HttpTransport = class {
|
|
|
8946
9391
|
method: "POST",
|
|
8947
9392
|
headers,
|
|
8948
9393
|
body: JSON.stringify(msg),
|
|
8949
|
-
signal
|
|
9394
|
+
signal
|
|
8950
9395
|
});
|
|
8951
9396
|
const sid = resp.headers.get("mcp-session-id");
|
|
8952
9397
|
if (sid) this.sessionId = sid;
|
|
@@ -8954,25 +9399,63 @@ var HttpTransport = class {
|
|
|
8954
9399
|
if (!resp.ok) throw new Error(`MCP HTTP ${resp.status} ${resp.statusText}`);
|
|
8955
9400
|
if (isNotify) return;
|
|
8956
9401
|
const ct = resp.headers.get("content-type") ?? "";
|
|
8957
|
-
const data = ct.includes("text/event-stream") ?
|
|
9402
|
+
const data = ct.includes("text/event-stream") ? await readSseResponse(resp, msg.id, onProgress) : await resp.json();
|
|
8958
9403
|
if (data?.error) throw new Error(data.error?.message ?? JSON.stringify(data.error));
|
|
8959
9404
|
return data?.result;
|
|
8960
9405
|
}
|
|
8961
9406
|
async close() {
|
|
8962
9407
|
}
|
|
8963
9408
|
};
|
|
8964
|
-
function
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
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;
|
|
8973
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
|
+
});
|
|
8974
9458
|
}
|
|
8975
|
-
return {};
|
|
8976
9459
|
}
|
|
8977
9460
|
var McpClient = class {
|
|
8978
9461
|
constructor(transport, clientInfo = { name: "agentx", version: "0" }) {
|
|
@@ -8996,8 +9479,9 @@ var McpClient = class {
|
|
|
8996
9479
|
const r = await this.transport.request("tools/list");
|
|
8997
9480
|
return Array.isArray(r?.tools) ? r.tools : [];
|
|
8998
9481
|
}
|
|
8999
|
-
|
|
9000
|
-
|
|
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);
|
|
9001
9485
|
}
|
|
9002
9486
|
async listResources() {
|
|
9003
9487
|
const r = await this.transport.request("resources/list");
|
|
@@ -9011,7 +9495,7 @@ var McpClient = class {
|
|
|
9011
9495
|
}
|
|
9012
9496
|
};
|
|
9013
9497
|
function buildTransport(cfg) {
|
|
9014
|
-
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 });
|
|
9015
9499
|
}
|
|
9016
9500
|
function withTimeout(p, ms, label) {
|
|
9017
9501
|
if (!ms || ms <= 0) return p;
|
|
@@ -9033,11 +9517,11 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
|
|
|
9033
9517
|
return await withTimeout((async () => {
|
|
9034
9518
|
const init = await client.connect();
|
|
9035
9519
|
const specs = await client.listTools();
|
|
9036
|
-
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}__`);
|
|
9037
9521
|
return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
|
|
9038
9522
|
})(), mountTimeoutMs, name);
|
|
9039
9523
|
} catch (e) {
|
|
9040
|
-
await client.close().catch((err2) =>
|
|
9524
|
+
await client.close().catch((err2) => log16.debug(`close after failed mount of "${name}": ${err2}`));
|
|
9041
9525
|
throw e;
|
|
9042
9526
|
}
|
|
9043
9527
|
}
|
|
@@ -9048,15 +9532,15 @@ function validEntries(servers) {
|
|
|
9048
9532
|
return Object.entries(servers).filter(([name, cfg]) => {
|
|
9049
9533
|
if (!cfg || cfg.disabled) return false;
|
|
9050
9534
|
if (!cfg.command && !cfg.url) {
|
|
9051
|
-
|
|
9535
|
+
log16.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
9052
9536
|
return false;
|
|
9053
9537
|
}
|
|
9054
9538
|
return true;
|
|
9055
9539
|
});
|
|
9056
9540
|
}
|
|
9057
9541
|
function logMountFailure(name, e) {
|
|
9058
|
-
if (e instanceof McpAuthError)
|
|
9059
|
-
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}`);
|
|
9060
9544
|
}
|
|
9061
9545
|
async function mountMcpServers(servers = {}, opts = {}) {
|
|
9062
9546
|
const entries = validEntries(servers);
|
|
@@ -9066,7 +9550,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
|
|
|
9066
9550
|
const name = entries[i][0];
|
|
9067
9551
|
if (r.status === "fulfilled") {
|
|
9068
9552
|
out.push(r.value);
|
|
9069
|
-
|
|
9553
|
+
log16.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
|
|
9070
9554
|
} else logMountFailure(name, r.reason);
|
|
9071
9555
|
});
|
|
9072
9556
|
return out;
|
|
@@ -9106,7 +9590,7 @@ var McpPool = class {
|
|
|
9106
9590
|
const prev = this.warm.get(key);
|
|
9107
9591
|
if (prev) {
|
|
9108
9592
|
clearTimeout(prev.timer);
|
|
9109
|
-
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}`));
|
|
9110
9594
|
}
|
|
9111
9595
|
const e = { client, timer: void 0 };
|
|
9112
9596
|
this.warm.set(key, e);
|
|
@@ -9123,7 +9607,7 @@ var McpPool = class {
|
|
|
9123
9607
|
const e = this.warm.get(key);
|
|
9124
9608
|
if (!e) return;
|
|
9125
9609
|
this.warm.delete(key);
|
|
9126
|
-
await e.client.close().catch((err2) =>
|
|
9610
|
+
await e.client.close().catch((err2) => log16.debug(`warm-pool evict close failed: ${err2}`));
|
|
9127
9611
|
}
|
|
9128
9612
|
async closeAll() {
|
|
9129
9613
|
for (const e of this.warm.values()) {
|
|
@@ -9341,7 +9825,7 @@ init_tools_shell();
|
|
|
9341
9825
|
// src/tools.notify.ts
|
|
9342
9826
|
init_logging();
|
|
9343
9827
|
import { execFile } from "child_process";
|
|
9344
|
-
var
|
|
9828
|
+
var log18 = forComponent("notify");
|
|
9345
9829
|
function makeNotifyTool(opts = {}) {
|
|
9346
9830
|
const platform2 = opts.platform ?? process.platform;
|
|
9347
9831
|
const run = opts.exec ?? execFile;
|
|
@@ -9365,7 +9849,7 @@ function makeNotifyTool(opts = {}) {
|
|
|
9365
9849
|
return new Promise((resolve5) => {
|
|
9366
9850
|
run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
|
|
9367
9851
|
if (e) {
|
|
9368
|
-
|
|
9852
|
+
log18.debug("notification failed", e);
|
|
9369
9853
|
resolve5(`Notification failed: ${e.message}`);
|
|
9370
9854
|
} else resolve5("Notification shown.");
|
|
9371
9855
|
});
|
|
@@ -9381,7 +9865,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
|
|
|
9381
9865
|
init_logging();
|
|
9382
9866
|
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
9383
9867
|
import { homedir } from "os";
|
|
9384
|
-
var
|
|
9868
|
+
var log19 = forComponent("cli-util");
|
|
9385
9869
|
function dotDirs(base, sub, opts = {}) {
|
|
9386
9870
|
const home = opts.home ?? homedir();
|
|
9387
9871
|
const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
|
|
@@ -9398,7 +9882,7 @@ function parseJson(text, fallback, what = "json") {
|
|
|
9398
9882
|
try {
|
|
9399
9883
|
return JSON.parse(text);
|
|
9400
9884
|
} catch (e) {
|
|
9401
|
-
|
|
9885
|
+
log19.debug(`parseJson(${what}) failed: ${e.message}`);
|
|
9402
9886
|
return fallback;
|
|
9403
9887
|
}
|
|
9404
9888
|
}
|
|
@@ -9408,7 +9892,7 @@ function readJsonFile(path, fallback) {
|
|
|
9408
9892
|
try {
|
|
9409
9893
|
text = readFileSync4(path, "utf8");
|
|
9410
9894
|
} catch (e) {
|
|
9411
|
-
|
|
9895
|
+
log19.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
|
|
9412
9896
|
return fallback;
|
|
9413
9897
|
}
|
|
9414
9898
|
return parseJson(text, fallback, path);
|
|
@@ -9463,9 +9947,9 @@ function toCursorMcp(servers) {
|
|
|
9463
9947
|
}
|
|
9464
9948
|
return Object.keys(out).length ? { mcpServers: out } : void 0;
|
|
9465
9949
|
}
|
|
9466
|
-
function cursorProviderOptions(model, cwd, mcpServers) {
|
|
9950
|
+
function cursorProviderOptions(model, cwd, mcpServers, hostMcpServers) {
|
|
9467
9951
|
if (!(model ?? "").startsWith("cursor/")) return void 0;
|
|
9468
|
-
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() } };
|
|
9469
9953
|
}
|
|
9470
9954
|
function toClaudeCodeMcp(servers) {
|
|
9471
9955
|
if (!servers) return void 0;
|
|
@@ -9492,8 +9976,8 @@ function claudeCodeProviderOptions(model, cwd, mcpServers) {
|
|
|
9492
9976
|
const warm = process.env.AGENTX_CC_WARM === "0" ? {} : { claudeCodeSession: randomUUID() };
|
|
9493
9977
|
return { cwd, ...env ? { env } : {}, ...warm, ...toClaudeCodeMcp(mcpServers) ?? {} };
|
|
9494
9978
|
}
|
|
9495
|
-
function providerOptionsFor(model, cwd, mcpServers) {
|
|
9496
|
-
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);
|
|
9497
9981
|
}
|
|
9498
9982
|
async function buildAgent(o) {
|
|
9499
9983
|
if (typeof o.sandbox !== "boolean")
|
|
@@ -9578,12 +10062,14 @@ Reference files in them by their mount path (the left side).`;
|
|
|
9578
10062
|
const hooks = o.learnFromMistakes && memoryWriteDir ? composeHooks(o.hooks, lessonCapture({ fs, dir: memoryWriteDir, minRepeats: 2 })) : o.hooks;
|
|
9579
10063
|
let realShell = [];
|
|
9580
10064
|
const agentRef = {};
|
|
10065
|
+
const jobRegistry = new JobRegistry();
|
|
9581
10066
|
const useRealShell = o.realShell ?? !virtual;
|
|
9582
10067
|
if (useRealShell && !virtual && !isClaudeCode) {
|
|
9583
10068
|
const jobs = new ShellJobRegistry({
|
|
9584
10069
|
cwd,
|
|
9585
10070
|
killOnExit: true,
|
|
9586
10071
|
osSandbox: o.osSandbox,
|
|
10072
|
+
jobs: jobRegistry,
|
|
9587
10073
|
onExit: (n) => agentRef.agent?.inject(formatJobExit(n))
|
|
9588
10074
|
});
|
|
9589
10075
|
realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox, jobsSurviveRun: o.singleRun !== true }), ...makeShellJobTools(jobs)];
|
|
@@ -9592,6 +10078,7 @@ Reference files in them by their mount path (the left side).`;
|
|
|
9592
10078
|
const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
|
|
9593
10079
|
return agentRef.agent = new Agent({
|
|
9594
10080
|
ai: o.ai,
|
|
10081
|
+
jobs: jobRegistry,
|
|
9595
10082
|
fs,
|
|
9596
10083
|
model: o.model ?? "anthropic/claude-sonnet-4-6",
|
|
9597
10084
|
// PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
|
|
@@ -9603,7 +10090,7 @@ Reference files in them by their mount path (the left side).`;
|
|
|
9603
10090
|
// Warm cursor session (default on; CURSOR_WARM=0 to disable): one long-lived agent reused across
|
|
9604
10091
|
// turns, amortising the ~2s Agent.create + h2 handshake. Keyed per agentx launch.
|
|
9605
10092
|
...(() => {
|
|
9606
|
-
const po = providerOptionsFor(o.model, cwd, o.mcpServers);
|
|
10093
|
+
const po = providerOptionsFor(o.model, cwd, o.mcpServers, o.hostMcpServers);
|
|
9607
10094
|
return po ? { providerOptions: po } : {};
|
|
9608
10095
|
})(),
|
|
9609
10096
|
...(() => {
|
|
@@ -9704,7 +10191,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
|
|
|
9704
10191
|
import { homedir as homedir3 } from "os";
|
|
9705
10192
|
import { dirname as dirname3, join as join6 } from "path";
|
|
9706
10193
|
import { fileURLToPath } from "url";
|
|
9707
|
-
var
|
|
10194
|
+
var log20 = forComponent("VoiceIO");
|
|
9708
10195
|
var now3 = () => performance.now();
|
|
9709
10196
|
var Player = class {
|
|
9710
10197
|
proc = null;
|
|
@@ -9718,7 +10205,7 @@ var Player = class {
|
|
|
9718
10205
|
["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
|
|
9719
10206
|
{ stdio: ["pipe", "ignore", "ignore"] }
|
|
9720
10207
|
);
|
|
9721
|
-
this.proc.on("error", (e) =>
|
|
10208
|
+
this.proc.on("error", (e) => log20.warn(`ffplay error: ${e.message}`));
|
|
9722
10209
|
this.proc.stdin.on("error", () => {
|
|
9723
10210
|
});
|
|
9724
10211
|
this.bytesWritten = 0;
|
|
@@ -9756,7 +10243,7 @@ function detectFfmpegMic() {
|
|
|
9756
10243
|
const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
|
|
9757
10244
|
const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
|
|
9758
10245
|
if (!mic) throw new Error("no audio input device found");
|
|
9759
|
-
|
|
10246
|
+
log20.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
|
|
9760
10247
|
return `:${mic.idx}`;
|
|
9761
10248
|
}
|
|
9762
10249
|
function detectedInputDevice() {
|
|
@@ -9792,15 +10279,15 @@ function resolveAecBinary() {
|
|
|
9792
10279
|
if (existsSync4(bin) && statSync4(bin).mtimeMs >= statSync4(src).mtimeMs) return bin;
|
|
9793
10280
|
if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
|
|
9794
10281
|
mkdirSync5(cacheDir, { recursive: true });
|
|
9795
|
-
|
|
10282
|
+
log20.info("compiling AEC mic helper (first run)\u2026");
|
|
9796
10283
|
const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
|
|
9797
10284
|
if (build.status !== 0) {
|
|
9798
|
-
|
|
10285
|
+
log20.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
|
|
9799
10286
|
return null;
|
|
9800
10287
|
}
|
|
9801
10288
|
const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
|
|
9802
10289
|
if (sign.status !== 0) {
|
|
9803
|
-
|
|
10290
|
+
log20.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
|
|
9804
10291
|
return null;
|
|
9805
10292
|
}
|
|
9806
10293
|
return bin;
|
|
@@ -9842,16 +10329,16 @@ var NodeMicSource = class {
|
|
|
9842
10329
|
this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
|
|
9843
10330
|
} else {
|
|
9844
10331
|
if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
|
|
9845
|
-
|
|
10332
|
+
log20.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
|
|
9846
10333
|
this.proc = spawn2(
|
|
9847
10334
|
"ffmpeg",
|
|
9848
10335
|
["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
|
|
9849
10336
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
9850
10337
|
);
|
|
9851
|
-
this.proc.stderr.on("data", (d) =>
|
|
10338
|
+
this.proc.stderr.on("data", (d) => log20.warn(`ffmpeg: ${String(d).trim()}`));
|
|
9852
10339
|
}
|
|
9853
10340
|
this.proc.on("exit", (c) => {
|
|
9854
|
-
if (c && !this.stopped)
|
|
10341
|
+
if (c && !this.stopped) log20.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
|
|
9855
10342
|
});
|
|
9856
10343
|
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
9857
10344
|
}
|
|
@@ -9917,7 +10404,7 @@ var AecDuplexAudio = class {
|
|
|
9917
10404
|
this.proc.stdin.on("error", () => {
|
|
9918
10405
|
});
|
|
9919
10406
|
this.proc.on("exit", (c) => {
|
|
9920
|
-
if (c && !this.stopped)
|
|
10407
|
+
if (c && !this.stopped) log20.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
|
|
9921
10408
|
});
|
|
9922
10409
|
this.proc.stdout.on("data", (chunk) => {
|
|
9923
10410
|
this.gotChunk = true;
|
|
@@ -9933,7 +10420,7 @@ var AecDuplexAudio = class {
|
|
|
9933
10420
|
openMicSettings();
|
|
9934
10421
|
this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
|
|
9935
10422
|
}
|
|
9936
|
-
} else
|
|
10423
|
+
} else log20.debug(`mic-aec: ${s}`);
|
|
9937
10424
|
}
|
|
9938
10425
|
});
|
|
9939
10426
|
if (!this.noVpio && !this.triedFallback) {
|
|
@@ -9942,7 +10429,7 @@ var AecDuplexAudio = class {
|
|
|
9942
10429
|
this.triedFallback = true;
|
|
9943
10430
|
this.noVpio = true;
|
|
9944
10431
|
this._aec = false;
|
|
9945
|
-
|
|
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)");
|
|
9946
10433
|
this.onDegrade?.();
|
|
9947
10434
|
this.killProc();
|
|
9948
10435
|
this.spawnHelper();
|
|
@@ -10162,7 +10649,7 @@ function fakeVoiceParts(uttFile) {
|
|
|
10162
10649
|
init_logging();
|
|
10163
10650
|
import { appendFile, mkdirSync as mkdirSync6 } from "fs";
|
|
10164
10651
|
import { dirname as dirname4 } from "path";
|
|
10165
|
-
var
|
|
10652
|
+
var log21 = forComponent("VoiceDiag");
|
|
10166
10653
|
var JsonlDiagSink = class {
|
|
10167
10654
|
constructor(path) {
|
|
10168
10655
|
this.path = path;
|
|
@@ -10203,7 +10690,7 @@ var JsonlDiagSink = class {
|
|
|
10203
10690
|
disable(e) {
|
|
10204
10691
|
this.disabled = true;
|
|
10205
10692
|
this.queue.length = 0;
|
|
10206
|
-
|
|
10693
|
+
log21.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
|
|
10207
10694
|
}
|
|
10208
10695
|
/** True once a failure permanently disabled the sink (tests/inspection). */
|
|
10209
10696
|
get isDisabled() {
|
|
@@ -10273,7 +10760,7 @@ async function loadConfig(cwd) {
|
|
|
10273
10760
|
|
|
10274
10761
|
// cli/hooks-config.ts
|
|
10275
10762
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
10276
|
-
var
|
|
10763
|
+
var log22 = forComponent("hooks");
|
|
10277
10764
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
10278
10765
|
function ruleMatches(rule, toolName) {
|
|
10279
10766
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -10290,7 +10777,7 @@ function runCmd(rule, env) {
|
|
|
10290
10777
|
});
|
|
10291
10778
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
10292
10779
|
} catch (e) {
|
|
10293
|
-
|
|
10780
|
+
log22.debug(`hook command failed: ${rule.command}`, e);
|
|
10294
10781
|
return { code: 1, out: String(e?.message ?? e) };
|
|
10295
10782
|
}
|
|
10296
10783
|
}
|
|
@@ -10397,7 +10884,7 @@ function formatDiff(ops, opts = {}) {
|
|
|
10397
10884
|
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
|
|
10398
10885
|
import { homedir as homedir5 } from "os";
|
|
10399
10886
|
import { join as join8 } from "path";
|
|
10400
|
-
var
|
|
10887
|
+
var log23 = forComponent("session");
|
|
10401
10888
|
var globalDir = () => join8(homedir5(), ".agent", "sessions");
|
|
10402
10889
|
var SessionStore = class {
|
|
10403
10890
|
dir;
|
|
@@ -10442,7 +10929,7 @@ var SessionStore = class {
|
|
|
10442
10929
|
}
|
|
10443
10930
|
load(id) {
|
|
10444
10931
|
if (!this.safeId(id)) {
|
|
10445
|
-
|
|
10932
|
+
log23.debug(`rejecting unsafe session id: ${id}`);
|
|
10446
10933
|
return void 0;
|
|
10447
10934
|
}
|
|
10448
10935
|
const path = join8(this.dir, `${id}.json`);
|
|
@@ -10450,7 +10937,7 @@ var SessionStore = class {
|
|
|
10450
10937
|
try {
|
|
10451
10938
|
return JSON.parse(readFileSync7(path, "utf8"));
|
|
10452
10939
|
} catch (e) {
|
|
10453
|
-
|
|
10940
|
+
log23.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
10454
10941
|
return void 0;
|
|
10455
10942
|
}
|
|
10456
10943
|
}
|
|
@@ -10463,7 +10950,7 @@ var SessionStore = class {
|
|
|
10463
10950
|
try {
|
|
10464
10951
|
metas.push(JSON.parse(readFileSync7(join8(this.dir, f), "utf8")).meta);
|
|
10465
10952
|
} catch (e) {
|
|
10466
|
-
|
|
10953
|
+
log23.debug(`skipping unreadable session file ${f}`, e);
|
|
10467
10954
|
}
|
|
10468
10955
|
}
|
|
10469
10956
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -10632,7 +11119,7 @@ import { promisify } from "util";
|
|
|
10632
11119
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync8 } from "fs";
|
|
10633
11120
|
import { homedir as homedir6 } from "os";
|
|
10634
11121
|
import { join as join9, resolve as resolve3, sep as sep3, parse as parsePath } from "path";
|
|
10635
|
-
var
|
|
11122
|
+
var log24 = forComponent("checkpoints");
|
|
10636
11123
|
var exec = promisify(execFile3);
|
|
10637
11124
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
10638
11125
|
function isBroadRoot(p) {
|
|
@@ -10678,7 +11165,7 @@ var ShadowRepo = class {
|
|
|
10678
11165
|
writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
10679
11166
|
this.ready = true;
|
|
10680
11167
|
} catch (e) {
|
|
10681
|
-
|
|
11168
|
+
log24.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
10682
11169
|
this.ready = false;
|
|
10683
11170
|
}
|
|
10684
11171
|
return this.ready;
|
|
@@ -10689,7 +11176,7 @@ var ShadowRepo = class {
|
|
|
10689
11176
|
}
|
|
10690
11177
|
async commit(label, forced = []) {
|
|
10691
11178
|
await this.run("add", "-A");
|
|
10692
|
-
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));
|
|
10693
11180
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
10694
11181
|
}
|
|
10695
11182
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -10697,8 +11184,8 @@ var ShadowRepo = class {
|
|
|
10697
11184
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
10698
11185
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
10699
11186
|
async amendForced(paths) {
|
|
10700
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
10701
|
-
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));
|
|
10702
11189
|
}
|
|
10703
11190
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
10704
11191
|
async log(ref) {
|
|
@@ -10758,7 +11245,7 @@ var ShadowRepo = class {
|
|
|
10758
11245
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
10759
11246
|
});
|
|
10760
11247
|
} catch (e) {
|
|
10761
|
-
|
|
11248
|
+
log24.debug("checkpoint prune failed", e);
|
|
10762
11249
|
}
|
|
10763
11250
|
}
|
|
10764
11251
|
};
|
|
@@ -10826,7 +11313,7 @@ var GitCheckpoints = class {
|
|
|
10826
11313
|
use(sessionId) {
|
|
10827
11314
|
if (sessionId === this.session) return;
|
|
10828
11315
|
this.session = sessionId;
|
|
10829
|
-
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));
|
|
10830
11317
|
}
|
|
10831
11318
|
async begin(label) {
|
|
10832
11319
|
if (!await this.start()) return;
|
|
@@ -10846,7 +11333,7 @@ var GitCheckpoints = class {
|
|
|
10846
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
|
|
10847
11334
|
`);
|
|
10848
11335
|
} else {
|
|
10849
|
-
|
|
11336
|
+
log24.debug("checkpoint commit failed", e);
|
|
10850
11337
|
survivors.push(this.repos[i]);
|
|
10851
11338
|
survivorCaches.push(this.caches[i] ?? []);
|
|
10852
11339
|
}
|
|
@@ -10881,7 +11368,7 @@ var GitCheckpoints = class {
|
|
|
10881
11368
|
if (this.forced.has(abs)) continue;
|
|
10882
11369
|
this.forced.add(abs);
|
|
10883
11370
|
const i = this.repoIndexFor(abs);
|
|
10884
|
-
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));
|
|
10885
11372
|
}
|
|
10886
11373
|
}
|
|
10887
11374
|
};
|
|
@@ -12704,7 +13191,7 @@ import { spawnSync as spawnSync5 } from "child_process";
|
|
|
12704
13191
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
12705
13192
|
import { homedir as homedir8 } from "os";
|
|
12706
13193
|
import { join as join12 } from "path";
|
|
12707
|
-
var
|
|
13194
|
+
var log25 = forComponent("os-sched");
|
|
12708
13195
|
var OsScheduler = class {
|
|
12709
13196
|
options;
|
|
12710
13197
|
constructor(options) {
|
|
@@ -12766,7 +13253,7 @@ var OsScheduler = class {
|
|
|
12766
13253
|
}
|
|
12767
13254
|
}
|
|
12768
13255
|
} catch (e) {
|
|
12769
|
-
|
|
13256
|
+
log25.debug(`cancel ${id}`, e);
|
|
12770
13257
|
}
|
|
12771
13258
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
12772
13259
|
try {
|
|
@@ -12883,7 +13370,7 @@ import { spawn as spawn3 } from "child_process";
|
|
|
12883
13370
|
import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
|
|
12884
13371
|
import { homedir as homedir9 } from "os";
|
|
12885
13372
|
import { join as join13 } from "path";
|
|
12886
|
-
var
|
|
13373
|
+
var log26 = forComponent("remote-trigger");
|
|
12887
13374
|
var TRIGGER_DIR = () => join13(homedir9(), ".agent", "triggers");
|
|
12888
13375
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
|
|
12889
13376
|
var TriggerServer = class {
|
|
@@ -12921,13 +13408,13 @@ var TriggerServer = class {
|
|
|
12921
13408
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
12922
13409
|
}
|
|
12923
13410
|
});
|
|
12924
|
-
conn.on("error", (e) =>
|
|
13411
|
+
conn.on("error", (e) => log26.debug("trigger conn error", e));
|
|
12925
13412
|
});
|
|
12926
|
-
this.server.on("error", (e) =>
|
|
13413
|
+
this.server.on("error", (e) => log26.debug("trigger server error", e));
|
|
12927
13414
|
this.server.listen(p);
|
|
12928
13415
|
this.path = p;
|
|
12929
13416
|
} catch (e) {
|
|
12930
|
-
|
|
13417
|
+
log26.debug("trigger server unavailable", e);
|
|
12931
13418
|
}
|
|
12932
13419
|
}
|
|
12933
13420
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -13049,7 +13536,7 @@ var strike = C("9");
|
|
|
13049
13536
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
13050
13537
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
13051
13538
|
var err = (s) => process.stderr.write(s);
|
|
13052
|
-
var
|
|
13539
|
+
var log27 = forComponent("cli");
|
|
13053
13540
|
var VERSION = (() => {
|
|
13054
13541
|
try {
|
|
13055
13542
|
return JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -13099,9 +13586,9 @@ var spinner = /* @__PURE__ */ (() => {
|
|
|
13099
13586
|
if (!tty || timer) return;
|
|
13100
13587
|
t0 = this.turnStart || Date.now();
|
|
13101
13588
|
timer = setInterval(() => {
|
|
13102
|
-
const
|
|
13589
|
+
const secs2 = Math.round((Date.now() - t0) / 1e3);
|
|
13103
13590
|
const tail = this.tail?.();
|
|
13104
|
-
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 : ""));
|
|
13105
13592
|
}, 90);
|
|
13106
13593
|
},
|
|
13107
13594
|
stop() {
|
|
@@ -13794,7 +14281,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
|
|
|
13794
14281
|
function turnCost(model, usage) {
|
|
13795
14282
|
return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
|
|
13796
14283
|
}
|
|
13797
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
14284
|
+
async function evaluateGoal(ai, condition, transcript, log28) {
|
|
13798
14285
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
13799
14286
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
13800
14287
|
return text.slice(0, 600);
|
|
@@ -13814,7 +14301,7 @@ ${recent}` }
|
|
|
13814
14301
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
13815
14302
|
if (match) return JSON.parse(match[0]);
|
|
13816
14303
|
} catch (e) {
|
|
13817
|
-
|
|
14304
|
+
log28(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
13818
14305
|
`));
|
|
13819
14306
|
}
|
|
13820
14307
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -13982,7 +14469,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
|
|
|
13982
14469
|
anthropicWebSearch: cfg.anthropicWebSearch
|
|
13983
14470
|
};
|
|
13984
14471
|
}
|
|
13985
|
-
async function makeAgent(args, ai, cfg, extraTools = []) {
|
|
14472
|
+
async function makeAgent(args, ai, cfg, extraTools = [], hostMcpServers) {
|
|
13986
14473
|
const virtual = args.vfs || !!args.boddb;
|
|
13987
14474
|
if (virtual && args.shell) err(yellow(` \u26A0 --shell ignored in ${args.boddb ? "--boddb" : "--vfs sandbox"} (a real shell would escape to disk)
|
|
13988
14475
|
`));
|
|
@@ -13996,7 +14483,7 @@ async function makeAgent(args, ai, cfg, extraTools = []) {
|
|
|
13996
14483
|
else if (args.shell === false) err(dim(" \u2296 --no-shell: VFS bash only (no real /bin/sh)\n"));
|
|
13997
14484
|
if (args.harden && !virtual) err(dim(` \u26E8 hardened shell: writes confined to cwd+tmp${args.hardenNet ? "" : ", network blocked"} (sandbox-exec/bwrap)
|
|
13998
14485
|
`));
|
|
13999
|
-
const opts = optsFor(args, ai, cfg, extraTools);
|
|
14486
|
+
const opts = { ...optsFor(args, ai, cfg, extraTools), hostMcpServers };
|
|
14000
14487
|
const agent = await buildAgent(opts);
|
|
14001
14488
|
const display = displayHooks(agent.options.fs, { flush: agent.options.host?.flushText });
|
|
14002
14489
|
agent.options.hooks = cfg.hooks ? composeHooks(display, hooksFromConfig(cfg.hooks)) : display;
|
|
@@ -14035,7 +14522,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
14035
14522
|
return tools;
|
|
14036
14523
|
}
|
|
14037
14524
|
async function closeMcp(mounted) {
|
|
14038
|
-
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))));
|
|
14039
14526
|
}
|
|
14040
14527
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
14041
14528
|
function mentionRefs(line) {
|
|
@@ -14087,7 +14574,7 @@ async function expandMentions(fs, line) {
|
|
|
14087
14574
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
14088
14575
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
14089
14576
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
14090
|
-
|
|
14577
|
+
log27.debug("mcp mention resolve failed", e);
|
|
14091
14578
|
return null;
|
|
14092
14579
|
});
|
|
14093
14580
|
if (body != null) {
|
|
@@ -14174,7 +14661,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
14174
14661
|
try {
|
|
14175
14662
|
store.save(session);
|
|
14176
14663
|
} catch (ex) {
|
|
14177
|
-
|
|
14664
|
+
log27.debug("mid-turn session flush failed", ex);
|
|
14178
14665
|
}
|
|
14179
14666
|
}
|
|
14180
14667
|
return origNotify(e);
|
|
@@ -14201,7 +14688,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
14201
14688
|
}
|
|
14202
14689
|
agent.options.host?.flushText?.();
|
|
14203
14690
|
flushLookupGroup();
|
|
14204
|
-
const
|
|
14691
|
+
const secs2 = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
14205
14692
|
const cost = turnCost(agent.options.model, res.usage);
|
|
14206
14693
|
const m = res.usageEstimated ? "~" : "";
|
|
14207
14694
|
const tok = res.usage?.totalTokens ? `${m}${(res.usage.totalTokens / 1e3).toFixed(1)}k tok \xB7 ${cost > 0 ? m + fmtUsd(cost) + " \xB7 " : ""}` : "";
|
|
@@ -14212,7 +14699,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
14212
14699
|
const didWork = agent.transcript.slice(msgsBefore).some((m2) => m2.role === "assistant" || m2.role === "tool");
|
|
14213
14700
|
const silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens && !didWork;
|
|
14214
14701
|
if (!silentAbort)
|
|
14215
|
-
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}
|
|
14216
14703
|
`));
|
|
14217
14704
|
if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
|
|
14218
14705
|
if (res.finishReason === "error" && res.error) {
|
|
@@ -14323,14 +14810,14 @@ var isCancelTeardown = (e) => {
|
|
|
14323
14810
|
function installCancelGuards(mounted) {
|
|
14324
14811
|
process.on("unhandledRejection", (e) => {
|
|
14325
14812
|
if (isCancelTeardown(e)) {
|
|
14326
|
-
|
|
14813
|
+
log27.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
14327
14814
|
return;
|
|
14328
14815
|
}
|
|
14329
|
-
|
|
14816
|
+
log27.error("unhandledRejection", e);
|
|
14330
14817
|
});
|
|
14331
14818
|
process.on("uncaughtException", (e) => {
|
|
14332
14819
|
if (isCancelTeardown(e)) {
|
|
14333
|
-
|
|
14820
|
+
log27.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
14334
14821
|
return;
|
|
14335
14822
|
}
|
|
14336
14823
|
console.error(e);
|
|
@@ -14344,7 +14831,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
14344
14831
|
let mcpToolNames = [];
|
|
14345
14832
|
const initialMcpTools = mcpAgentTools(mounted);
|
|
14346
14833
|
mcpToolNames = initialMcpTools.map((t) => t.name);
|
|
14347
|
-
const agent = await makeAgent(args, ai, cfg, initialMcpTools);
|
|
14834
|
+
const agent = await makeAgent(args, ai, cfg, initialMcpTools, mounted.map((m) => m.name));
|
|
14348
14835
|
if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
|
|
14349
14836
|
exitRequested = true;
|
|
14350
14837
|
})];
|
|
@@ -14586,7 +15073,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
14586
15073
|
...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
|
|
14587
15074
|
actModel: agent.options.model,
|
|
14588
15075
|
actOptions: workerOptions,
|
|
14589
|
-
providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers),
|
|
15076
|
+
providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name)),
|
|
14590
15077
|
...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
|
|
14591
15078
|
host,
|
|
14592
15079
|
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true, emotionTags: emotionsOn } : {},
|
|
@@ -14668,7 +15155,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
14668
15155
|
const setModel = (m) => {
|
|
14669
15156
|
work.model = m;
|
|
14670
15157
|
work.contextWindow = getModelInfo(m)?.contextWindow ?? 0;
|
|
14671
|
-
work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers);
|
|
15158
|
+
work.providerOptions = providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name));
|
|
14672
15159
|
if (dx) dx.options.actModel = m;
|
|
14673
15160
|
persistModel(cwd, m);
|
|
14674
15161
|
err(dim(" model \u2192 " + m + "\n"));
|
|
@@ -14735,7 +15222,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
14735
15222
|
try {
|
|
14736
15223
|
mkdirSync12(dir, { recursive: true });
|
|
14737
15224
|
} catch (e) {
|
|
14738
|
-
|
|
15225
|
+
log27.debug("paste image dir create failed", e);
|
|
14739
15226
|
}
|
|
14740
15227
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
14741
15228
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -14848,7 +15335,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
14848
15335
|
mkdirSync12(join14(cwd, ".agent"), { recursive: true });
|
|
14849
15336
|
appendFileSync(histPath, line + "\n");
|
|
14850
15337
|
} catch (e) {
|
|
14851
|
-
|
|
15338
|
+
log27.debug("history write failed", e);
|
|
14852
15339
|
}
|
|
14853
15340
|
};
|
|
14854
15341
|
const ago = (t) => {
|
|
@@ -14920,7 +15407,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
14920
15407
|
try {
|
|
14921
15408
|
store.save(session);
|
|
14922
15409
|
} catch (e) {
|
|
14923
|
-
|
|
15410
|
+
log27.debug("session save after rewind failed", e);
|
|
14924
15411
|
}
|
|
14925
15412
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
14926
15413
|
`));
|
|
@@ -14954,7 +15441,7 @@ ${task}`;
|
|
|
14954
15441
|
bangContext.length = 0;
|
|
14955
15442
|
}
|
|
14956
15443
|
const delta = await refreshCatalogs().catch((e) => {
|
|
14957
|
-
|
|
15444
|
+
log27.debug("catalog refresh failed", e);
|
|
14958
15445
|
return "";
|
|
14959
15446
|
});
|
|
14960
15447
|
if (delta) {
|
|
@@ -15218,7 +15705,7 @@ ${task}`;
|
|
|
15218
15705
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
15219
15706
|
run: async () => {
|
|
15220
15707
|
await refreshCatalogs().catch((e) => {
|
|
15221
|
-
|
|
15708
|
+
log27.debug("catalog refresh failed", e);
|
|
15222
15709
|
});
|
|
15223
15710
|
face.reprepare();
|
|
15224
15711
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -15720,7 +16207,7 @@ ${task}`;
|
|
|
15720
16207
|
try {
|
|
15721
16208
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
15722
16209
|
} catch (e) {
|
|
15723
|
-
|
|
16210
|
+
log27.debug(`loadAgents(${d}) failed`, e);
|
|
15724
16211
|
}
|
|
15725
16212
|
}
|
|
15726
16213
|
if (!seen.size) {
|
|
@@ -15808,7 +16295,7 @@ ${task}`;
|
|
|
15808
16295
|
}
|
|
15809
16296
|
if (idx >= 0) {
|
|
15810
16297
|
const old = mounted.splice(idx, 1)[0];
|
|
15811
|
-
await old.client.close().catch((e) =>
|
|
16298
|
+
await old.client.close().catch((e) => log27.debug("mcp close failed", e));
|
|
15812
16299
|
}
|
|
15813
16300
|
try {
|
|
15814
16301
|
const m = await mountMcpServer(name, conf);
|
|
@@ -15836,7 +16323,7 @@ ${task}`;
|
|
|
15836
16323
|
}
|
|
15837
16324
|
const m = mounted.splice(idx, 1)[0];
|
|
15838
16325
|
remountMcpTools();
|
|
15839
|
-
await m.client.close().catch((e) =>
|
|
16326
|
+
await m.client.close().catch((e) => log27.debug("mcp close failed", e));
|
|
15840
16327
|
err(dim(` removed "${name}"
|
|
15841
16328
|
`));
|
|
15842
16329
|
return;
|
|
@@ -16039,16 +16526,16 @@ ${task}`;
|
|
|
16039
16526
|
try {
|
|
16040
16527
|
return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
16041
16528
|
} catch (e) {
|
|
16042
|
-
|
|
16529
|
+
log27.debug("completion readdir failed", absDir, e);
|
|
16043
16530
|
return null;
|
|
16044
16531
|
}
|
|
16045
16532
|
};
|
|
16046
16533
|
const agentCommandNames = () => [...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
|
|
16047
16534
|
const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...agentCommandNames()];
|
|
16048
|
-
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;
|
|
16049
16536
|
const suggest = (lineToCursor) => {
|
|
16050
16537
|
const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), agentCommands: agentCommandNames(), listDir, bangHistory: history });
|
|
16051
|
-
return { hits, token, describe };
|
|
16538
|
+
return { hits, token, describe: describe2 };
|
|
16052
16539
|
};
|
|
16053
16540
|
const editor = createLineEditor(process.stderr);
|
|
16054
16541
|
editorRef = editor;
|
|
@@ -16175,7 +16662,7 @@ ${out}
|
|
|
16175
16662
|
return;
|
|
16176
16663
|
}
|
|
16177
16664
|
await refreshCatalogs().catch((e) => {
|
|
16178
|
-
|
|
16665
|
+
log27.debug("catalog refresh failed", e);
|
|
16179
16666
|
});
|
|
16180
16667
|
const sk = skills.find((x) => x.name === name);
|
|
16181
16668
|
if (sk) {
|
|
@@ -16663,7 +17150,7 @@ async function main() {
|
|
|
16663
17150
|
});
|
|
16664
17151
|
if (args.task) {
|
|
16665
17152
|
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join14(cwd, ".agent", "mcp-auth.json") }));
|
|
16666
|
-
const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted));
|
|
17153
|
+
const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted), mounted.map((m) => m.name));
|
|
16667
17154
|
const store = new SessionStore(cwd);
|
|
16668
17155
|
const session = startSession(args, store, agent, cwd);
|
|
16669
17156
|
process.once("SIGINT", () => activeTurn?.abort());
|